fix issues with retweet info, adjust spacing
This commit is contained in:
commit
45ee170221
|
@ -11,7 +11,7 @@ module.exports = {
|
||||||
'html'
|
'html'
|
||||||
],
|
],
|
||||||
// add your custom rules here
|
// add your custom rules here
|
||||||
'rules': {
|
rules: {
|
||||||
// allow paren-less arrow functions
|
// allow paren-less arrow functions
|
||||||
'arrow-parens': 0,
|
'arrow-parens': 0,
|
||||||
// allow async-await
|
// allow async-await
|
||||||
|
|
|
@ -17,6 +17,7 @@
|
||||||
"babel-plugin-add-module-exports": "^0.2.1",
|
"babel-plugin-add-module-exports": "^0.2.1",
|
||||||
"babel-plugin-lodash": "^3.2.11",
|
"babel-plugin-lodash": "^3.2.11",
|
||||||
"chromatism": "^3.0.0",
|
"chromatism": "^3.0.0",
|
||||||
|
"cropperjs": "^1.4.3",
|
||||||
"diff": "^3.0.1",
|
"diff": "^3.0.1",
|
||||||
"karma-mocha-reporter": "^2.2.1",
|
"karma-mocha-reporter": "^2.2.1",
|
||||||
"localforage": "^1.5.0",
|
"localforage": "^1.5.0",
|
||||||
|
@ -27,6 +28,7 @@
|
||||||
"sass-loader": "^4.0.2",
|
"sass-loader": "^4.0.2",
|
||||||
"vue": "^2.5.13",
|
"vue": "^2.5.13",
|
||||||
"vue-chat-scroll": "^1.2.1",
|
"vue-chat-scroll": "^1.2.1",
|
||||||
|
"vue-compose": "^0.7.1",
|
||||||
"vue-i18n": "^7.3.2",
|
"vue-i18n": "^7.3.2",
|
||||||
"vue-router": "^3.0.1",
|
"vue-router": "^3.0.1",
|
||||||
"vue-template-compiler": "^2.3.4",
|
"vue-template-compiler": "^2.3.4",
|
||||||
|
|
10
src/App.js
10
src/App.js
|
@ -66,12 +66,16 @@ export default {
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
logo () { return this.$store.state.instance.logo },
|
logo () { return this.$store.state.instance.logo },
|
||||||
style () {
|
bgStyle () {
|
||||||
return {
|
return {
|
||||||
'--body-background-image': `url(${this.background})`,
|
|
||||||
'background-image': `url(${this.background})`
|
'background-image': `url(${this.background})`
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
bgAppStyle () {
|
||||||
|
return {
|
||||||
|
'--body-background-image': `url(${this.background})`
|
||||||
|
}
|
||||||
|
},
|
||||||
sitename () { return this.$store.state.instance.name },
|
sitename () { return this.$store.state.instance.name },
|
||||||
chat () { return this.$store.state.chat.channel.state === 'joined' },
|
chat () { return this.$store.state.chat.channel.state === 'joined' },
|
||||||
suggestionsEnabled () { return this.$store.state.instance.suggestionsEnabled },
|
suggestionsEnabled () { return this.$store.state.instance.suggestionsEnabled },
|
||||||
|
@ -82,7 +86,7 @@ export default {
|
||||||
unseenNotificationsCount () {
|
unseenNotificationsCount () {
|
||||||
return this.unseenNotifications.length
|
return this.unseenNotifications.length
|
||||||
},
|
},
|
||||||
showFeaturesPanel () { return this.$store.state.config.showFeaturesPanel }
|
showFeaturesPanel () { return this.$store.state.instance.showFeaturesPanel }
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
scrollToTop () {
|
scrollToTop () {
|
||||||
|
|
39
src/App.scss
39
src/App.scss
|
@ -1,15 +1,21 @@
|
||||||
@import './_variables.scss';
|
@import './_variables.scss';
|
||||||
|
|
||||||
#app {
|
#app {
|
||||||
background-size: cover;
|
|
||||||
background-attachment: fixed;
|
|
||||||
background-repeat: no-repeat;
|
|
||||||
background-position: 0 50px;
|
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.app-bg-wrapper {
|
||||||
|
position: fixed;
|
||||||
|
z-index: -1;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
background-size: cover;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: 0 50%;
|
||||||
|
}
|
||||||
|
|
||||||
i {
|
i {
|
||||||
user-select: none;
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
@ -175,8 +181,7 @@ input, textarea, .select {
|
||||||
color: $fallback--text;
|
color: $fallback--text;
|
||||||
color: var(--text, $fallback--text);
|
color: var(--text, $fallback--text);
|
||||||
}
|
}
|
||||||
&:disabled,
|
&:disabled {
|
||||||
{
|
|
||||||
&,
|
&,
|
||||||
& + label,
|
& + label,
|
||||||
& + label::before {
|
& + label::before {
|
||||||
|
@ -643,10 +648,6 @@ nav {
|
||||||
color: var(--lightText, $fallback--lightText);
|
color: var(--lightText, $fallback--lightText);
|
||||||
}
|
}
|
||||||
|
|
||||||
.text-format {
|
|
||||||
float: right;
|
|
||||||
}
|
|
||||||
|
|
||||||
div {
|
div {
|
||||||
padding-top: 5px;
|
padding-top: 5px;
|
||||||
}
|
}
|
||||||
|
@ -719,3 +720,21 @@ nav {
|
||||||
margin-right: 0.8em;
|
margin-right: 0.8em;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.login-hint {
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
@media all and (min-width: 801px) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 1em 0px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn.btn-default {
|
||||||
|
min-height: 28px;
|
||||||
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
<template>
|
<template>
|
||||||
<div id="app" v-bind:style="style">
|
<div id="app" v-bind:style="bgAppStyle">
|
||||||
|
<div class="app-bg-wrapper" v-bind:style="bgStyle"></div>
|
||||||
<nav class='nav-bar container' @click="scrollToTop()" id="nav">
|
<nav class='nav-bar container' @click="scrollToTop()" id="nav">
|
||||||
<div class='logo' :style='logoBgStyle'>
|
<div class='logo' :style='logoBgStyle'>
|
||||||
<div class='mask' :style='logoMaskStyle'></div>
|
<div class='mask' :style='logoMaskStyle'></div>
|
||||||
|
@ -37,6 +38,11 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="main">
|
<div class="main">
|
||||||
|
<div v-if="!currentUser" class="login-hint panel panel-default">
|
||||||
|
<router-link :to="{ name: 'login' }" class="panel-body">
|
||||||
|
{{ $t("login.hint") }}
|
||||||
|
</router-link>
|
||||||
|
</div>
|
||||||
<transition name="fade">
|
<transition name="fade">
|
||||||
<router-view></router-view>
|
<router-view></router-view>
|
||||||
</transition>
|
</transition>
|
||||||
|
|
|
@ -55,10 +55,10 @@ const afterStoreSetup = ({ store, i18n }) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
copyInstanceOption('nsfwCensorImage')
|
copyInstanceOption('nsfwCensorImage')
|
||||||
copyInstanceOption('theme')
|
|
||||||
copyInstanceOption('background')
|
copyInstanceOption('background')
|
||||||
copyInstanceOption('hidePostStats')
|
copyInstanceOption('hidePostStats')
|
||||||
copyInstanceOption('hideUserStats')
|
copyInstanceOption('hideUserStats')
|
||||||
|
copyInstanceOption('hideFilteredStatuses')
|
||||||
copyInstanceOption('logo')
|
copyInstanceOption('logo')
|
||||||
|
|
||||||
store.dispatch('setInstanceOption', {
|
store.dispatch('setInstanceOption', {
|
||||||
|
@ -84,8 +84,10 @@ const afterStoreSetup = ({ store, i18n }) => {
|
||||||
copyInstanceOption('loginMethod')
|
copyInstanceOption('loginMethod')
|
||||||
copyInstanceOption('scopeCopy')
|
copyInstanceOption('scopeCopy')
|
||||||
copyInstanceOption('subjectLineBehavior')
|
copyInstanceOption('subjectLineBehavior')
|
||||||
|
copyInstanceOption('postContentType')
|
||||||
copyInstanceOption('alwaysShowSubjectInput')
|
copyInstanceOption('alwaysShowSubjectInput')
|
||||||
copyInstanceOption('noAttachmentLinks')
|
copyInstanceOption('noAttachmentLinks')
|
||||||
|
copyInstanceOption('showFeaturesPanel')
|
||||||
|
|
||||||
if ((config.chatDisabled)) {
|
if ((config.chatDisabled)) {
|
||||||
store.dispatch('disableChat')
|
store.dispatch('disableChat')
|
||||||
|
@ -93,6 +95,9 @@ const afterStoreSetup = ({ store, i18n }) => {
|
||||||
store.dispatch('initializeSocket')
|
store.dispatch('initializeSocket')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return store.dispatch('setTheme', config['theme'])
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
const router = new VueRouter({
|
const router = new VueRouter({
|
||||||
mode: 'history',
|
mode: 'history',
|
||||||
routes: routes(store),
|
routes: routes(store),
|
||||||
|
|
|
@ -9,7 +9,7 @@ const About = {
|
||||||
TermsOfServicePanel
|
TermsOfServicePanel
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
showFeaturesPanel () { return this.$store.state.config.showFeaturesPanel }
|
showFeaturesPanel () { return this.$store.state.instance.showFeaturesPanel }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,28 @@
|
||||||
|
import UserCardContent from '../user_card_content/user_card_content.vue'
|
||||||
|
import UserAvatar from '../user_avatar/user_avatar.vue'
|
||||||
|
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
|
||||||
|
|
||||||
|
const BasicUserCard = {
|
||||||
|
props: [
|
||||||
|
'user'
|
||||||
|
],
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
userExpanded: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
UserCardContent,
|
||||||
|
UserAvatar
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
toggleUserExpanded () {
|
||||||
|
this.userExpanded = !this.userExpanded
|
||||||
|
},
|
||||||
|
userProfileLink (user) {
|
||||||
|
return generateProfileLink(user.id, user.screen_name, this.$store.state.instance.restrictedNicknames)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default BasicUserCard
|
|
@ -0,0 +1,92 @@
|
||||||
|
<template>
|
||||||
|
<div class="user-card">
|
||||||
|
<router-link :to="userProfileLink(user)">
|
||||||
|
<UserAvatar class="avatar" :compact="true" @click.prevent.native="toggleUserExpanded" :src="user.profile_image_url"/>
|
||||||
|
</router-link>
|
||||||
|
<div class="user-card-expanded-content" v-if="userExpanded">
|
||||||
|
<user-card-content :user="user" :switcher="false"></user-card-content>
|
||||||
|
</div>
|
||||||
|
<div class="user-card-collapsed-content" v-else>
|
||||||
|
<div class="user-card-primary-area">
|
||||||
|
<div :title="user.name" class="user-name">
|
||||||
|
<span v-if="user.name_html" v-html="user.name_html"></span>
|
||||||
|
<span v-else>{{ user.name }}</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<router-link class='user-screen-name' :to="userProfileLink(user)">
|
||||||
|
@{{user.screen_name}}
|
||||||
|
</router-link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="user-card-secondary-area">
|
||||||
|
<slot name="secondary-area"></slot>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script src="./basic_user_card.js"></script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
@import '../../_variables.scss';
|
||||||
|
|
||||||
|
.user-card {
|
||||||
|
display: flex;
|
||||||
|
flex: 1 0;
|
||||||
|
padding-top: 0.6em;
|
||||||
|
padding-right: 1em;
|
||||||
|
padding-bottom: 0.6em;
|
||||||
|
padding-left: 1em;
|
||||||
|
border-bottom: 1px solid;
|
||||||
|
margin: 0;
|
||||||
|
border-bottom-color: $fallback--border;
|
||||||
|
border-bottom-color: var(--border, $fallback--border);
|
||||||
|
|
||||||
|
&-collapsed-content {
|
||||||
|
margin-left: 0.7em;
|
||||||
|
text-align: left;
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-primary-area {
|
||||||
|
flex: 1;
|
||||||
|
.user-name {
|
||||||
|
img {
|
||||||
|
object-fit: contain;
|
||||||
|
height: 16px;
|
||||||
|
width: 16px;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&-secondary-area {
|
||||||
|
flex: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-expanded-content {
|
||||||
|
flex: 1;
|
||||||
|
margin: 0.2em 0 0 0.7em;
|
||||||
|
border-radius: $fallback--panelRadius;
|
||||||
|
border-radius: var(--panelRadius, $fallback--panelRadius);
|
||||||
|
border-style: solid;
|
||||||
|
border-color: $fallback--border;
|
||||||
|
border-color: var(--border, $fallback--border);
|
||||||
|
border-width: 1px;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
.panel-heading {
|
||||||
|
background: transparent;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,37 @@
|
||||||
|
import BasicUserCard from '../basic_user_card/basic_user_card.vue'
|
||||||
|
|
||||||
|
const BlockCard = {
|
||||||
|
props: ['userId'],
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
progress: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
user () {
|
||||||
|
return this.$store.getters.userById(this.userId)
|
||||||
|
},
|
||||||
|
blocked () {
|
||||||
|
return this.user.statusnet_blocking
|
||||||
|
}
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
BasicUserCard
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
unblockUser () {
|
||||||
|
this.progress = true
|
||||||
|
this.$store.dispatch('unblockUser', this.user.id).then(() => {
|
||||||
|
this.progress = false
|
||||||
|
})
|
||||||
|
},
|
||||||
|
blockUser () {
|
||||||
|
this.progress = true
|
||||||
|
this.$store.dispatch('blockUser', this.user.id).then(() => {
|
||||||
|
this.progress = false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default BlockCard
|
|
@ -0,0 +1,24 @@
|
||||||
|
<template>
|
||||||
|
<basic-user-card :user="user">
|
||||||
|
<template slot="secondary-area">
|
||||||
|
<button class="btn btn-default" @click="unblockUser" :disabled="progress" v-if="blocked">
|
||||||
|
<template v-if="progress">
|
||||||
|
{{ $t('user_card.unblock_progress') }}
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
{{ $t('user_card.unblock') }}
|
||||||
|
</template>
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-default" @click="blockUser" :disabled="progress" v-else>
|
||||||
|
<template v-if="progress">
|
||||||
|
{{ $t('user_card.block_progress') }}
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
{{ $t('user_card.block') }}
|
||||||
|
</template>
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
</basic-user-card>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script src="./block_card.js"></script>
|
|
@ -3,8 +3,8 @@
|
||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
<div class="panel-heading timeline-heading" :class="{ 'chat-heading': floating }" @click.stop.prevent="togglePanel">
|
<div class="panel-heading timeline-heading" :class="{ 'chat-heading': floating }" @click.stop.prevent="togglePanel">
|
||||||
<div class="title">
|
<div class="title">
|
||||||
{{$t('chat.title')}}
|
<span>{{$t('chat.title')}}</span>
|
||||||
<i class="icon-cancel" style="float: right;" v-if="floating"></i>
|
<i class="icon-cancel" v-if="floating"></i>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="chat-window" v-chat-scroll>
|
<div class="chat-window" v-chat-scroll>
|
||||||
|
@ -98,4 +98,11 @@
|
||||||
resize: none;
|
resize: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.chat-panel {
|
||||||
|
.title {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -9,9 +9,9 @@ const sortById = (a, b) => {
|
||||||
if (isSeqA && isSeqB) {
|
if (isSeqA && isSeqB) {
|
||||||
return seqA < seqB ? -1 : 1
|
return seqA < seqB ? -1 : 1
|
||||||
} else if (isSeqA && !isSeqB) {
|
} else if (isSeqA && !isSeqB) {
|
||||||
return 1
|
|
||||||
} else if (!isSeqA && isSeqB) {
|
|
||||||
return -1
|
return -1
|
||||||
|
} else if (!isSeqA && isSeqB) {
|
||||||
|
return 1
|
||||||
} else {
|
} else {
|
||||||
return a.id < b.id ? -1 : 1
|
return a.id < b.id ? -1 : 1
|
||||||
}
|
}
|
||||||
|
@ -36,6 +36,13 @@ const conversation = {
|
||||||
status () {
|
status () {
|
||||||
return this.statusoid
|
return this.statusoid
|
||||||
},
|
},
|
||||||
|
statusId () {
|
||||||
|
if (this.statusoid.retweeted_status) {
|
||||||
|
return this.statusoid.retweeted_status.id
|
||||||
|
} else {
|
||||||
|
return this.statusoid.id
|
||||||
|
}
|
||||||
|
},
|
||||||
conversation () {
|
conversation () {
|
||||||
if (!this.status) {
|
if (!this.status) {
|
||||||
return []
|
return []
|
||||||
|
@ -79,7 +86,7 @@ const conversation = {
|
||||||
const conversationId = this.status.statusnet_conversation_id
|
const conversationId = this.status.statusnet_conversation_id
|
||||||
this.$store.state.api.backendInteractor.fetchConversation({id: conversationId})
|
this.$store.state.api.backendInteractor.fetchConversation({id: conversationId})
|
||||||
.then((statuses) => this.$store.dispatch('addNewStatuses', { statuses }))
|
.then((statuses) => this.$store.dispatch('addNewStatuses', { statuses }))
|
||||||
.then(() => this.setHighlight(this.statusoid.id))
|
.then(() => this.setHighlight(this.statusId))
|
||||||
} else {
|
} else {
|
||||||
const id = this.$route.params.id
|
const id = this.$route.params.id
|
||||||
this.$store.state.api.backendInteractor.fetchStatus({id})
|
this.$store.state.api.backendInteractor.fetchStatus({id})
|
||||||
|
@ -91,11 +98,7 @@ const conversation = {
|
||||||
return this.replies[id] || []
|
return this.replies[id] || []
|
||||||
},
|
},
|
||||||
focused (id) {
|
focused (id) {
|
||||||
if (this.statusoid.retweeted_status) {
|
return id === this.statusId
|
||||||
return (id === this.statusoid.retweeted_status.id)
|
|
||||||
} else {
|
|
||||||
return (id === this.statusoid.id)
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
setHighlight (id) {
|
setHighlight (id) {
|
||||||
this.highlight = id
|
this.highlight = id
|
||||||
|
|
|
@ -25,6 +25,9 @@ const FollowList = {
|
||||||
},
|
},
|
||||||
entries () {
|
entries () {
|
||||||
return this.showFollowers ? this.user.followers : this.user.friends
|
return this.showFollowers ? this.user.followers : this.user.friends
|
||||||
|
},
|
||||||
|
showFollowsYou () {
|
||||||
|
return !this.showFollowers || (this.showFollowers && this.userId !== this.$store.state.users.currentUser.id)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
@ -54,6 +57,9 @@ const FollowList = {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
watch: {
|
||||||
|
'user': 'fetchEntries'
|
||||||
|
},
|
||||||
components: {
|
components: {
|
||||||
UserCard
|
UserCard
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
<user-card
|
<user-card
|
||||||
v-for="entry in entries"
|
v-for="entry in entries"
|
||||||
:key="entry.id" :user="entry"
|
:key="entry.id" :user="entry"
|
||||||
:showFollows="true"
|
:noFollowsYou="!showFollowsYou"
|
||||||
/>
|
/>
|
||||||
<div class="text-center panel-footer">
|
<div class="text-center panel-footer">
|
||||||
<a v-if="error" @click="fetchEntries" class="alert error">
|
<a v-if="error" @click="fetchEntries" class="alert error">
|
||||||
|
|
|
@ -0,0 +1,128 @@
|
||||||
|
import Cropper from 'cropperjs'
|
||||||
|
import 'cropperjs/dist/cropper.css'
|
||||||
|
|
||||||
|
const ImageCropper = {
|
||||||
|
props: {
|
||||||
|
trigger: {
|
||||||
|
type: [String, window.Element],
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
submitHandler: {
|
||||||
|
type: Function,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
cropperOptions: {
|
||||||
|
type: Object,
|
||||||
|
default () {
|
||||||
|
return {
|
||||||
|
aspectRatio: 1,
|
||||||
|
autoCropArea: 1,
|
||||||
|
viewMode: 1,
|
||||||
|
movable: false,
|
||||||
|
zoomable: false,
|
||||||
|
guides: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mimes: {
|
||||||
|
type: String,
|
||||||
|
default: 'image/png, image/gif, image/jpeg, image/bmp, image/x-icon'
|
||||||
|
},
|
||||||
|
saveButtonLabel: {
|
||||||
|
type: String
|
||||||
|
},
|
||||||
|
cancelButtonLabel: {
|
||||||
|
type: String
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
cropper: undefined,
|
||||||
|
dataUrl: undefined,
|
||||||
|
filename: undefined,
|
||||||
|
submitting: false,
|
||||||
|
submitError: null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
saveText () {
|
||||||
|
return this.saveButtonLabel || this.$t('image_cropper.save')
|
||||||
|
},
|
||||||
|
cancelText () {
|
||||||
|
return this.cancelButtonLabel || this.$t('image_cropper.cancel')
|
||||||
|
},
|
||||||
|
submitErrorMsg () {
|
||||||
|
return this.submitError && this.submitError instanceof Error ? this.submitError.toString() : this.submitError
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
destroy () {
|
||||||
|
if (this.cropper) {
|
||||||
|
this.cropper.destroy()
|
||||||
|
}
|
||||||
|
this.$refs.input.value = ''
|
||||||
|
this.dataUrl = undefined
|
||||||
|
this.$emit('close')
|
||||||
|
},
|
||||||
|
submit () {
|
||||||
|
this.submitting = true
|
||||||
|
this.avatarUploadError = null
|
||||||
|
this.submitHandler(this.cropper, this.filename)
|
||||||
|
.then(() => this.destroy())
|
||||||
|
.catch((err) => {
|
||||||
|
this.submitError = err
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
this.submitting = false
|
||||||
|
})
|
||||||
|
},
|
||||||
|
pickImage () {
|
||||||
|
this.$refs.input.click()
|
||||||
|
},
|
||||||
|
createCropper () {
|
||||||
|
this.cropper = new Cropper(this.$refs.img, this.cropperOptions)
|
||||||
|
},
|
||||||
|
getTriggerDOM () {
|
||||||
|
return typeof this.trigger === 'object' ? this.trigger : document.querySelector(this.trigger)
|
||||||
|
},
|
||||||
|
readFile () {
|
||||||
|
const fileInput = this.$refs.input
|
||||||
|
if (fileInput.files != null && fileInput.files[0] != null) {
|
||||||
|
let reader = new window.FileReader()
|
||||||
|
reader.onload = (e) => {
|
||||||
|
this.dataUrl = e.target.result
|
||||||
|
this.$emit('open')
|
||||||
|
}
|
||||||
|
reader.readAsDataURL(fileInput.files[0])
|
||||||
|
this.filename = fileInput.files[0].name || 'unknown'
|
||||||
|
this.$emit('changed', fileInput.files[0], reader)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
clearError () {
|
||||||
|
this.submitError = null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted () {
|
||||||
|
// listen for click event on trigger
|
||||||
|
const trigger = this.getTriggerDOM()
|
||||||
|
if (!trigger) {
|
||||||
|
this.$emit('error', 'No image make trigger found.', 'user')
|
||||||
|
} else {
|
||||||
|
trigger.addEventListener('click', this.pickImage)
|
||||||
|
}
|
||||||
|
// listen for input file changes
|
||||||
|
const fileInput = this.$refs.input
|
||||||
|
fileInput.addEventListener('change', this.readFile)
|
||||||
|
},
|
||||||
|
beforeDestroy: function () {
|
||||||
|
// remove the event listeners
|
||||||
|
const trigger = this.getTriggerDOM()
|
||||||
|
if (trigger) {
|
||||||
|
trigger.removeEventListener('click', this.pickImage)
|
||||||
|
}
|
||||||
|
const fileInput = this.$refs.input
|
||||||
|
fileInput.removeEventListener('change', this.readFile)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ImageCropper
|
|
@ -0,0 +1,42 @@
|
||||||
|
<template>
|
||||||
|
<div class="image-cropper">
|
||||||
|
<div v-if="dataUrl">
|
||||||
|
<div class="image-cropper-image-container">
|
||||||
|
<img ref="img" :src="dataUrl" alt="" @load.stop="createCropper" />
|
||||||
|
</div>
|
||||||
|
<div class="image-cropper-buttons-wrapper">
|
||||||
|
<button class="btn" type="button" :disabled="submitting" @click="submit" v-text="saveText"></button>
|
||||||
|
<button class="btn" type="button" :disabled="submitting" @click="destroy" v-text="cancelText"></button>
|
||||||
|
<i class="icon-spin4 animate-spin" v-if="submitting"></i>
|
||||||
|
</div>
|
||||||
|
<div class="alert error" v-if="submitError">
|
||||||
|
{{submitErrorMsg}}
|
||||||
|
<i class="button-icon icon-cancel" @click="clearError"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<input ref="input" type="file" class="image-cropper-img-input" :accept="mimes">
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script src="./image_cropper.js"></script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.image-cropper {
|
||||||
|
&-img-input {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-image-container {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
img {
|
||||||
|
display: block;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&-buttons-wrapper {
|
||||||
|
margin-top: 15px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -11,27 +11,62 @@ const MediaModal = {
|
||||||
showing () {
|
showing () {
|
||||||
return this.$store.state.mediaViewer.activated
|
return this.$store.state.mediaViewer.activated
|
||||||
},
|
},
|
||||||
|
media () {
|
||||||
|
return this.$store.state.mediaViewer.media
|
||||||
|
},
|
||||||
currentIndex () {
|
currentIndex () {
|
||||||
return this.$store.state.mediaViewer.currentIndex
|
return this.$store.state.mediaViewer.currentIndex
|
||||||
},
|
},
|
||||||
currentMedia () {
|
currentMedia () {
|
||||||
return this.$store.state.mediaViewer.media[this.currentIndex]
|
return this.media[this.currentIndex]
|
||||||
|
},
|
||||||
|
canNavigate () {
|
||||||
|
return this.media.length > 1
|
||||||
},
|
},
|
||||||
type () {
|
type () {
|
||||||
return this.currentMedia ? fileTypeService.fileType(this.currentMedia.mimetype) : null
|
return this.currentMedia ? fileTypeService.fileType(this.currentMedia.mimetype) : null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
created () {
|
|
||||||
document.addEventListener('keyup', e => {
|
|
||||||
if (e.keyCode === 27 && this.showing) { // escape
|
|
||||||
this.hide()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
},
|
|
||||||
methods: {
|
methods: {
|
||||||
hide () {
|
hide () {
|
||||||
this.$store.dispatch('closeMediaViewer')
|
this.$store.dispatch('closeMediaViewer')
|
||||||
|
},
|
||||||
|
goPrev () {
|
||||||
|
if (this.canNavigate) {
|
||||||
|
const prevIndex = this.currentIndex === 0 ? this.media.length - 1 : (this.currentIndex - 1)
|
||||||
|
this.$store.dispatch('setCurrent', this.media[prevIndex])
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
goNext () {
|
||||||
|
if (this.canNavigate) {
|
||||||
|
const nextIndex = this.currentIndex === this.media.length - 1 ? 0 : (this.currentIndex + 1)
|
||||||
|
this.$store.dispatch('setCurrent', this.media[nextIndex])
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handleKeyupEvent (e) {
|
||||||
|
if (this.showing && e.keyCode === 27) { // escape
|
||||||
|
this.hide()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handleKeydownEvent (e) {
|
||||||
|
if (!this.showing) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (e.keyCode === 39) { // arrow right
|
||||||
|
this.goNext()
|
||||||
|
} else if (e.keyCode === 37) { // arrow left
|
||||||
|
this.goPrev()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted () {
|
||||||
|
document.addEventListener('keyup', this.handleKeyupEvent)
|
||||||
|
document.addEventListener('keydown', this.handleKeydownEvent)
|
||||||
|
},
|
||||||
|
destroyed () {
|
||||||
|
document.removeEventListener('keyup', this.handleKeyupEvent)
|
||||||
|
document.removeEventListener('keydown', this.handleKeydownEvent)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -8,6 +8,22 @@
|
||||||
:controls="true"
|
:controls="true"
|
||||||
@click.stop.native="">
|
@click.stop.native="">
|
||||||
</VideoAttachment>
|
</VideoAttachment>
|
||||||
|
<button
|
||||||
|
:title="$t('media_modal.previous')"
|
||||||
|
class="modal-view-button-arrow modal-view-button-arrow--prev"
|
||||||
|
v-if="canNavigate"
|
||||||
|
@click.stop.prevent="goPrev"
|
||||||
|
>
|
||||||
|
<i class="icon-left-open arrow-icon" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
:title="$t('media_modal.next')"
|
||||||
|
class="modal-view-button-arrow modal-view-button-arrow--next"
|
||||||
|
v-if="canNavigate"
|
||||||
|
@click.stop.prevent="goNext"
|
||||||
|
>
|
||||||
|
<i class="icon-right-open arrow-icon" />
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -19,15 +35,29 @@
|
||||||
.modal-view {
|
.modal-view {
|
||||||
z-index: 1000;
|
z-index: 1000;
|
||||||
position: fixed;
|
position: fixed;
|
||||||
width: 100vw;
|
|
||||||
height: 100vh;
|
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
background-color: rgba(0, 0, 0, 0.5);
|
background-color: rgba(0, 0, 0, 0.5);
|
||||||
cursor: pointer;
|
|
||||||
|
&:hover {
|
||||||
|
.modal-view-button-arrow {
|
||||||
|
opacity: 0.75;
|
||||||
|
|
||||||
|
&:focus,
|
||||||
|
&:hover {
|
||||||
|
outline: none;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
&:hover {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-image {
|
.modal-image {
|
||||||
|
@ -35,4 +65,49 @@
|
||||||
max-height: 90%;
|
max-height: 90%;
|
||||||
box-shadow: 0px 5px 15px 0 rgba(0, 0, 0, 0.5);
|
box-shadow: 0px 5px 15px 0 rgba(0, 0, 0, 0.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.modal-view-button-arrow {
|
||||||
|
position: absolute;
|
||||||
|
display: block;
|
||||||
|
top: 50%;
|
||||||
|
margin-top: -50px;
|
||||||
|
width: 70px;
|
||||||
|
height: 100px;
|
||||||
|
border: 0;
|
||||||
|
padding: 0;
|
||||||
|
opacity: 0;
|
||||||
|
box-shadow: none;
|
||||||
|
background: none;
|
||||||
|
appearance: none;
|
||||||
|
overflow: visible;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: opacity 333ms cubic-bezier(.4,0,.22,1);
|
||||||
|
|
||||||
|
.arrow-icon {
|
||||||
|
position: absolute;
|
||||||
|
top: 35px;
|
||||||
|
height: 30px;
|
||||||
|
width: 32px;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 30px;
|
||||||
|
color: #FFF;
|
||||||
|
text-align: center;
|
||||||
|
background-color: rgba(0,0,0,.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
&--prev {
|
||||||
|
left: 0;
|
||||||
|
.arrow-icon {
|
||||||
|
left: 6px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&--next {
|
||||||
|
right: 0;
|
||||||
|
.arrow-icon {
|
||||||
|
right: 6px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -0,0 +1,37 @@
|
||||||
|
import BasicUserCard from '../basic_user_card/basic_user_card.vue'
|
||||||
|
|
||||||
|
const MuteCard = {
|
||||||
|
props: ['userId'],
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
progress: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
user () {
|
||||||
|
return this.$store.getters.userById(this.userId)
|
||||||
|
},
|
||||||
|
muted () {
|
||||||
|
return this.user.muted
|
||||||
|
}
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
BasicUserCard
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
unmuteUser () {
|
||||||
|
this.progress = true
|
||||||
|
this.$store.dispatch('unmuteUser', this.user.id).then(() => {
|
||||||
|
this.progress = false
|
||||||
|
})
|
||||||
|
},
|
||||||
|
muteUser () {
|
||||||
|
this.progress = true
|
||||||
|
this.$store.dispatch('muteUser', this.user.id).then(() => {
|
||||||
|
this.progress = false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default MuteCard
|
|
@ -0,0 +1,24 @@
|
||||||
|
<template>
|
||||||
|
<basic-user-card :user="user">
|
||||||
|
<template slot="secondary-area">
|
||||||
|
<button class="btn btn-default" @click="unmuteUser" :disabled="progress" v-if="muted">
|
||||||
|
<template v-if="progress">
|
||||||
|
{{ $t('user_card.unmute_progress') }}
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
{{ $t('user_card.unmute') }}
|
||||||
|
</template>
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-default" @click="muteUser" :disabled="progress" v-else>
|
||||||
|
<template v-if="progress">
|
||||||
|
{{ $t('user_card.mute_progress') }}
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
{{ $t('user_card.mute') }}
|
||||||
|
</template>
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
</basic-user-card>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script src="./mute_card.js"></script>
|
|
@ -20,6 +20,9 @@
|
||||||
<li v-if='currentUser && currentUser.locked'>
|
<li v-if='currentUser && currentUser.locked'>
|
||||||
<router-link :to="{ name: 'friend-requests' }">
|
<router-link :to="{ name: 'friend-requests' }">
|
||||||
{{ $t("nav.friend_requests")}}
|
{{ $t("nav.friend_requests")}}
|
||||||
|
<span v-if='currentUser.follow_request_count > 0' class="badge follow-request-count">
|
||||||
|
{{currentUser.follow_request_count}}
|
||||||
|
</span>
|
||||||
</router-link>
|
</router-link>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
|
@ -52,6 +55,12 @@
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.follow-request-count {
|
||||||
|
margin: -6px 10px;
|
||||||
|
background-color: $fallback--bg;
|
||||||
|
background-color: var(--input, $fallback--faint);
|
||||||
|
}
|
||||||
|
|
||||||
.nav-panel li {
|
.nav-panel li {
|
||||||
border-bottom: 1px solid;
|
border-bottom: 1px solid;
|
||||||
border-color: $fallback--border;
|
border-color: $fallback--border;
|
||||||
|
|
|
@ -103,6 +103,7 @@
|
||||||
flex: 1 1 0;
|
flex: 1 1 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: nowrap;
|
flex-wrap: nowrap;
|
||||||
|
justify-content: space-between;
|
||||||
|
|
||||||
.name-and-action {
|
.name-and-action {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
@ -123,8 +124,8 @@
|
||||||
object-fit: contain
|
object-fit: contain
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.timeago {
|
.timeago {
|
||||||
float: right;
|
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -56,6 +56,10 @@ const PostStatusForm = {
|
||||||
? this.copyMessageScope
|
? this.copyMessageScope
|
||||||
: this.$store.state.users.currentUser.default_scope
|
: this.$store.state.users.currentUser.default_scope
|
||||||
|
|
||||||
|
const contentType = typeof this.$store.state.config.postContentType === 'undefined'
|
||||||
|
? this.$store.state.instance.postContentType
|
||||||
|
: this.$store.state.config.postContentType
|
||||||
|
|
||||||
return {
|
return {
|
||||||
dropFiles: [],
|
dropFiles: [],
|
||||||
submitDisabled: false,
|
submitDisabled: false,
|
||||||
|
@ -65,10 +69,10 @@ const PostStatusForm = {
|
||||||
newStatus: {
|
newStatus: {
|
||||||
spoilerText: this.subject || '',
|
spoilerText: this.subject || '',
|
||||||
status: statusText,
|
status: statusText,
|
||||||
contentType: 'text/plain',
|
|
||||||
nsfw: false,
|
nsfw: false,
|
||||||
files: [],
|
files: [],
|
||||||
visibility: scope
|
visibility: scope,
|
||||||
|
contentType
|
||||||
},
|
},
|
||||||
caret: 0
|
caret: 0
|
||||||
}
|
}
|
||||||
|
|
|
@ -118,6 +118,14 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.post-status-form {
|
||||||
|
.visibility-tray {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
flex-direction: row-reverse;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.post-status-form, .login {
|
.post-status-form, .login {
|
||||||
.form-bottom {
|
.form-bottom {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
|
@ -7,7 +7,7 @@ const PublicAndExternalTimeline = {
|
||||||
timeline () { return this.$store.state.statuses.timelines.publicAndExternal }
|
timeline () { return this.$store.state.statuses.timelines.publicAndExternal }
|
||||||
},
|
},
|
||||||
created () {
|
created () {
|
||||||
this.$store.dispatch('startFetching', 'publicAndExternal')
|
this.$store.dispatch('startFetching', { timeline: 'publicAndExternal' })
|
||||||
},
|
},
|
||||||
destroyed () {
|
destroyed () {
|
||||||
this.$store.dispatch('stopFetching', 'publicAndExternal')
|
this.$store.dispatch('stopFetching', 'publicAndExternal')
|
||||||
|
|
|
@ -7,7 +7,7 @@ const PublicTimeline = {
|
||||||
timeline () { return this.$store.state.statuses.timelines.public }
|
timeline () { return this.$store.state.statuses.timelines.public }
|
||||||
},
|
},
|
||||||
created () {
|
created () {
|
||||||
this.$store.dispatch('startFetching', 'public')
|
this.$store.dispatch('startFetching', { timeline: 'public' })
|
||||||
},
|
},
|
||||||
destroyed () {
|
destroyed () {
|
||||||
this.$store.dispatch('stopFetching', 'public')
|
this.$store.dispatch('stopFetching', 'public')
|
||||||
|
|
|
@ -27,6 +27,11 @@ const settings = {
|
||||||
: user.hideUserStats,
|
: user.hideUserStats,
|
||||||
hideUserStatsDefault: this.$t('settings.values.' + instance.hideUserStats),
|
hideUserStatsDefault: this.$t('settings.values.' + instance.hideUserStats),
|
||||||
|
|
||||||
|
hideFilteredStatusesLocal: typeof user.hideFilteredStatuses === 'undefined'
|
||||||
|
? instance.hideFilteredStatuses
|
||||||
|
: user.hideFilteredStatuses,
|
||||||
|
hideFilteredStatusesDefault: this.$t('settings.values.' + instance.hideFilteredStatuses),
|
||||||
|
|
||||||
notificationVisibilityLocal: user.notificationVisibility,
|
notificationVisibilityLocal: user.notificationVisibility,
|
||||||
replyVisibilityLocal: user.replyVisibility,
|
replyVisibilityLocal: user.replyVisibility,
|
||||||
loopVideoLocal: user.loopVideo,
|
loopVideoLocal: user.loopVideo,
|
||||||
|
@ -46,6 +51,11 @@ const settings = {
|
||||||
: user.subjectLineBehavior,
|
: user.subjectLineBehavior,
|
||||||
subjectLineBehaviorDefault: instance.subjectLineBehavior,
|
subjectLineBehaviorDefault: instance.subjectLineBehavior,
|
||||||
|
|
||||||
|
postContentTypeLocal: typeof user.postContentType === 'undefined'
|
||||||
|
? instance.postContentType
|
||||||
|
: user.postContentType,
|
||||||
|
postContentTypeDefault: instance.postContentType,
|
||||||
|
|
||||||
alwaysShowSubjectInputLocal: typeof user.alwaysShowSubjectInput === 'undefined'
|
alwaysShowSubjectInputLocal: typeof user.alwaysShowSubjectInput === 'undefined'
|
||||||
? instance.alwaysShowSubjectInput
|
? instance.alwaysShowSubjectInput
|
||||||
: user.alwaysShowSubjectInput,
|
: user.alwaysShowSubjectInput,
|
||||||
|
@ -81,7 +91,8 @@ const settings = {
|
||||||
},
|
},
|
||||||
currentSaveStateNotice () {
|
currentSaveStateNotice () {
|
||||||
return this.$store.state.interface.settings.currentSaveStateNotice
|
return this.$store.state.interface.settings.currentSaveStateNotice
|
||||||
}
|
},
|
||||||
|
instanceSpecificPanelPresent () { return this.$store.state.instance.showInstanceSpecificPanel }
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
hideAttachmentsLocal (value) {
|
hideAttachmentsLocal (value) {
|
||||||
|
@ -96,6 +107,9 @@ const settings = {
|
||||||
hideUserStatsLocal (value) {
|
hideUserStatsLocal (value) {
|
||||||
this.$store.dispatch('setOption', { name: 'hideUserStats', value })
|
this.$store.dispatch('setOption', { name: 'hideUserStats', value })
|
||||||
},
|
},
|
||||||
|
hideFilteredStatusesLocal (value) {
|
||||||
|
this.$store.dispatch('setOption', { name: 'hideFilteredStatuses', value })
|
||||||
|
},
|
||||||
hideNsfwLocal (value) {
|
hideNsfwLocal (value) {
|
||||||
this.$store.dispatch('setOption', { name: 'hideNsfw', value })
|
this.$store.dispatch('setOption', { name: 'hideNsfw', value })
|
||||||
},
|
},
|
||||||
|
@ -157,6 +171,9 @@ const settings = {
|
||||||
subjectLineBehaviorLocal (value) {
|
subjectLineBehaviorLocal (value) {
|
||||||
this.$store.dispatch('setOption', { name: 'subjectLineBehavior', value })
|
this.$store.dispatch('setOption', { name: 'subjectLineBehavior', value })
|
||||||
},
|
},
|
||||||
|
postContentTypeLocal (value) {
|
||||||
|
this.$store.dispatch('setOption', { name: 'postContentType', value })
|
||||||
|
},
|
||||||
stopGifs (value) {
|
stopGifs (value) {
|
||||||
this.$store.dispatch('setOption', { name: 'stopGifs', value })
|
this.$store.dispatch('setOption', { name: 'stopGifs', value })
|
||||||
},
|
},
|
||||||
|
|
|
@ -27,7 +27,7 @@
|
||||||
<li>
|
<li>
|
||||||
<interface-language-switcher />
|
<interface-language-switcher />
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li v-if="instanceSpecificPanelPresent">
|
||||||
<input type="checkbox" id="hideISP" v-model="hideISPLocal">
|
<input type="checkbox" id="hideISP" v-model="hideISPLocal">
|
||||||
<label for="hideISP">{{$t('settings.hide_isp')}}</label>
|
<label for="hideISP">{{$t('settings.hide_isp')}}</label>
|
||||||
</li>
|
</li>
|
||||||
|
@ -100,6 +100,28 @@
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
|
<li>
|
||||||
|
<div>
|
||||||
|
{{$t('settings.post_status_content_type')}}
|
||||||
|
<label for="postContentType" class="select">
|
||||||
|
<select id="postContentType" v-model="postContentTypeLocal">
|
||||||
|
<option value="text/plain">
|
||||||
|
{{$t('settings.status_content_type_plain')}}
|
||||||
|
{{postContentTypeDefault == 'text/plain' ? $t('settings.instance_default_simple') : ''}}
|
||||||
|
</option>
|
||||||
|
<option value="text/html">
|
||||||
|
HTML
|
||||||
|
{{postContentTypeDefault == 'text/html' ? $t('settings.instance_default_simple') : ''}}
|
||||||
|
</option>
|
||||||
|
<option value="text/markdown">
|
||||||
|
Markdown
|
||||||
|
{{postContentTypeDefault == 'text/markdown' ? $t('settings.instance_default_simple') : ''}}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
<i class="icon-down-open"/>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -205,7 +227,6 @@
|
||||||
</label>
|
</label>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</label>
|
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
{{$t('settings.replies_in_timeline')}}
|
{{$t('settings.replies_in_timeline')}}
|
||||||
|
@ -232,11 +253,18 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="setting-item">
|
<div class="setting-item">
|
||||||
|
<div>
|
||||||
<p>{{$t('settings.filtering_explanation')}}</p>
|
<p>{{$t('settings.filtering_explanation')}}</p>
|
||||||
<textarea id="muteWords" v-model="muteWordsString"></textarea>
|
<textarea id="muteWords" v-model="muteWordsString"></textarea>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<input type="checkbox" id="hideFilteredStatuses" v-model="hideFilteredStatusesLocal">
|
||||||
|
<label for="hideFilteredStatuses">
|
||||||
|
{{$t('settings.hide_filtered_statuses')}} {{$t('settings.instance_default', { value: hideFilteredStatusesDefault })}}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</tab-switcher>
|
</tab-switcher>
|
||||||
</keep-alive>
|
</keep-alive>
|
||||||
</div>
|
</div>
|
||||||
|
@ -283,20 +311,6 @@
|
||||||
color: $fallback--cRed;
|
color: $fallback--cRed;
|
||||||
}
|
}
|
||||||
|
|
||||||
.old-avatar {
|
|
||||||
width: 128px;
|
|
||||||
border-radius: $fallback--avatarRadius;
|
|
||||||
border-radius: var(--avatarRadius, $fallback--avatarRadius);
|
|
||||||
}
|
|
||||||
|
|
||||||
.new-avatar {
|
|
||||||
object-fit: cover;
|
|
||||||
width: 128px;
|
|
||||||
height: 128px;
|
|
||||||
border-radius: $fallback--avatarRadius;
|
|
||||||
border-radius: var(--avatarRadius, $fallback--avatarRadius);
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn {
|
.btn {
|
||||||
min-height: 28px;
|
min-height: 28px;
|
||||||
min-width: 10em;
|
min-width: 10em;
|
||||||
|
|
|
@ -45,6 +45,10 @@
|
||||||
<li v-if="currentUser && currentUser.locked" @click="toggleDrawer">
|
<li v-if="currentUser && currentUser.locked" @click="toggleDrawer">
|
||||||
<router-link to='/friend-requests'>
|
<router-link to='/friend-requests'>
|
||||||
{{ $t("nav.friend_requests") }}
|
{{ $t("nav.friend_requests") }}
|
||||||
|
<span v-if='currentUser.follow_request_count > 0' class="badge follow-request-count">
|
||||||
|
{{currentUser.follow_request_count}}
|
||||||
|
</span>
|
||||||
|
|
||||||
</router-link>
|
</router-link>
|
||||||
</li>
|
</li>
|
||||||
<li @click="toggleDrawer">
|
<li @click="toggleDrawer">
|
||||||
|
|
|
@ -10,8 +10,8 @@ import LinkPreview from '../link-preview/link-preview.vue'
|
||||||
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
|
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
|
||||||
import fileType from 'src/services/file_type/file_type.service'
|
import fileType from 'src/services/file_type/file_type.service'
|
||||||
import { highlightClass, highlightStyle } from '../../services/user_highlighter/user_highlighter.js'
|
import { highlightClass, highlightStyle } from '../../services/user_highlighter/user_highlighter.js'
|
||||||
import { mentionMatchesUrl } from 'src/services/mention_matcher/mention_matcher.js'
|
import { mentionMatchesUrl, extractTagFromUrl } from 'src/services/matcher/matcher.service.js'
|
||||||
import { filter, find } from 'lodash'
|
import { filter, find, unescape } from 'lodash'
|
||||||
|
|
||||||
const Status = {
|
const Status = {
|
||||||
name: 'Status',
|
name: 'Status',
|
||||||
|
@ -110,6 +110,14 @@ const Status = {
|
||||||
return hits
|
return hits
|
||||||
},
|
},
|
||||||
muted () { return !this.unmuted && (this.status.user.muted || this.muteWordHits.length > 0) },
|
muted () { return !this.unmuted && (this.status.user.muted || this.muteWordHits.length > 0) },
|
||||||
|
hideFilteredStatuses () {
|
||||||
|
return typeof this.$store.state.config.hideFilteredStatuses === 'undefined'
|
||||||
|
? this.$store.state.instance.hideFilteredStatuses
|
||||||
|
: this.$store.state.config.hideFilteredStatuses
|
||||||
|
},
|
||||||
|
hideStatus () {
|
||||||
|
return (this.hideReply || this.deleted) || (this.muted && this.hideFilteredStatuses)
|
||||||
|
},
|
||||||
isFocused () {
|
isFocused () {
|
||||||
// retweet or root of an expanded conversation
|
// retweet or root of an expanded conversation
|
||||||
if (this.focused) {
|
if (this.focused) {
|
||||||
|
@ -201,14 +209,15 @@ const Status = {
|
||||||
},
|
},
|
||||||
replySubject () {
|
replySubject () {
|
||||||
if (!this.status.summary) return ''
|
if (!this.status.summary) return ''
|
||||||
|
const decodedSummary = unescape(this.status.summary)
|
||||||
const behavior = typeof this.$store.state.config.subjectLineBehavior === 'undefined'
|
const behavior = typeof this.$store.state.config.subjectLineBehavior === 'undefined'
|
||||||
? this.$store.state.instance.subjectLineBehavior
|
? this.$store.state.instance.subjectLineBehavior
|
||||||
: this.$store.state.config.subjectLineBehavior
|
: this.$store.state.config.subjectLineBehavior
|
||||||
const startsWithRe = this.status.summary.match(/^re[: ]/i)
|
const startsWithRe = decodedSummary.match(/^re[: ]/i)
|
||||||
if (behavior !== 'noop' && startsWithRe || behavior === 'masto') {
|
if (behavior !== 'noop' && startsWithRe || behavior === 'masto') {
|
||||||
return this.status.summary
|
return decodedSummary
|
||||||
} else if (behavior === 'email') {
|
} else if (behavior === 'email') {
|
||||||
return 're: '.concat(this.status.summary)
|
return 're: '.concat(decodedSummary)
|
||||||
} else if (behavior === 'noop') {
|
} else if (behavior === 'noop') {
|
||||||
return ''
|
return ''
|
||||||
}
|
}
|
||||||
|
@ -273,7 +282,7 @@ const Status = {
|
||||||
}
|
}
|
||||||
if (target.tagName === 'A') {
|
if (target.tagName === 'A') {
|
||||||
if (target.className.match(/mention/)) {
|
if (target.className.match(/mention/)) {
|
||||||
const href = target.getAttribute('href')
|
const href = target.href
|
||||||
const attn = this.status.attentions.find(attn => mentionMatchesUrl(attn, href))
|
const attn = this.status.attentions.find(attn => mentionMatchesUrl(attn, href))
|
||||||
if (attn) {
|
if (attn) {
|
||||||
event.stopPropagation()
|
event.stopPropagation()
|
||||||
|
@ -283,6 +292,15 @@ const Status = {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (target.className.match(/hashtag/)) {
|
||||||
|
// Extract tag name from link url
|
||||||
|
const tag = extractTagFromUrl(target.href)
|
||||||
|
if (tag) {
|
||||||
|
const link = this.generateTagLink(tag)
|
||||||
|
this.$router.push(link)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
window.open(target.href, '_blank')
|
window.open(target.href, '_blank')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -341,6 +359,9 @@ const Status = {
|
||||||
generateUserProfileLink (id, name) {
|
generateUserProfileLink (id, name) {
|
||||||
return generateProfileLink(id, name, this.$store.state.instance.restrictedNicknames)
|
return generateProfileLink(id, name, this.$store.state.instance.restrictedNicknames)
|
||||||
},
|
},
|
||||||
|
generateTagLink (tag) {
|
||||||
|
return `/tag/${tag}`
|
||||||
|
},
|
||||||
setMedia () {
|
setMedia () {
|
||||||
const attachments = this.attachmentSize === 'hide' ? this.status.attachments : this.galleryAttachments
|
const attachments = this.attachmentSize === 'hide' ? this.status.attachments : this.galleryAttachments
|
||||||
return () => this.$store.dispatch('setMedia', attachments)
|
return () => this.$store.dispatch('setMedia', attachments)
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="status-el" v-if="!hideReply && !deleted" :class="[{ 'status-el_focused': isFocused }, { 'status-conversation': inlineExpanded }]">
|
<div class="status-el" v-if="!hideStatus" :class="[{ 'status-el_focused': isFocused }, { 'status-conversation': inlineExpanded }]">
|
||||||
<template v-if="muted && !noReplyLinks">
|
<template v-if="muted && !noReplyLinks">
|
||||||
<div class="media status container muted">
|
<div class="media status container muted">
|
||||||
<small>
|
<small>
|
||||||
|
@ -13,12 +13,11 @@
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<div v-if="retweet && !noHeading" :class="[repeaterClass, { highlighted: repeaterStyle }]" :style="[repeaterStyle]" class="media container retweet-info">
|
<div v-if="retweet && !noHeading" :class="[repeaterClass, { highlighted: repeaterStyle }]" :style="[repeaterStyle]" class="media container retweet-info">
|
||||||
<UserAvatar v-if="retweet" :betterShadow="betterShadow" :src="statusoid.user.profile_image_url_original"/>
|
<UserAvatar class="media-left" v-if="retweet" :betterShadow="betterShadow" :src="statusoid.user.profile_image_url_original"/>
|
||||||
<div class="media-body faint">
|
<div class="media-body faint">
|
||||||
<span class="user-name">
|
<span class="user-name">
|
||||||
<router-link :to="retweeterProfileLink">
|
<router-link v-if="retweeterHtml" :to="retweeterProfileLink" v-html="retweeterHtml"/>
|
||||||
{{retweeterHtml || retweeter}}
|
<router-link v-else :to="retweeterProfileLink">{{retweeter}}</router-link>
|
||||||
</router-link>
|
|
||||||
</span>
|
</span>
|
||||||
<i class='fa icon-retweet retweeted' :title="$t('tool_tip.repeat')"></i>
|
<i class='fa icon-retweet retweeted' :title="$t('tool_tip.repeat')"></i>
|
||||||
{{$t('timeline.repeated')}}
|
{{$t('timeline.repeated')}}
|
||||||
|
@ -78,7 +77,7 @@
|
||||||
</router-link>
|
</router-link>
|
||||||
</div>
|
</div>
|
||||||
<h4 class="replies" v-if="inConversation && !noReplyLinks">
|
<h4 class="replies" v-if="inConversation && !noReplyLinks">
|
||||||
<small v-if="replies.length">Replies:</small>
|
<small class="faint" v-if="replies.length">Replies:</small>
|
||||||
<small class="reply-link" v-for="reply in replies">
|
<small class="reply-link" v-for="reply in replies">
|
||||||
<a href="#" @click.prevent="gotoOriginal(reply.id)" @mouseenter="replyEnter(reply.id, $event)" @mouseout="replyLeave()">{{reply.name}} </a>
|
<a href="#" @click.prevent="gotoOriginal(reply.id)" @mouseenter="replyEnter(reply.id, $event)" @mouseout="replyLeave()">{{reply.name}} </a>
|
||||||
</small>
|
</small>
|
||||||
|
@ -325,11 +324,11 @@
|
||||||
}
|
}
|
||||||
.reply-to {
|
.reply-to {
|
||||||
display: flex;
|
display: flex;
|
||||||
text-overflow: ellpisis;
|
|
||||||
}
|
}
|
||||||
.reply-to-text {
|
.reply-to-text {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
|
margin: 0 0.4em 0 0.2em;
|
||||||
}
|
}
|
||||||
.replies {
|
.replies {
|
||||||
line-height: 18px;
|
line-height: 18px;
|
||||||
|
@ -410,9 +409,11 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
p {
|
p {
|
||||||
margin: 0;
|
margin: 0 0 1em 0;
|
||||||
margin-top: 0.2em;
|
}
|
||||||
margin-bottom: 0.5em;
|
|
||||||
|
p:last-child {
|
||||||
|
margin: 0 0 0 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
h1 {
|
h1 {
|
||||||
|
@ -437,7 +438,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.retweet-info {
|
.retweet-info {
|
||||||
padding: 0.4em 0.6em 0 0.6em;
|
padding: 0.4em 0.75em;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
|
||||||
.avatar.still-image {
|
.avatar.still-image {
|
||||||
|
@ -456,6 +457,19 @@
|
||||||
align-content: center;
|
align-content: center;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
|
|
||||||
|
.user-name {
|
||||||
|
font-weight: bold;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
|
||||||
|
img {
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
vertical-align: middle;
|
||||||
|
object-fit: contain
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
i {
|
i {
|
||||||
padding: 0 0.2em;
|
padding: 0 0.2em;
|
||||||
}
|
}
|
||||||
|
@ -495,10 +509,9 @@
|
||||||
.status-actions {
|
.status-actions {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
margin-top: 0.5em;
|
margin-top: 0.75em;
|
||||||
|
|
||||||
div, favorite-button {
|
div, favorite-button {
|
||||||
// padding-top: 0.25em;
|
|
||||||
max-width: 4em;
|
max-width: 4em;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
@ -526,9 +539,8 @@
|
||||||
.status {
|
.status {
|
||||||
display: flex;
|
display: flex;
|
||||||
padding: 0.75em;
|
padding: 0.75em;
|
||||||
// padding: 0.6em;
|
|
||||||
&.is-retweet {
|
&.is-retweet {
|
||||||
padding-top: 0.1em;
|
padding-top: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -563,7 +575,7 @@ a.unmute {
|
||||||
|
|
||||||
.timeline > {
|
.timeline > {
|
||||||
.status-el:last-child {
|
.status-el:last-child {
|
||||||
border-bottom-radius: 0 0 $fallback--panelRadius $fallback--panelRadius;;
|
border-radius: 0 0 $fallback--panelRadius $fallback--panelRadius;
|
||||||
border-radius: 0 0 var(--panelRadius, $fallback--panelRadius) var(--panelRadius, $fallback--panelRadius);
|
border-radius: 0 0 var(--panelRadius, $fallback--panelRadius) var(--panelRadius, $fallback--panelRadius);
|
||||||
border-bottom: none;
|
border-bottom: none;
|
||||||
}
|
}
|
||||||
|
|
|
@ -37,7 +37,7 @@ export default Vue.component('tab-switcher', {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class={ classesWrapper.join(' ')}>
|
<div class={ classesWrapper.join(' ')}>
|
||||||
<button onClick={this.activateTab(index)} class={ classesTab.join(' ') }>{slot.data.attrs.label}</button>
|
<button disabled={slot.data.attrs.disabled} onClick={this.activateTab(index)} class={ classesTab.join(' ') }>{slot.data.attrs.label}</button>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
|
@ -3,7 +3,7 @@ import Timeline from '../timeline/timeline.vue'
|
||||||
const TagTimeline = {
|
const TagTimeline = {
|
||||||
created () {
|
created () {
|
||||||
this.$store.commit('clearTimeline', { timeline: 'tag' })
|
this.$store.commit('clearTimeline', { timeline: 'tag' })
|
||||||
this.$store.dispatch('startFetching', { 'tag': this.tag })
|
this.$store.dispatch('startFetching', { timeline: 'tag', tag: this.tag })
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
Timeline
|
Timeline
|
||||||
|
@ -15,7 +15,7 @@ const TagTimeline = {
|
||||||
watch: {
|
watch: {
|
||||||
tag () {
|
tag () {
|
||||||
this.$store.commit('clearTimeline', { timeline: 'tag' })
|
this.$store.commit('clearTimeline', { timeline: 'tag' })
|
||||||
this.$store.dispatch('startFetching', { 'tag': this.tag })
|
this.$store.dispatch('startFetching', { timeline: 'tag', tag: this.tag })
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
destroyed () {
|
destroyed () {
|
||||||
|
|
|
@ -11,7 +11,8 @@ const Timeline = {
|
||||||
'title',
|
'title',
|
||||||
'userId',
|
'userId',
|
||||||
'tag',
|
'tag',
|
||||||
'embedded'
|
'embedded',
|
||||||
|
'count'
|
||||||
],
|
],
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
|
@ -53,6 +54,8 @@ const Timeline = {
|
||||||
|
|
||||||
window.addEventListener('scroll', this.scrollLoad)
|
window.addEventListener('scroll', this.scrollLoad)
|
||||||
|
|
||||||
|
if (this.timelineName === 'friends' && !credentials) { return false }
|
||||||
|
|
||||||
timelineFetcher.fetchAndUpdate({
|
timelineFetcher.fetchAndUpdate({
|
||||||
store,
|
store,
|
||||||
credentials,
|
credentials,
|
||||||
|
|
|
@ -20,7 +20,10 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div :class="classes.footer">
|
<div :class="classes.footer">
|
||||||
<div v-if="bottomedOut" class="new-status-notification text-center panel-footer faint">
|
<div v-if="count===0" class="new-status-notification text-center panel-footer faint">
|
||||||
|
{{$t('timeline.no_statuses')}}
|
||||||
|
</div>
|
||||||
|
<div v-else-if="bottomedOut" class="new-status-notification text-center panel-footer faint">
|
||||||
{{$t('timeline.no_more_statuses')}}
|
{{$t('timeline.no_more_statuses')}}
|
||||||
</div>
|
</div>
|
||||||
<a v-else-if="!timeline.loading" href="#" v-on:click.prevent='fetchOlderStatuses()'>
|
<a v-else-if="!timeline.loading" href="#" v-on:click.prevent='fetchOlderStatuses()'>
|
||||||
|
|
|
@ -1,16 +1,20 @@
|
||||||
import UserCardContent from '../user_card_content/user_card_content.vue'
|
import UserCardContent from '../user_card_content/user_card_content.vue'
|
||||||
import UserAvatar from '../user_avatar/user_avatar.vue'
|
import UserAvatar from '../user_avatar/user_avatar.vue'
|
||||||
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
|
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
|
||||||
|
import { requestFollow, requestUnfollow } from '../../services/follow_manipulate/follow_manipulate'
|
||||||
|
|
||||||
const UserCard = {
|
const UserCard = {
|
||||||
props: [
|
props: [
|
||||||
'user',
|
'user',
|
||||||
'showFollows',
|
'noFollowsYou',
|
||||||
'showApproval'
|
'showApproval'
|
||||||
],
|
],
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
userExpanded: false
|
userExpanded: false,
|
||||||
|
followRequestInProgress: false,
|
||||||
|
followRequestSent: false,
|
||||||
|
updated: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
|
@ -18,7 +22,11 @@ const UserCard = {
|
||||||
UserAvatar
|
UserAvatar
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
currentUser () { return this.$store.state.users.currentUser }
|
currentUser () { return this.$store.state.users.currentUser },
|
||||||
|
following () { return this.updated ? this.updated.following : this.user.following },
|
||||||
|
showFollow () {
|
||||||
|
return !this.showApproval && (!this.following || this.updated && !this.updated.following)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
toggleUserExpanded () {
|
toggleUserExpanded () {
|
||||||
|
@ -34,6 +42,21 @@ const UserCard = {
|
||||||
},
|
},
|
||||||
userProfileLink (user) {
|
userProfileLink (user) {
|
||||||
return generateProfileLink(user.id, user.screen_name, this.$store.state.instance.restrictedNicknames)
|
return generateProfileLink(user.id, user.screen_name, this.$store.state.instance.restrictedNicknames)
|
||||||
|
},
|
||||||
|
followUser () {
|
||||||
|
this.followRequestInProgress = true
|
||||||
|
requestFollow(this.user, this.$store).then(({ sent, updated }) => {
|
||||||
|
this.followRequestInProgress = false
|
||||||
|
this.followRequestSent = sent
|
||||||
|
this.updated = updated
|
||||||
|
})
|
||||||
|
},
|
||||||
|
unfollowUser () {
|
||||||
|
this.followRequestInProgress = true
|
||||||
|
requestUnfollow(this.user, this.$store).then(({ updated }) => {
|
||||||
|
this.followRequestInProgress = false
|
||||||
|
this.updated = updated
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,34 +1,59 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<router-link :to="userProfileLink(user)">
|
<router-link :to="userProfileLink(user)">
|
||||||
<UserAvatar class="avatar" :compact="true" @click.prevent.native="toggleUserExpanded" :src="user.profile_image_url"/>
|
<UserAvatar class="avatar" @click.prevent.native="toggleUserExpanded" :src="user.profile_image_url"/>
|
||||||
</router-link>
|
</router-link>
|
||||||
|
<div class="user-card-main-content">
|
||||||
<div class="usercard" v-if="userExpanded">
|
<div class="usercard" v-if="userExpanded">
|
||||||
<user-card-content :user="user" :switcher="false"></user-card-content>
|
<user-card-content :user="user" :switcher="false"></user-card-content>
|
||||||
</div>
|
</div>
|
||||||
<div class="name-and-screen-name" v-else>
|
<div class="name-and-screen-name" v-if="!userExpanded">
|
||||||
<div :title="user.name" v-if="user.name_html" class="user-name">
|
<div :title="user.name" class="user-name">
|
||||||
<span v-html="user.name_html"></span>
|
<span v-if="user.name_html" v-html="user.name_html"></span>
|
||||||
<span class="follows-you" v-if="!userExpanded && showFollows && user.follows_you">
|
<span v-else>{{ user.name }}</span>
|
||||||
{{ currentUser.id == user.id ? $t('user_card.its_you') : $t('user_card.follows_you') }}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div :title="user.name" v-else class="user-name">
|
<div class="user-link-action">
|
||||||
{{ user.name }}
|
|
||||||
<span class="follows-you" v-if="!userExpanded && showFollows && user.follows_you">
|
|
||||||
{{ currentUser.id == user.id ? $t('user_card.its_you') : $t('user_card.follows_you') }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<router-link class='user-screen-name' :to="userProfileLink(user)">
|
<router-link class='user-screen-name' :to="userProfileLink(user)">
|
||||||
@{{user.screen_name}}
|
@{{user.screen_name}}
|
||||||
</router-link>
|
</router-link>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="follow-box" v-if="!userExpanded">
|
||||||
|
<span class="faint" v-if="!noFollowsYou && user.follows_you">
|
||||||
|
{{ currentUser.id == user.id ? $t('user_card.its_you') : $t('user_card.follows_you') }}
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
v-if="showFollow"
|
||||||
|
class="btn btn-default"
|
||||||
|
@click="followUser"
|
||||||
|
:disabled="followRequestInProgress"
|
||||||
|
:title="followRequestSent ? $t('user_card.follow_again') : ''"
|
||||||
|
>
|
||||||
|
<template v-if="followRequestInProgress">
|
||||||
|
{{ $t('user_card.follow_progress') }}
|
||||||
|
</template>
|
||||||
|
<template v-else-if="followRequestSent">
|
||||||
|
{{ $t('user_card.follow_sent') }}
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
{{ $t('user_card.follow') }}
|
||||||
|
</template>
|
||||||
|
</button>
|
||||||
|
<button v-if="following" class="btn btn-default pressed" @click="unfollowUser" :disabled="followRequestInProgress">
|
||||||
|
<template v-if="followRequestInProgress">
|
||||||
|
{{ $t('user_card.follow_progress') }}
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
{{ $t('user_card.follow_unfollow') }}
|
||||||
|
</template>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
<div class="approval" v-if="showApproval">
|
<div class="approval" v-if="showApproval">
|
||||||
<button class="btn btn-default" @click="approveUser">{{ $t('user_card.approve') }}</button>
|
<button class="btn btn-default" @click="approveUser">{{ $t('user_card.approve') }}</button>
|
||||||
<button class="btn btn-default" @click="denyUser">{{ $t('user_card.deny') }}</button>
|
<button class="btn btn-default" @click="denyUser">{{ $t('user_card.deny') }}</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script src="./user_card.js"></script>
|
<script src="./user_card.js"></script>
|
||||||
|
@ -36,11 +61,18 @@
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
@import '../../_variables.scss';
|
@import '../../_variables.scss';
|
||||||
|
|
||||||
.name-and-screen-name {
|
.user-card-main-content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex: 1 1 100%;
|
||||||
margin-left: 0.7em;
|
margin-left: 0.7em;
|
||||||
margin-top:0.0em;
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.name-and-screen-name {
|
||||||
text-align: left;
|
text-align: left;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
.user-name {
|
.user-name {
|
||||||
img {
|
img {
|
||||||
object-fit: contain;
|
object-fit: contain;
|
||||||
|
@ -49,12 +81,14 @@
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.user-link-action {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.follows-you {
|
|
||||||
margin-left: 2em;
|
|
||||||
float: right;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card {
|
.card {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
@ -71,11 +105,26 @@
|
||||||
.avatar {
|
.avatar {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.follow-box {
|
||||||
|
text-align: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
line-height: 1.5em;
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
margin-top: 0.5em;
|
||||||
|
margin-left: auto;
|
||||||
|
width: 10em;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.usercard {
|
.usercard {
|
||||||
width: fill-available;
|
width: fill-available;
|
||||||
margin: 0.2em 0 0 0.7em;
|
|
||||||
border-radius: $fallback--panelRadius;
|
border-radius: $fallback--panelRadius;
|
||||||
border-radius: var(--panelRadius, $fallback--panelRadius);
|
border-radius: var(--panelRadius, $fallback--panelRadius);
|
||||||
border-style: solid;
|
border-style: solid;
|
||||||
|
@ -96,9 +145,15 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.approval {
|
.approval {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
flex-wrap: wrap;
|
||||||
button {
|
button {
|
||||||
width: 100%;
|
margin-top: 0.5em;
|
||||||
margin-bottom: 0.5em;
|
margin-right: 0.5em;
|
||||||
|
flex: 1 1;
|
||||||
|
max-width: 12em;
|
||||||
|
min-width: 8em;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import UserAvatar from '../user_avatar/user_avatar.vue'
|
import UserAvatar from '../user_avatar/user_avatar.vue'
|
||||||
import { hex2rgb } from '../../services/color_convert/color_convert.js'
|
import { hex2rgb } from '../../services/color_convert/color_convert.js'
|
||||||
|
import { requestFollow, requestUnfollow } from '../../services/follow_manipulate/follow_manipulate'
|
||||||
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
|
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
@ -79,6 +80,12 @@ export default {
|
||||||
set (color) {
|
set (color) {
|
||||||
this.$store.dispatch('setHighlight', { user: this.user.screen_name, color })
|
this.$store.dispatch('setHighlight', { user: this.user.screen_name, color })
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
visibleRole () {
|
||||||
|
const validRole = (this.user.role === 'admin' || this.user.role === 'moderator')
|
||||||
|
const showRole = this.isOtherUser || this.user.show_role
|
||||||
|
|
||||||
|
return validRole && showRole && this.user.role
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
|
@ -86,67 +93,15 @@ export default {
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
followUser () {
|
followUser () {
|
||||||
const store = this.$store
|
|
||||||
this.followRequestInProgress = true
|
this.followRequestInProgress = true
|
||||||
store.state.api.backendInteractor.followUser(this.user.id)
|
requestFollow(this.user, this.$store).then(({sent}) => {
|
||||||
.then((followedUser) => store.commit('addNewUsers', [followedUser]))
|
|
||||||
.then(() => {
|
|
||||||
// For locked users we just mark it that we sent the follow request
|
|
||||||
if (this.user.locked) {
|
|
||||||
this.followRequestInProgress = false
|
this.followRequestInProgress = false
|
||||||
this.followRequestSent = true
|
this.followRequestSent = sent
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.user.following) {
|
|
||||||
// If we get result immediately, just stop.
|
|
||||||
this.followRequestInProgress = false
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// But usually we don't get result immediately, so we ask server
|
|
||||||
// for updated user profile to confirm if we are following them
|
|
||||||
// Sometimes it takes several tries. Sometimes we end up not following
|
|
||||||
// user anyway, probably because they locked themselves and we
|
|
||||||
// don't know that yet.
|
|
||||||
// Recursive Promise, it will call itself up to 3 times.
|
|
||||||
const fetchUser = (attempt) => new Promise((resolve, reject) => {
|
|
||||||
setTimeout(() => {
|
|
||||||
store.state.api.backendInteractor.fetchUser({ id: this.user.id })
|
|
||||||
.then((user) => store.commit('addNewUsers', [user]))
|
|
||||||
.then(() => resolve([this.user.following, attempt]))
|
|
||||||
.catch((e) => reject(e))
|
|
||||||
}, 500)
|
|
||||||
}).then(([following, attempt]) => {
|
|
||||||
if (!following && attempt <= 3) {
|
|
||||||
// If we BE reports that we still not following that user - retry,
|
|
||||||
// increment attempts by one
|
|
||||||
return fetchUser(++attempt)
|
|
||||||
} else {
|
|
||||||
// If we run out of attempts, just return whatever status is.
|
|
||||||
return following
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return fetchUser(1)
|
|
||||||
.then((following) => {
|
|
||||||
if (following) {
|
|
||||||
// We confirmed and everything its good.
|
|
||||||
this.followRequestInProgress = false
|
|
||||||
} else {
|
|
||||||
// If after all the tries, just treat it as if user is locked
|
|
||||||
this.followRequestInProgress = false
|
|
||||||
this.followRequestSent = true
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
unfollowUser () {
|
unfollowUser () {
|
||||||
const store = this.$store
|
|
||||||
this.followRequestInProgress = true
|
this.followRequestInProgress = true
|
||||||
store.state.api.backendInteractor.unfollowUser(this.user.id)
|
requestUnfollow(this.user, this.$store).then(() => {
|
||||||
.then((unfollowedUser) => store.commit('addNewUsers', [unfollowedUser]))
|
|
||||||
.then(() => {
|
|
||||||
this.followRequestInProgress = false
|
this.followRequestInProgress = false
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
|
@ -13,13 +13,15 @@
|
||||||
<router-link :to="{ name: 'user-settings' }" v-if="!isOtherUser">
|
<router-link :to="{ name: 'user-settings' }" v-if="!isOtherUser">
|
||||||
<i class="button-icon icon-cog usersettings" :title="$t('tool_tip.user_settings')"></i>
|
<i class="button-icon icon-cog usersettings" :title="$t('tool_tip.user_settings')"></i>
|
||||||
</router-link>
|
</router-link>
|
||||||
<a :href="user.statusnet_profile_url" target="_blank" v-if="isOtherUser">
|
<a :href="user.statusnet_profile_url" target="_blank" v-if="isOtherUser && !user.is_local">
|
||||||
<i class="icon-link-ext usersettings"></i>
|
<i class="icon-link-ext usersettings"></i>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<router-link class='user-screen-name' :to="userProfileLink(user)">
|
<router-link class='user-screen-name' :to="userProfileLink(user)">
|
||||||
<span class="handle">@{{user.screen_name}}</span><span v-if="user.locked"><i class="icon icon-lock"></i></span>
|
<span class="handle">@{{user.screen_name}}
|
||||||
|
<span class="alert staff" v-if="!hideBio && !!visibleRole">{{visibleRole}}</span>
|
||||||
|
</span><span v-if="user.locked"><i class="icon icon-lock"></i></span>
|
||||||
<span v-if="!hideUserStatsLocal && !hideBio" class="dailyAvg">{{dailyAvg}} {{ $t('user_card.per_day') }}</span>
|
<span v-if="!hideUserStatsLocal && !hideBio" class="dailyAvg">{{dailyAvg}} {{ $t('user_card.per_day') }}</span>
|
||||||
</router-link>
|
</router-link>
|
||||||
</div>
|
</div>
|
||||||
|
@ -247,6 +249,15 @@
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO use proper colors
|
||||||
|
.staff {
|
||||||
|
text-transform: capitalize;
|
||||||
|
color: $fallback--text;
|
||||||
|
color: var(--btnText, $fallback--text);
|
||||||
|
background-color: $fallback--fg;
|
||||||
|
background-color: var(--btn, $fallback--fg);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.user-meta {
|
.user-meta {
|
||||||
|
@ -375,6 +386,4 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.floater {
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -8,15 +8,15 @@ const UserProfile = {
|
||||||
this.$store.commit('clearTimeline', { timeline: 'user' })
|
this.$store.commit('clearTimeline', { timeline: 'user' })
|
||||||
this.$store.commit('clearTimeline', { timeline: 'favorites' })
|
this.$store.commit('clearTimeline', { timeline: 'favorites' })
|
||||||
this.$store.commit('clearTimeline', { timeline: 'media' })
|
this.$store.commit('clearTimeline', { timeline: 'media' })
|
||||||
this.$store.dispatch('startFetching', ['user', this.fetchBy])
|
this.$store.dispatch('startFetching', { timeline: 'user', userId: this.fetchBy })
|
||||||
this.$store.dispatch('startFetching', ['media', this.fetchBy])
|
this.$store.dispatch('startFetching', { timeline: 'media', userId: this.fetchBy })
|
||||||
this.startFetchFavorites()
|
this.startFetchFavorites()
|
||||||
if (!this.user.id) {
|
if (!this.user.id) {
|
||||||
this.$store.dispatch('fetchUser', this.fetchBy)
|
this.$store.dispatch('fetchUser', this.fetchBy)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
destroyed () {
|
destroyed () {
|
||||||
this.cleanUp(this.userId)
|
this.cleanUp()
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
timeline () {
|
timeline () {
|
||||||
|
@ -58,17 +58,23 @@ const UserProfile = {
|
||||||
},
|
},
|
||||||
isExternal () {
|
isExternal () {
|
||||||
return this.$route.name === 'external-user-profile'
|
return this.$route.name === 'external-user-profile'
|
||||||
|
},
|
||||||
|
followsTabVisible () {
|
||||||
|
return this.isUs || !this.user.hide_follows
|
||||||
|
},
|
||||||
|
followersTabVisible () {
|
||||||
|
return this.isUs || !this.user.hide_followers
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
startFetchFavorites () {
|
startFetchFavorites () {
|
||||||
if (this.isUs) {
|
if (this.isUs) {
|
||||||
this.$store.dispatch('startFetching', ['favorites', this.fetchBy])
|
this.$store.dispatch('startFetching', { timeline: 'favorites', userId: this.fetchBy })
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
startUp () {
|
startUp () {
|
||||||
this.$store.dispatch('startFetching', ['user', this.fetchBy])
|
this.$store.dispatch('startFetching', { timeline: 'user', userId: this.fetchBy })
|
||||||
this.$store.dispatch('startFetching', ['media', this.fetchBy])
|
this.$store.dispatch('startFetching', { timeline: 'media', userId: this.fetchBy })
|
||||||
|
|
||||||
this.startFetchFavorites()
|
this.startFetchFavorites()
|
||||||
},
|
},
|
||||||
|
|
|
@ -9,19 +9,21 @@
|
||||||
<tab-switcher :renderOnlyFocused="true">
|
<tab-switcher :renderOnlyFocused="true">
|
||||||
<Timeline
|
<Timeline
|
||||||
:label="$t('user_card.statuses')"
|
:label="$t('user_card.statuses')"
|
||||||
|
:disabled="!user.statuses_count"
|
||||||
|
:count="user.statuses_count"
|
||||||
:embedded="true"
|
:embedded="true"
|
||||||
:title="$t('user_profile.timeline_title')"
|
:title="$t('user_profile.timeline_title')"
|
||||||
:timeline="timeline"
|
:timeline="timeline"
|
||||||
:timeline-name="'user'"
|
:timeline-name="'user'"
|
||||||
:user-id="fetchBy"
|
:user-id="fetchBy"
|
||||||
/>
|
/>
|
||||||
<div :label="$t('user_card.followees')">
|
<div :label="$t('user_card.followees')" v-if="followsTabVisible" :disabled="!user.friends_count">
|
||||||
<FollowList v-if="user.friends_count > 0" :userId="userId" :showFollowers="false" />
|
<FollowList v-if="user.friends_count > 0" :userId="userId" :showFollowers="false" />
|
||||||
<div class="userlist-placeholder" v-else>
|
<div class="userlist-placeholder" v-else>
|
||||||
<i class="icon-spin3 animate-spin"></i>
|
<i class="icon-spin3 animate-spin"></i>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div :label="$t('user_card.followers')">
|
<div :label="$t('user_card.followers')" v-if="followersTabVisible" :disabled="!user.followers_count">
|
||||||
<FollowList v-if="user.followers_count > 0" :userId="userId" :showFollowers="true" />
|
<FollowList v-if="user.followers_count > 0" :userId="userId" :showFollowers="true" />
|
||||||
<div class="userlist-placeholder" v-else>
|
<div class="userlist-placeholder" v-else>
|
||||||
<i class="icon-spin3 animate-spin"></i>
|
<i class="icon-spin3 animate-spin"></i>
|
||||||
|
@ -29,6 +31,7 @@
|
||||||
</div>
|
</div>
|
||||||
<Timeline
|
<Timeline
|
||||||
:label="$t('user_card.media')"
|
:label="$t('user_card.media')"
|
||||||
|
:disabled="!media.visibleStatuses.length"
|
||||||
:embedded="true" :title="$t('user_card.media')"
|
:embedded="true" :title="$t('user_card.media')"
|
||||||
timeline-name="media"
|
timeline-name="media"
|
||||||
:timeline="media"
|
:timeline="media"
|
||||||
|
@ -37,6 +40,7 @@
|
||||||
<Timeline
|
<Timeline
|
||||||
v-if="isUs"
|
v-if="isUs"
|
||||||
:label="$t('user_card.favorites')"
|
:label="$t('user_card.favorites')"
|
||||||
|
:disabled="!favorites.visibleStatuses.length"
|
||||||
:embedded="true"
|
:embedded="true"
|
||||||
:title="$t('user_card.favorites')"
|
:title="$t('user_card.favorites')"
|
||||||
timeline-name="favorites"
|
timeline-name="favorites"
|
||||||
|
|
|
@ -10,7 +10,8 @@ const userSearch = {
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
username: '',
|
username: '',
|
||||||
users: []
|
users: [],
|
||||||
|
loading: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted () {
|
mounted () {
|
||||||
|
@ -30,8 +31,10 @@ const userSearch = {
|
||||||
this.users = []
|
this.users = []
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
this.loading = true
|
||||||
userSearchApi.search({query, store: this.$store})
|
userSearchApi.search({query, store: this.$store})
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
|
this.loading = false
|
||||||
this.users = res
|
this.users = res
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,7 +9,10 @@
|
||||||
<i class="icon-search"/>
|
<i class="icon-search"/>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="panel-body">
|
<div v-if="loading" class="text-center loading-icon">
|
||||||
|
<i class="icon-spin3 animate-spin"/>
|
||||||
|
</div>
|
||||||
|
<div v-else class="panel-body">
|
||||||
<user-card v-for="user in users" :key="user.id" :user="user" :showFollows="true"></user-card>
|
<user-card v-for="user in users" :key="user.id" :user="user" :showFollows="true"></user-card>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -27,4 +30,8 @@
|
||||||
margin-left: 0.5em;
|
margin-left: 0.5em;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.loading-icon {
|
||||||
|
padding: 1em;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1,8 +1,33 @@
|
||||||
import { unescape } from 'lodash'
|
import { compose } from 'vue-compose'
|
||||||
|
import unescape from 'lodash/unescape'
|
||||||
|
import get from 'lodash/get'
|
||||||
|
|
||||||
import TabSwitcher from '../tab_switcher/tab_switcher.js'
|
import TabSwitcher from '../tab_switcher/tab_switcher.js'
|
||||||
|
import ImageCropper from '../image_cropper/image_cropper.vue'
|
||||||
import StyleSwitcher from '../style_switcher/style_switcher.vue'
|
import StyleSwitcher from '../style_switcher/style_switcher.vue'
|
||||||
import fileSizeFormatService from '../../services/file_size_format/file_size_format.js'
|
import fileSizeFormatService from '../../services/file_size_format/file_size_format.js'
|
||||||
|
import BlockCard from '../block_card/block_card.vue'
|
||||||
|
import MuteCard from '../mute_card/mute_card.vue'
|
||||||
|
import withSubscription from '../../hocs/with_subscription/with_subscription'
|
||||||
|
import withList from '../../hocs/with_list/with_list'
|
||||||
|
|
||||||
|
const BlockList = compose(
|
||||||
|
withSubscription({
|
||||||
|
fetch: (props, $store) => $store.dispatch('fetchBlocks'),
|
||||||
|
select: (props, $store) => get($store.state.users.currentUser, 'blockIds', []),
|
||||||
|
childPropName: 'entries'
|
||||||
|
}),
|
||||||
|
withList({ getEntryProps: userId => ({ userId }) })
|
||||||
|
)(BlockCard)
|
||||||
|
|
||||||
|
const MuteList = compose(
|
||||||
|
withSubscription({
|
||||||
|
fetch: (props, $store) => $store.dispatch('fetchMutes'),
|
||||||
|
select: (props, $store) => get($store.state.users.currentUser, 'muteIds', []),
|
||||||
|
childPropName: 'entries'
|
||||||
|
}),
|
||||||
|
withList({ getEntryProps: userId => ({ userId }) })
|
||||||
|
)(MuteCard)
|
||||||
|
|
||||||
const UserSettings = {
|
const UserSettings = {
|
||||||
data () {
|
data () {
|
||||||
|
@ -14,18 +39,18 @@ const UserSettings = {
|
||||||
newDefaultScope: this.$store.state.users.currentUser.default_scope,
|
newDefaultScope: this.$store.state.users.currentUser.default_scope,
|
||||||
hideFollows: this.$store.state.users.currentUser.hide_follows,
|
hideFollows: this.$store.state.users.currentUser.hide_follows,
|
||||||
hideFollowers: this.$store.state.users.currentUser.hide_followers,
|
hideFollowers: this.$store.state.users.currentUser.hide_followers,
|
||||||
|
showRole: this.$store.state.users.currentUser.show_role,
|
||||||
|
role: this.$store.state.users.currentUser.role,
|
||||||
followList: null,
|
followList: null,
|
||||||
followImportError: false,
|
followImportError: false,
|
||||||
followsImported: false,
|
followsImported: false,
|
||||||
enableFollowsExport: true,
|
enableFollowsExport: true,
|
||||||
avatarUploading: false,
|
pickAvatarBtnVisible: true,
|
||||||
bannerUploading: false,
|
bannerUploading: false,
|
||||||
backgroundUploading: false,
|
backgroundUploading: false,
|
||||||
followListUploading: false,
|
followListUploading: false,
|
||||||
avatarPreview: null,
|
|
||||||
bannerPreview: null,
|
bannerPreview: null,
|
||||||
backgroundPreview: null,
|
backgroundPreview: null,
|
||||||
avatarUploadError: null,
|
|
||||||
bannerUploadError: null,
|
bannerUploadError: null,
|
||||||
backgroundUploadError: null,
|
backgroundUploadError: null,
|
||||||
deletingAccount: false,
|
deletingAccount: false,
|
||||||
|
@ -39,7 +64,10 @@ const UserSettings = {
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
StyleSwitcher,
|
StyleSwitcher,
|
||||||
TabSwitcher
|
TabSwitcher,
|
||||||
|
ImageCropper,
|
||||||
|
BlockList,
|
||||||
|
MuteList
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
user () {
|
user () {
|
||||||
|
@ -58,6 +86,9 @@ const UserSettings = {
|
||||||
private: { selected: this.newDefaultScope === 'private' },
|
private: { selected: this.newDefaultScope === 'private' },
|
||||||
direct: { selected: this.newDefaultScope === 'direct' }
|
direct: { selected: this.newDefaultScope === 'direct' }
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
currentSaveStateNotice () {
|
||||||
|
return this.$store.state.interface.settings.currentSaveStateNotice
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
@ -71,6 +102,8 @@ const UserSettings = {
|
||||||
const no_rich_text = this.newNoRichText
|
const no_rich_text = this.newNoRichText
|
||||||
const hide_follows = this.hideFollows
|
const hide_follows = this.hideFollows
|
||||||
const hide_followers = this.hideFollowers
|
const hide_followers = this.hideFollowers
|
||||||
|
const show_role = this.showRole
|
||||||
|
|
||||||
/* eslint-enable camelcase */
|
/* eslint-enable camelcase */
|
||||||
this.$store.state.api.backendInteractor
|
this.$store.state.api.backendInteractor
|
||||||
.updateProfile({
|
.updateProfile({
|
||||||
|
@ -83,7 +116,8 @@ const UserSettings = {
|
||||||
default_scope,
|
default_scope,
|
||||||
no_rich_text,
|
no_rich_text,
|
||||||
hide_follows,
|
hide_follows,
|
||||||
hide_followers
|
hide_followers,
|
||||||
|
show_role
|
||||||
/* eslint-enable camelcase */
|
/* eslint-enable camelcase */
|
||||||
}}).then((user) => {
|
}}).then((user) => {
|
||||||
if (!user.error) {
|
if (!user.error) {
|
||||||
|
@ -112,35 +146,15 @@ const UserSettings = {
|
||||||
}
|
}
|
||||||
reader.readAsDataURL(file)
|
reader.readAsDataURL(file)
|
||||||
},
|
},
|
||||||
submitAvatar () {
|
submitAvatar (cropper) {
|
||||||
if (!this.avatarPreview) { return }
|
const img = cropper.getCroppedCanvas().toDataURL('image/jpeg')
|
||||||
|
return this.$store.state.api.backendInteractor.updateAvatar({ params: { img } }).then((user) => {
|
||||||
let img = this.avatarPreview
|
|
||||||
// eslint-disable-next-line no-undef
|
|
||||||
let imginfo = new Image()
|
|
||||||
let cropX, cropY, cropW, cropH
|
|
||||||
imginfo.src = img
|
|
||||||
if (imginfo.height > imginfo.width) {
|
|
||||||
cropX = 0
|
|
||||||
cropW = imginfo.width
|
|
||||||
cropY = Math.floor((imginfo.height - imginfo.width) / 2)
|
|
||||||
cropH = imginfo.width
|
|
||||||
} else {
|
|
||||||
cropY = 0
|
|
||||||
cropH = imginfo.height
|
|
||||||
cropX = Math.floor((imginfo.width - imginfo.height) / 2)
|
|
||||||
cropW = imginfo.height
|
|
||||||
}
|
|
||||||
this.avatarUploading = true
|
|
||||||
this.$store.state.api.backendInteractor.updateAvatar({params: {img, cropX, cropY, cropW, cropH}}).then((user) => {
|
|
||||||
if (!user.error) {
|
if (!user.error) {
|
||||||
this.$store.commit('addNewUsers', [user])
|
this.$store.commit('addNewUsers', [user])
|
||||||
this.$store.commit('setCurrentUser', user)
|
this.$store.commit('setCurrentUser', user)
|
||||||
this.avatarPreview = null
|
|
||||||
} else {
|
} else {
|
||||||
this.avatarUploadError = this.$t('upload.error.base') + user.error
|
throw new Error(this.$t('upload.error.base') + user.error)
|
||||||
}
|
}
|
||||||
this.avatarUploading = false
|
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
clearUploadError (slot) {
|
clearUploadError (slot) {
|
||||||
|
@ -238,7 +252,9 @@ const UserSettings = {
|
||||||
exportFollows () {
|
exportFollows () {
|
||||||
this.enableFollowsExport = false
|
this.enableFollowsExport = false
|
||||||
this.$store.state.api.backendInteractor
|
this.$store.state.api.backendInteractor
|
||||||
.fetchFriends({id: this.$store.state.users.currentUser.id})
|
.exportFriends({
|
||||||
|
id: this.$store.state.users.currentUser.id
|
||||||
|
})
|
||||||
.then((friendList) => {
|
.then((friendList) => {
|
||||||
this.exportPeople(friendList, 'friends.csv')
|
this.exportPeople(friendList, 'friends.csv')
|
||||||
setTimeout(() => { this.enableFollowsExport = true }, 2000)
|
setTimeout(() => { this.enableFollowsExport = true }, 2000)
|
||||||
|
|
|
@ -1,8 +1,21 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="settings panel panel-default">
|
<div class="settings panel panel-default">
|
||||||
<div class="panel-heading">
|
<div class="panel-heading">
|
||||||
|
<div class="title">
|
||||||
{{$t('settings.user_settings')}}
|
{{$t('settings.user_settings')}}
|
||||||
</div>
|
</div>
|
||||||
|
<transition name="fade">
|
||||||
|
<template v-if="currentSaveStateNotice">
|
||||||
|
<div @click.prevent class="alert error" v-if="currentSaveStateNotice.error">
|
||||||
|
{{ $t('settings.saving_err') }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div @click.prevent class="alert transparent" v-if="!currentSaveStateNotice.error">
|
||||||
|
{{ $t('settings.saving_ok') }}
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</transition>
|
||||||
|
</div>
|
||||||
<div class="panel-body profile-edit">
|
<div class="panel-body profile-edit">
|
||||||
<tab-switcher>
|
<tab-switcher>
|
||||||
<div :label="$t('settings.profile_tab')">
|
<div :label="$t('settings.profile_tab')">
|
||||||
|
@ -37,25 +50,21 @@
|
||||||
<input type="checkbox" v-model="hideFollowers" id="account-hide-followers">
|
<input type="checkbox" v-model="hideFollowers" id="account-hide-followers">
|
||||||
<label for="account-hide-followers">{{$t('settings.hide_followers_description')}}</label>
|
<label for="account-hide-followers">{{$t('settings.hide_followers_description')}}</label>
|
||||||
</p>
|
</p>
|
||||||
|
<p>
|
||||||
|
<input type="checkbox" v-model="showRole" id="account-show-role">
|
||||||
|
<label for="account-show-role" v-if="role === 'admin'">{{$t('settings.show_admin_badge')}}</label>
|
||||||
|
<label for="account-show-role" v-if="role === 'moderator'">{{$t('settings.show_moderator_badge')}}</label>
|
||||||
|
</p>
|
||||||
<button :disabled='newName && newName.length === 0' class="btn btn-default" @click="updateProfile">{{$t('general.submit')}}</button>
|
<button :disabled='newName && newName.length === 0' class="btn btn-default" @click="updateProfile">{{$t('general.submit')}}</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="setting-item">
|
<div class="setting-item">
|
||||||
<h2>{{$t('settings.avatar')}}</h2>
|
<h2>{{$t('settings.avatar')}}</h2>
|
||||||
<p class="visibility-notice">{{$t('settings.avatar_size_instruction')}}</p>
|
<p class="visibility-notice">{{$t('settings.avatar_size_instruction')}}</p>
|
||||||
<p>{{$t('settings.current_avatar')}}</p>
|
<p>{{$t('settings.current_avatar')}}</p>
|
||||||
<img :src="user.profile_image_url_original" class="old-avatar"></img>
|
<img :src="user.profile_image_url_original" class="current-avatar"></img>
|
||||||
<p>{{$t('settings.set_new_avatar')}}</p>
|
<p>{{$t('settings.set_new_avatar')}}</p>
|
||||||
<img class="new-avatar" v-bind:src="avatarPreview" v-if="avatarPreview">
|
<button class="btn" type="button" id="pick-avatar" v-show="pickAvatarBtnVisible">{{$t('settings.upload_a_photo')}}</button>
|
||||||
</img>
|
<image-cropper trigger="#pick-avatar" :submitHandler="submitAvatar" @open="pickAvatarBtnVisible=false" @close="pickAvatarBtnVisible=true" />
|
||||||
<div>
|
|
||||||
<input type="file" @change="uploadFile('avatar', $event)" ></input>
|
|
||||||
</div>
|
|
||||||
<i class="icon-spin4 animate-spin" v-if="avatarUploading"></i>
|
|
||||||
<button class="btn btn-default" v-else-if="avatarPreview" @click="submitAvatar">{{$t('general.submit')}}</button>
|
|
||||||
<div class='alert error' v-if="avatarUploadError">
|
|
||||||
Error: {{ avatarUploadError }}
|
|
||||||
<i class="button-icon icon-cancel" @click="clearUploadError('avatar')"></i>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="setting-item">
|
<div class="setting-item">
|
||||||
<h2>{{$t('settings.profile_banner')}}</h2>
|
<h2>{{$t('settings.profile_banner')}}</h2>
|
||||||
|
@ -153,6 +162,12 @@
|
||||||
<h2>{{$t('settings.follow_export_processing')}}</h2>
|
<h2>{{$t('settings.follow_export_processing')}}</h2>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div :label="$t('settings.blocks_tab')">
|
||||||
|
<block-list :refresh="true">
|
||||||
|
<template slot="empty">{{$t('settings.no_blocks')}}</template>
|
||||||
|
</block-list>
|
||||||
|
</div>
|
||||||
</tab-switcher>
|
</tab-switcher>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -162,6 +177,8 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
|
@import '../../_variables.scss';
|
||||||
|
|
||||||
.profile-edit {
|
.profile-edit {
|
||||||
.bio {
|
.bio {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
@ -173,7 +190,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.banner {
|
.banner {
|
||||||
max-width: 400px;
|
max-width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.uploading {
|
.uploading {
|
||||||
|
@ -184,5 +201,17 @@
|
||||||
.name-changer {
|
.name-changer {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.bg {
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.current-avatar {
|
||||||
|
display: block;
|
||||||
|
width: 150px;
|
||||||
|
height: 150px;
|
||||||
|
border-radius: $fallback--avatarRadius;
|
||||||
|
border-radius: var(--avatarRadius, $fallback--avatarRadius);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -0,0 +1,40 @@
|
||||||
|
import Vue from 'vue'
|
||||||
|
import map from 'lodash/map'
|
||||||
|
import isEmpty from 'lodash/isEmpty'
|
||||||
|
import './with_list.scss'
|
||||||
|
|
||||||
|
const defaultEntryPropsGetter = entry => ({ entry })
|
||||||
|
const defaultKeyGetter = entry => entry.id
|
||||||
|
|
||||||
|
const withList = ({
|
||||||
|
getEntryProps = defaultEntryPropsGetter, // function to accept entry and index values and return props to be passed into the item component
|
||||||
|
getKey = defaultKeyGetter // funciton to accept entry and index values and return key prop value
|
||||||
|
}) => (ItemComponent) => (
|
||||||
|
Vue.component('withList', {
|
||||||
|
props: [
|
||||||
|
'entries', // array of entry
|
||||||
|
'entryProps', // additional props to be passed into each entry
|
||||||
|
'entryListeners' // additional event listeners to be passed into each entry
|
||||||
|
],
|
||||||
|
render (createElement) {
|
||||||
|
return (
|
||||||
|
<div class="with-list">
|
||||||
|
{map(this.entries, (entry, index) => {
|
||||||
|
const props = {
|
||||||
|
key: getKey(entry, index),
|
||||||
|
props: {
|
||||||
|
...this.$props.entryProps,
|
||||||
|
...getEntryProps(entry, index)
|
||||||
|
},
|
||||||
|
on: this.$props.entryListeners
|
||||||
|
}
|
||||||
|
return <ItemComponent {...props} />
|
||||||
|
})}
|
||||||
|
{isEmpty(this.entries) && this.$slots.empty && <div class="with-list-empty-content faint">{this.$slots.empty}</div>}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
export default withList
|
|
@ -0,0 +1,6 @@
|
||||||
|
.with-list {
|
||||||
|
&-empty-content {
|
||||||
|
text-align: center;
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,91 @@
|
||||||
|
import Vue from 'vue'
|
||||||
|
import filter from 'lodash/filter'
|
||||||
|
import isEmpty from 'lodash/isEmpty'
|
||||||
|
import './with_load_more.scss'
|
||||||
|
|
||||||
|
const withLoadMore = ({
|
||||||
|
fetch, // function to fetch entries and return a promise
|
||||||
|
select, // function to select data from store
|
||||||
|
childPropName = 'entries' // name of the prop to be passed into the wrapped component
|
||||||
|
}) => (WrappedComponent) => {
|
||||||
|
const originalProps = WrappedComponent.props || []
|
||||||
|
const props = filter(originalProps, v => v !== 'entries')
|
||||||
|
|
||||||
|
return Vue.component('withLoadMore', {
|
||||||
|
render (createElement) {
|
||||||
|
const props = {
|
||||||
|
props: {
|
||||||
|
...this.$props,
|
||||||
|
[childPropName]: this.entries
|
||||||
|
},
|
||||||
|
on: this.$listeners,
|
||||||
|
scopedSlots: this.$scopedSlots
|
||||||
|
}
|
||||||
|
const children = Object.entries(this.$slots).map(([key, value]) => createElement('template', { slot: key }, value))
|
||||||
|
return (
|
||||||
|
<div class="with-load-more">
|
||||||
|
<WrappedComponent {...props}>
|
||||||
|
{children}
|
||||||
|
</WrappedComponent>
|
||||||
|
<div class="with-load-more-footer">
|
||||||
|
{this.error && <a onClick={this.fetchEntries} class="alert error">{this.$t('general.generic_error')}</a>}
|
||||||
|
{!this.error && this.loading && <i class="icon-spin3 animate-spin"/>}
|
||||||
|
{!this.error && !this.loading && !this.bottomedOut && <a onClick={this.fetchEntries}>{this.$t('general.more')}</a>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
props,
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
loading: false,
|
||||||
|
bottomedOut: false,
|
||||||
|
error: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
entries () {
|
||||||
|
return select(this.$props, this.$store) || []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created () {
|
||||||
|
window.addEventListener('scroll', this.scrollLoad)
|
||||||
|
if (this.entries.length === 0) {
|
||||||
|
this.fetchEntries()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
destroyed () {
|
||||||
|
window.removeEventListener('scroll', this.scrollLoad)
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
fetchEntries () {
|
||||||
|
if (!this.loading) {
|
||||||
|
this.loading = true
|
||||||
|
this.error = false
|
||||||
|
fetch(this.$props, this.$store)
|
||||||
|
.then((newEntries) => {
|
||||||
|
this.loading = false
|
||||||
|
this.bottomedOut = isEmpty(newEntries)
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
this.loading = false
|
||||||
|
this.error = true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
scrollLoad (e) {
|
||||||
|
const bodyBRect = document.body.getBoundingClientRect()
|
||||||
|
const height = Math.max(bodyBRect.height, -(bodyBRect.y))
|
||||||
|
if (this.loading === false &&
|
||||||
|
this.bottomedOut === false &&
|
||||||
|
this.$el.offsetHeight > 0 &&
|
||||||
|
(window.innerHeight + window.pageYOffset) >= (height - 750)
|
||||||
|
) {
|
||||||
|
this.fetchEntries()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export default withLoadMore
|
|
@ -0,0 +1,10 @@
|
||||||
|
.with-load-more {
|
||||||
|
&-footer {
|
||||||
|
padding: 10px;
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
.error {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,84 @@
|
||||||
|
import Vue from 'vue'
|
||||||
|
import reject from 'lodash/reject'
|
||||||
|
import isEmpty from 'lodash/isEmpty'
|
||||||
|
import omit from 'lodash/omit'
|
||||||
|
import './with_subscription.scss'
|
||||||
|
|
||||||
|
const withSubscription = ({
|
||||||
|
fetch, // function to fetch entries and return a promise
|
||||||
|
select, // function to select data from store
|
||||||
|
childPropName = 'content' // name of the prop to be passed into the wrapped component
|
||||||
|
}) => (WrappedComponent) => {
|
||||||
|
const originalProps = WrappedComponent.props || []
|
||||||
|
const props = reject(originalProps, v => v === 'content')
|
||||||
|
|
||||||
|
return Vue.component('withSubscription', {
|
||||||
|
props: [
|
||||||
|
...props,
|
||||||
|
'refresh' // boolean saying to force-fetch data whenever created
|
||||||
|
],
|
||||||
|
render (createElement) {
|
||||||
|
if (!this.error && !this.loading) {
|
||||||
|
const props = {
|
||||||
|
props: {
|
||||||
|
...omit(this.$props, 'refresh'),
|
||||||
|
[childPropName]: this.fetchedData
|
||||||
|
},
|
||||||
|
on: this.$listeners,
|
||||||
|
scopedSlots: this.$scopedSlots
|
||||||
|
}
|
||||||
|
const children = Object.entries(this.$slots).map(([key, value]) => createElement('template', { slot: key }, value))
|
||||||
|
return (
|
||||||
|
<div class="with-subscription">
|
||||||
|
<WrappedComponent {...props}>
|
||||||
|
{children}
|
||||||
|
</WrappedComponent>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<div class="with-subscription-loading">
|
||||||
|
{this.error
|
||||||
|
? <a onClick={this.fetchData} class="alert error">{this.$t('general.generic_error')}</a>
|
||||||
|
: <i class="icon-spin3 animate-spin"/>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
loading: false,
|
||||||
|
error: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
fetchedData () {
|
||||||
|
return select(this.$props, this.$store)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created () {
|
||||||
|
if (this.refresh || isEmpty(this.fetchedData)) {
|
||||||
|
this.fetchData()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
fetchData () {
|
||||||
|
if (!this.loading) {
|
||||||
|
this.loading = true
|
||||||
|
this.error = false
|
||||||
|
fetch(this.$props, this.$store)
|
||||||
|
.then(() => {
|
||||||
|
this.loading = false
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
this.error = true
|
||||||
|
this.loading = false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export default withSubscription
|
|
@ -0,0 +1,10 @@
|
||||||
|
.with-subscription {
|
||||||
|
&-loading {
|
||||||
|
padding: 10px;
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
.error {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -132,6 +132,7 @@
|
||||||
"preload_images": "Bilder vorausladen",
|
"preload_images": "Bilder vorausladen",
|
||||||
"hide_post_stats": "Beitragsstatistiken verbergen (z.B. die Anzahl der Favoriten)",
|
"hide_post_stats": "Beitragsstatistiken verbergen (z.B. die Anzahl der Favoriten)",
|
||||||
"hide_user_stats": "Benutzerstatistiken verbergen (z.B. die Anzahl der Follower)",
|
"hide_user_stats": "Benutzerstatistiken verbergen (z.B. die Anzahl der Follower)",
|
||||||
|
"hide_filtered_statuses": "Gefilterte Beiträge verbergen",
|
||||||
"import_followers_from_a_csv_file": "Importiere Follower, denen du folgen möchtest, aus einer CSV-Datei",
|
"import_followers_from_a_csv_file": "Importiere Follower, denen du folgen möchtest, aus einer CSV-Datei",
|
||||||
"import_theme": "Farbschema laden",
|
"import_theme": "Farbschema laden",
|
||||||
"inputRadius": "Eingabefelder",
|
"inputRadius": "Eingabefelder",
|
||||||
|
|
|
@ -21,6 +21,11 @@
|
||||||
"more": "More",
|
"more": "More",
|
||||||
"generic_error": "An error occured"
|
"generic_error": "An error occured"
|
||||||
},
|
},
|
||||||
|
"image_cropper": {
|
||||||
|
"crop_picture": "Crop picture",
|
||||||
|
"save": "Save",
|
||||||
|
"cancel": "Cancel"
|
||||||
|
},
|
||||||
"login": {
|
"login": {
|
||||||
"login": "Log in",
|
"login": "Log in",
|
||||||
"description": "Log in with OAuth",
|
"description": "Log in with OAuth",
|
||||||
|
@ -28,7 +33,12 @@
|
||||||
"password": "Password",
|
"password": "Password",
|
||||||
"placeholder": "e.g. lain",
|
"placeholder": "e.g. lain",
|
||||||
"register": "Register",
|
"register": "Register",
|
||||||
"username": "Username"
|
"username": "Username",
|
||||||
|
"hint": "Log in to join the discussion"
|
||||||
|
},
|
||||||
|
"media_modal": {
|
||||||
|
"previous": "Previous",
|
||||||
|
"next": "Next"
|
||||||
},
|
},
|
||||||
"nav": {
|
"nav": {
|
||||||
"about": "About",
|
"about": "About",
|
||||||
|
@ -100,6 +110,7 @@
|
||||||
"avatarRadius": "Avatars",
|
"avatarRadius": "Avatars",
|
||||||
"background": "Background",
|
"background": "Background",
|
||||||
"bio": "Bio",
|
"bio": "Bio",
|
||||||
|
"blocks_tab": "Blocks",
|
||||||
"btnRadius": "Buttons",
|
"btnRadius": "Buttons",
|
||||||
"cBlue": "Blue (Reply, follow)",
|
"cBlue": "Blue (Reply, follow)",
|
||||||
"cGreen": "Green (Retweet)",
|
"cGreen": "Green (Retweet)",
|
||||||
|
@ -139,6 +150,7 @@
|
||||||
"use_one_click_nsfw": "Open NSFW attachments with just one click",
|
"use_one_click_nsfw": "Open NSFW attachments with just one click",
|
||||||
"hide_post_stats": "Hide post statistics (e.g. the number of favorites)",
|
"hide_post_stats": "Hide post statistics (e.g. the number of favorites)",
|
||||||
"hide_user_stats": "Hide user statistics (e.g. the number of followers)",
|
"hide_user_stats": "Hide user statistics (e.g. the number of followers)",
|
||||||
|
"hide_filtered_statuses": "Hide filtered statuses",
|
||||||
"import_followers_from_a_csv_file": "Import follows from a csv file",
|
"import_followers_from_a_csv_file": "Import follows from a csv file",
|
||||||
"import_theme": "Load preset",
|
"import_theme": "Load preset",
|
||||||
"inputRadius": "Input fields",
|
"inputRadius": "Input fields",
|
||||||
|
@ -153,6 +165,7 @@
|
||||||
"lock_account_description": "Restrict your account to approved followers only",
|
"lock_account_description": "Restrict your account to approved followers only",
|
||||||
"loop_video": "Loop videos",
|
"loop_video": "Loop videos",
|
||||||
"loop_video_silent_only": "Loop only videos without sound (i.e. Mastodon's \"gifs\")",
|
"loop_video_silent_only": "Loop only videos without sound (i.e. Mastodon's \"gifs\")",
|
||||||
|
"mutes_tab": "Mutes",
|
||||||
"play_videos_in_modal": "Play videos directly in the media viewer",
|
"play_videos_in_modal": "Play videos directly in the media viewer",
|
||||||
"use_contain_fit": "Don't crop the attachment in thumbnails",
|
"use_contain_fit": "Don't crop the attachment in thumbnails",
|
||||||
"name": "Name",
|
"name": "Name",
|
||||||
|
@ -164,8 +177,12 @@
|
||||||
"notification_visibility_mentions": "Mentions",
|
"notification_visibility_mentions": "Mentions",
|
||||||
"notification_visibility_repeats": "Repeats",
|
"notification_visibility_repeats": "Repeats",
|
||||||
"no_rich_text_description": "Strip rich text formatting from all posts",
|
"no_rich_text_description": "Strip rich text formatting from all posts",
|
||||||
|
"no_blocks": "No blocks",
|
||||||
|
"no_mutes": "No mutes",
|
||||||
"hide_follows_description": "Don't show who I'm following",
|
"hide_follows_description": "Don't show who I'm following",
|
||||||
"hide_followers_description": "Don't show who's following me",
|
"hide_followers_description": "Don't show who's following me",
|
||||||
|
"show_admin_badge": "Show Admin badge in my profile",
|
||||||
|
"show_moderator_badge": "Show Moderator badge in my profile",
|
||||||
"nsfw_clickthrough": "Enable clickthrough NSFW attachment hiding",
|
"nsfw_clickthrough": "Enable clickthrough NSFW attachment hiding",
|
||||||
"panelRadius": "Panels",
|
"panelRadius": "Panels",
|
||||||
"pause_on_unfocused": "Pause streaming when tab is not focused",
|
"pause_on_unfocused": "Pause streaming when tab is not focused",
|
||||||
|
@ -192,6 +209,8 @@
|
||||||
"subject_line_email": "Like email: \"re: subject\"",
|
"subject_line_email": "Like email: \"re: subject\"",
|
||||||
"subject_line_mastodon": "Like mastodon: copy as is",
|
"subject_line_mastodon": "Like mastodon: copy as is",
|
||||||
"subject_line_noop": "Do not copy",
|
"subject_line_noop": "Do not copy",
|
||||||
|
"post_status_content_type": "Post status content type",
|
||||||
|
"status_content_type_plain": "Plain text",
|
||||||
"stop_gifs": "Play-on-hover GIFs",
|
"stop_gifs": "Play-on-hover GIFs",
|
||||||
"streaming": "Enable automatic streaming of new posts when scrolled to the top",
|
"streaming": "Enable automatic streaming of new posts when scrolled to the top",
|
||||||
"text": "Text",
|
"text": "Text",
|
||||||
|
@ -200,6 +219,7 @@
|
||||||
"theme_help_v2_1": "You can also override certain component's colors and opacity by toggling the checkbox, use \"Clear all\" button to clear all overrides.",
|
"theme_help_v2_1": "You can also override certain component's colors and opacity by toggling the checkbox, use \"Clear all\" button to clear all overrides.",
|
||||||
"theme_help_v2_2": "Icons underneath some entries are background/text contrast indicators, hover over for detailed info. Please keep in mind that when using transparency contrast indicators show the worst possible case.",
|
"theme_help_v2_2": "Icons underneath some entries are background/text contrast indicators, hover over for detailed info. Please keep in mind that when using transparency contrast indicators show the worst possible case.",
|
||||||
"tooltipRadius": "Tooltips/alerts",
|
"tooltipRadius": "Tooltips/alerts",
|
||||||
|
"upload_a_photo": "Upload a photo",
|
||||||
"user_settings": "User Settings",
|
"user_settings": "User Settings",
|
||||||
"values": {
|
"values": {
|
||||||
"false": "no",
|
"false": "no",
|
||||||
|
@ -326,7 +346,8 @@
|
||||||
"repeated": "repeated",
|
"repeated": "repeated",
|
||||||
"show_new": "Show new",
|
"show_new": "Show new",
|
||||||
"up_to_date": "Up-to-date",
|
"up_to_date": "Up-to-date",
|
||||||
"no_more_statuses": "No more statuses"
|
"no_more_statuses": "No more statuses",
|
||||||
|
"no_statuses": "No statuses"
|
||||||
},
|
},
|
||||||
"user_card": {
|
"user_card": {
|
||||||
"approve": "Approve",
|
"approve": "Approve",
|
||||||
|
@ -338,7 +359,7 @@
|
||||||
"follow_sent": "Request sent!",
|
"follow_sent": "Request sent!",
|
||||||
"follow_progress": "Requesting…",
|
"follow_progress": "Requesting…",
|
||||||
"follow_again": "Send request again?",
|
"follow_again": "Send request again?",
|
||||||
"follow_unfollow": "Stop following",
|
"follow_unfollow": "Unfollow",
|
||||||
"followees": "Following",
|
"followees": "Following",
|
||||||
"followers": "Followers",
|
"followers": "Followers",
|
||||||
"following": "Following!",
|
"following": "Following!",
|
||||||
|
@ -349,7 +370,13 @@
|
||||||
"muted": "Muted",
|
"muted": "Muted",
|
||||||
"per_day": "per day",
|
"per_day": "per day",
|
||||||
"remote_follow": "Remote follow",
|
"remote_follow": "Remote follow",
|
||||||
"statuses": "Statuses"
|
"statuses": "Statuses",
|
||||||
|
"unblock": "Unblock",
|
||||||
|
"unblock_progress": "Unblocking...",
|
||||||
|
"block_progress": "Blocking...",
|
||||||
|
"unmute": "Unmute",
|
||||||
|
"unmute_progress": "Unmuting...",
|
||||||
|
"mute_progress": "Muting..."
|
||||||
},
|
},
|
||||||
"user_profile": {
|
"user_profile": {
|
||||||
"timeline_title": "User Timeline"
|
"timeline_title": "User Timeline"
|
||||||
|
|
|
@ -28,7 +28,8 @@
|
||||||
"password": "Contraseña",
|
"password": "Contraseña",
|
||||||
"placeholder": "p.ej. lain",
|
"placeholder": "p.ej. lain",
|
||||||
"register": "Registrar",
|
"register": "Registrar",
|
||||||
"username": "Usuario"
|
"username": "Usuario",
|
||||||
|
"hint": "Inicia sesión para unirte a la discusión"
|
||||||
},
|
},
|
||||||
"nav": {
|
"nav": {
|
||||||
"about": "Sobre",
|
"about": "Sobre",
|
||||||
|
@ -55,7 +56,7 @@
|
||||||
"no_more_notifications": "No hay más notificaciones"
|
"no_more_notifications": "No hay más notificaciones"
|
||||||
},
|
},
|
||||||
"post_status": {
|
"post_status": {
|
||||||
"new_status": "Post new status",
|
"new_status": "Publicar un nuevo estado",
|
||||||
"account_not_locked_warning": "Tu cuenta no está {0}. Cualquiera puede seguirte y leer las entradas para Solo-Seguidores.",
|
"account_not_locked_warning": "Tu cuenta no está {0}. Cualquiera puede seguirte y leer las entradas para Solo-Seguidores.",
|
||||||
"account_not_locked_warning_link": "bloqueada",
|
"account_not_locked_warning_link": "bloqueada",
|
||||||
"attachments_sensitive": "Contenido sensible",
|
"attachments_sensitive": "Contenido sensible",
|
||||||
|
@ -139,6 +140,7 @@
|
||||||
"use_one_click_nsfw": "Abrir los adjuntos NSFW con un solo click.",
|
"use_one_click_nsfw": "Abrir los adjuntos NSFW con un solo click.",
|
||||||
"hide_post_stats": "Ocultar las estadísticas de las entradas (p.ej. el número de favoritos)",
|
"hide_post_stats": "Ocultar las estadísticas de las entradas (p.ej. el número de favoritos)",
|
||||||
"hide_user_stats": "Ocultar las estadísticas del usuario (p.ej. el número de seguidores)",
|
"hide_user_stats": "Ocultar las estadísticas del usuario (p.ej. el número de seguidores)",
|
||||||
|
"hide_filtered_statuses": "Ocultar estados filtrados",
|
||||||
"import_followers_from_a_csv_file": "Importar personas que tú sigues a partir de un archivo csv",
|
"import_followers_from_a_csv_file": "Importar personas que tú sigues a partir de un archivo csv",
|
||||||
"import_theme": "Importar tema",
|
"import_theme": "Importar tema",
|
||||||
"inputRadius": "Campos de entrada",
|
"inputRadius": "Campos de entrada",
|
||||||
|
@ -164,7 +166,10 @@
|
||||||
"notification_visibility_mentions": "Menciones",
|
"notification_visibility_mentions": "Menciones",
|
||||||
"notification_visibility_repeats": "Repeticiones (Repeats)",
|
"notification_visibility_repeats": "Repeticiones (Repeats)",
|
||||||
"no_rich_text_description": "Eliminar el formato de texto enriquecido de todas las entradas",
|
"no_rich_text_description": "Eliminar el formato de texto enriquecido de todas las entradas",
|
||||||
"hide_network_description": "No mostrar a quién sigo, ni quién me sigue",
|
"hide_follows_description": "No mostrar a quién sigo",
|
||||||
|
"hide_followers_description": "No mostrar quién me sigue",
|
||||||
|
"show_admin_badge": "Mostrar la placa de administrador en mi perfil",
|
||||||
|
"show_moderator_badge": "Mostrar la placa de moderador en mi perfil",
|
||||||
"nsfw_clickthrough": "Activar el clic para ocultar los adjuntos NSFW",
|
"nsfw_clickthrough": "Activar el clic para ocultar los adjuntos NSFW",
|
||||||
"panelRadius": "Paneles",
|
"panelRadius": "Paneles",
|
||||||
"pause_on_unfocused": "Parar la transmisión cuando no estés en foco.",
|
"pause_on_unfocused": "Parar la transmisión cuando no estés en foco.",
|
||||||
|
@ -191,6 +196,8 @@
|
||||||
"subject_line_email": "Tipo email: \"re: tema\"",
|
"subject_line_email": "Tipo email: \"re: tema\"",
|
||||||
"subject_line_mastodon": "Tipo mastodon: copiar como es",
|
"subject_line_mastodon": "Tipo mastodon: copiar como es",
|
||||||
"subject_line_noop": "No copiar",
|
"subject_line_noop": "No copiar",
|
||||||
|
"post_status_content_type": "Formato de publicación",
|
||||||
|
"status_content_type_plain": "Texto plano",
|
||||||
"stop_gifs": "Iniciar GIFs al pasar el ratón",
|
"stop_gifs": "Iniciar GIFs al pasar el ratón",
|
||||||
"streaming": "Habilite la transmisión automática de nuevas publicaciones cuando se desplaza hacia la parte superior",
|
"streaming": "Habilite la transmisión automática de nuevas publicaciones cuando se desplaza hacia la parte superior",
|
||||||
"text": "Texto",
|
"text": "Texto",
|
||||||
|
|
|
@ -17,7 +17,9 @@
|
||||||
},
|
},
|
||||||
"general": {
|
"general": {
|
||||||
"apply": "てきよう",
|
"apply": "てきよう",
|
||||||
"submit": "そうしん"
|
"submit": "そうしん",
|
||||||
|
"more": "つづき",
|
||||||
|
"generic_error": "エラーになりました"
|
||||||
},
|
},
|
||||||
"login": {
|
"login": {
|
||||||
"login": "ログイン",
|
"login": "ログイン",
|
||||||
|
@ -26,7 +28,8 @@
|
||||||
"password": "パスワード",
|
"password": "パスワード",
|
||||||
"placeholder": "れい: lain",
|
"placeholder": "れい: lain",
|
||||||
"register": "はじめる",
|
"register": "はじめる",
|
||||||
"username": "ユーザーめい"
|
"username": "ユーザーめい",
|
||||||
|
"hint": "はなしあいにくわわるには、ログインしてください"
|
||||||
},
|
},
|
||||||
"nav": {
|
"nav": {
|
||||||
"about": "これはなに?",
|
"about": "これはなに?",
|
||||||
|
@ -49,7 +52,8 @@
|
||||||
"load_older": "ふるいつうちをみる",
|
"load_older": "ふるいつうちをみる",
|
||||||
"notifications": "つうち",
|
"notifications": "つうち",
|
||||||
"read": "よんだ!",
|
"read": "よんだ!",
|
||||||
"repeated_you": "あなたのステータスがリピートされました"
|
"repeated_you": "あなたのステータスがリピートされました",
|
||||||
|
"no_more_notifications": "つうちはありません"
|
||||||
},
|
},
|
||||||
"post_status": {
|
"post_status": {
|
||||||
"new_status": "とうこうする",
|
"new_status": "とうこうする",
|
||||||
|
@ -117,6 +121,7 @@
|
||||||
"delete_account_description": "あなたのアカウントとメッセージが、きえます。",
|
"delete_account_description": "あなたのアカウントとメッセージが、きえます。",
|
||||||
"delete_account_error": "アカウントをけすことが、できなかったかもしれません。インスタンスのかんりしゃに、れんらくしてください。",
|
"delete_account_error": "アカウントをけすことが、できなかったかもしれません。インスタンスのかんりしゃに、れんらくしてください。",
|
||||||
"delete_account_instructions": "ほんとうにアカウントをけしてもいいなら、パスワードをかいてください。",
|
"delete_account_instructions": "ほんとうにアカウントをけしてもいいなら、パスワードをかいてください。",
|
||||||
|
"avatar_size_instruction": "アバターのおおきさは、150×150ピクセルか、それよりもおおきくするといいです。",
|
||||||
"export_theme": "セーブ",
|
"export_theme": "セーブ",
|
||||||
"filtering": "フィルタリング",
|
"filtering": "フィルタリング",
|
||||||
"filtering_explanation": "これらのことばをふくむすべてのものがミュートされます。1ぎょうに1つのことばをかいてください。",
|
"filtering_explanation": "これらのことばをふくむすべてのものがミュートされます。1ぎょうに1つのことばをかいてください。",
|
||||||
|
@ -132,8 +137,10 @@
|
||||||
"hide_attachments_in_tl": "タイムラインのファイルをかくす",
|
"hide_attachments_in_tl": "タイムラインのファイルをかくす",
|
||||||
"hide_isp": "インスタンススペシフィックパネルをかくす",
|
"hide_isp": "インスタンススペシフィックパネルをかくす",
|
||||||
"preload_images": "がぞうをさきよみする",
|
"preload_images": "がぞうをさきよみする",
|
||||||
|
"use_one_click_nsfw": "NSFWなファイルを1クリックでひらく",
|
||||||
"hide_post_stats": "とうこうのとうけいをかくす (れい: おきにいりのかず)",
|
"hide_post_stats": "とうこうのとうけいをかくす (れい: おきにいりのかず)",
|
||||||
"hide_user_stats": "ユーザーのとうけいをかくす (れい: フォロワーのかず)",
|
"hide_user_stats": "ユーザーのとうけいをかくす (れい: フォロワーのかず)",
|
||||||
|
"hide_filtered_statuses": "フィルターされたとうこうをかくす",
|
||||||
"import_followers_from_a_csv_file": "CSVファイルからフォローをインポートする",
|
"import_followers_from_a_csv_file": "CSVファイルからフォローをインポートする",
|
||||||
"import_theme": "ロード",
|
"import_theme": "ロード",
|
||||||
"inputRadius": "インプットフィールド",
|
"inputRadius": "インプットフィールド",
|
||||||
|
@ -148,6 +155,8 @@
|
||||||
"lock_account_description": "あなたがみとめたひとだけ、あなたのアカウントをフォローできる",
|
"lock_account_description": "あなたがみとめたひとだけ、あなたのアカウントをフォローできる",
|
||||||
"loop_video": "ビデオをくりかえす",
|
"loop_video": "ビデオをくりかえす",
|
||||||
"loop_video_silent_only": "おとのないビデオだけくりかえす",
|
"loop_video_silent_only": "おとのないビデオだけくりかえす",
|
||||||
|
"play_videos_in_modal": "ビデオをメディアビューアーでみる",
|
||||||
|
"use_contain_fit": "がぞうのサムネイルを、きりぬかない",
|
||||||
"name": "なまえ",
|
"name": "なまえ",
|
||||||
"name_bio": "なまえとプロフィール",
|
"name_bio": "なまえとプロフィール",
|
||||||
"new_password": "あたらしいパスワード",
|
"new_password": "あたらしいパスワード",
|
||||||
|
@ -157,8 +166,10 @@
|
||||||
"notification_visibility_mentions": "メンション",
|
"notification_visibility_mentions": "メンション",
|
||||||
"notification_visibility_repeats": "リピート",
|
"notification_visibility_repeats": "リピート",
|
||||||
"no_rich_text_description": "リッチテキストをつかわない",
|
"no_rich_text_description": "リッチテキストをつかわない",
|
||||||
"hide_follows_description": "フォローしている人を表示しない",
|
"hide_follows_description": "フォローしているひとをみせない",
|
||||||
"hide_followers_description": "フォローしている人を表示しない",
|
"hide_followers_description": "フォロワーをみせない",
|
||||||
|
"show_admin_badge": "アドミンのしるしをみる",
|
||||||
|
"show_moderator_badge": "モデレーターのしるしをみる",
|
||||||
"nsfw_clickthrough": "NSFWなファイルをかくす",
|
"nsfw_clickthrough": "NSFWなファイルをかくす",
|
||||||
"panelRadius": "パネル",
|
"panelRadius": "パネル",
|
||||||
"pause_on_unfocused": "タブにフォーカスがないときストリーミングをとめる",
|
"pause_on_unfocused": "タブにフォーカスがないときストリーミングをとめる",
|
||||||
|
@ -185,6 +196,8 @@
|
||||||
"subject_line_email": "メールふう: \"re: サブジェクト\"",
|
"subject_line_email": "メールふう: \"re: サブジェクト\"",
|
||||||
"subject_line_mastodon": "マストドンふう: そのままコピー",
|
"subject_line_mastodon": "マストドンふう: そのままコピー",
|
||||||
"subject_line_noop": "コピーしない",
|
"subject_line_noop": "コピーしない",
|
||||||
|
"post_status_content_type": "とうこうのコンテントタイプ",
|
||||||
|
"status_content_type_plain": "プレーンテキスト",
|
||||||
"stop_gifs": "カーソルをかさねたとき、GIFをうごかす",
|
"stop_gifs": "カーソルをかさねたとき、GIFをうごかす",
|
||||||
"streaming": "うえまでスクロールしたとき、じどうてきにストリーミングする",
|
"streaming": "うえまでスクロールしたとき、じどうてきにストリーミングする",
|
||||||
"text": "もじ",
|
"text": "もじ",
|
||||||
|
@ -318,13 +331,15 @@
|
||||||
"no_retweet_hint": "とうこうを「フォロワーのみ」または「ダイレクト」にすると、リピートできなくなります",
|
"no_retweet_hint": "とうこうを「フォロワーのみ」または「ダイレクト」にすると、リピートできなくなります",
|
||||||
"repeated": "リピート",
|
"repeated": "リピート",
|
||||||
"show_new": "よみこみ",
|
"show_new": "よみこみ",
|
||||||
"up_to_date": "さいしん"
|
"up_to_date": "さいしん",
|
||||||
|
"no_more_statuses": "これでおわりです"
|
||||||
},
|
},
|
||||||
"user_card": {
|
"user_card": {
|
||||||
"approve": "うけいれ",
|
"approve": "うけいれ",
|
||||||
"block": "ブロック",
|
"block": "ブロック",
|
||||||
"blocked": "ブロックしています!",
|
"blocked": "ブロックしています!",
|
||||||
"deny": "おことわり",
|
"deny": "おことわり",
|
||||||
|
"favorites": "おきにいり",
|
||||||
"follow": "フォロー",
|
"follow": "フォロー",
|
||||||
"follow_sent": "リクエストを、おくりました!",
|
"follow_sent": "リクエストを、おくりました!",
|
||||||
"follow_progress": "リクエストしています…",
|
"follow_progress": "リクエストしています…",
|
||||||
|
@ -335,6 +350,7 @@
|
||||||
"following": "フォローしています!",
|
"following": "フォローしています!",
|
||||||
"follows_you": "フォローされました!",
|
"follows_you": "フォローされました!",
|
||||||
"its_you": "これはあなたです!",
|
"its_you": "これはあなたです!",
|
||||||
|
"media": "メディア",
|
||||||
"mute": "ミュート",
|
"mute": "ミュート",
|
||||||
"muted": "ミュートしています!",
|
"muted": "ミュートしています!",
|
||||||
"per_day": "/日",
|
"per_day": "/日",
|
||||||
|
|
|
@ -129,6 +129,8 @@
|
||||||
"no_rich_text_description": "Убрать форматирование из всех постов",
|
"no_rich_text_description": "Убрать форматирование из всех постов",
|
||||||
"hide_follows_description": "Не показывать кого я читаю",
|
"hide_follows_description": "Не показывать кого я читаю",
|
||||||
"hide_followers_description": "Не показывать кто читает меня",
|
"hide_followers_description": "Не показывать кто читает меня",
|
||||||
|
"show_admin_badge": "Показывать значок администратора в моем профиле",
|
||||||
|
"show_moderator_badge": "Показывать значок модератора в моем профиле",
|
||||||
"nsfw_clickthrough": "Включить скрытие NSFW вложений",
|
"nsfw_clickthrough": "Включить скрытие NSFW вложений",
|
||||||
"panelRadius": "Панели",
|
"panelRadius": "Панели",
|
||||||
"pause_on_unfocused": "Приостановить загрузку когда вкладка не в фокусе",
|
"pause_on_unfocused": "Приостановить загрузку когда вкладка не в фокусе",
|
||||||
|
|
|
@ -48,7 +48,7 @@ export default function createPersistedState ({
|
||||||
return getState(key, storage).then((savedState) => {
|
return getState(key, storage).then((savedState) => {
|
||||||
return store => {
|
return store => {
|
||||||
try {
|
try {
|
||||||
if (typeof savedState === 'object') {
|
if (savedState !== null && typeof savedState === 'object') {
|
||||||
// build user cache
|
// build user cache
|
||||||
const usersState = savedState.users || {}
|
const usersState = savedState.users || {}
|
||||||
usersState.usersObject = {}
|
usersState.usersObject = {}
|
||||||
|
@ -84,12 +84,12 @@ export default function createPersistedState ({
|
||||||
setState(key, reducer(state, paths), storage)
|
setState(key, reducer(state, paths), storage)
|
||||||
.then(success => {
|
.then(success => {
|
||||||
if (typeof success !== 'undefined') {
|
if (typeof success !== 'undefined') {
|
||||||
if (mutation.type === 'setOption') {
|
if (mutation.type === 'setOption' || mutation.type === 'setCurrentUser') {
|
||||||
store.dispatch('settingsSaved', { success })
|
store.dispatch('settingsSaved', { success })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, error => {
|
}, error => {
|
||||||
if (mutation.type === 'setOption') {
|
if (mutation.type === 'setOption' || mutation.type === 'setCurrentUser') {
|
||||||
store.dispatch('settingsSaved', { error })
|
store.dispatch('settingsSaved', { error })
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import backendInteractorService from '../services/backend_interactor_service/backend_interactor_service.js'
|
import backendInteractorService from '../services/backend_interactor_service/backend_interactor_service.js'
|
||||||
import {isArray} from 'lodash'
|
|
||||||
import { Socket } from 'phoenix'
|
import { Socket } from 'phoenix'
|
||||||
|
|
||||||
const api = {
|
const api = {
|
||||||
|
@ -34,20 +33,12 @@ const api = {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
actions: {
|
actions: {
|
||||||
startFetching (store, timeline) {
|
startFetching (store, {timeline = 'friends', tag = false, userId = false}) {
|
||||||
let userId = false
|
|
||||||
|
|
||||||
// This is for user timelines
|
|
||||||
if (isArray(timeline)) {
|
|
||||||
userId = timeline[1]
|
|
||||||
timeline = timeline[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
// Don't start fetching if we already are.
|
// Don't start fetching if we already are.
|
||||||
if (!store.state.fetchers[timeline]) {
|
if (store.state.fetchers[timeline]) return
|
||||||
const fetcher = store.state.backendInteractor.startFetching({timeline, store, userId})
|
|
||||||
|
const fetcher = store.state.backendInteractor.startFetching({ timeline, store, userId, tag })
|
||||||
store.commit('addFetcher', { timeline, fetcher })
|
store.commit('addFetcher', { timeline, fetcher })
|
||||||
}
|
|
||||||
},
|
},
|
||||||
stopFetching (store, timeline) {
|
stopFetching (store, timeline) {
|
||||||
const fetcher = store.state.fetchers[timeline]
|
const fetcher = store.state.fetchers[timeline]
|
||||||
|
|
|
@ -31,7 +31,7 @@ const defaultState = {
|
||||||
scopeCopy: undefined, // instance default
|
scopeCopy: undefined, // instance default
|
||||||
subjectLineBehavior: undefined, // instance default
|
subjectLineBehavior: undefined, // instance default
|
||||||
alwaysShowSubjectInput: undefined, // instance default
|
alwaysShowSubjectInput: undefined, // instance default
|
||||||
showFeaturesPanel: true
|
postContentType: undefined // instance default
|
||||||
}
|
}
|
||||||
|
|
||||||
const config = {
|
const config = {
|
||||||
|
|
|
@ -21,13 +21,16 @@ const defaultState = {
|
||||||
collapseMessageWithSubject: false,
|
collapseMessageWithSubject: false,
|
||||||
hidePostStats: false,
|
hidePostStats: false,
|
||||||
hideUserStats: false,
|
hideUserStats: false,
|
||||||
|
hideFilteredStatuses: false,
|
||||||
disableChat: false,
|
disableChat: false,
|
||||||
scopeCopy: true,
|
scopeCopy: true,
|
||||||
subjectLineBehavior: 'email',
|
subjectLineBehavior: 'email',
|
||||||
|
postContentType: 'text/plain',
|
||||||
loginMethod: 'password',
|
loginMethod: 'password',
|
||||||
nsfwCensorImage: undefined,
|
nsfwCensorImage: undefined,
|
||||||
vapidPublicKey: undefined,
|
vapidPublicKey: undefined,
|
||||||
noAttachmentLinks: false,
|
noAttachmentLinks: false,
|
||||||
|
showFeaturesPanel: true,
|
||||||
|
|
||||||
// Nasty stuff
|
// Nasty stuff
|
||||||
pleromaBackend: true,
|
pleromaBackend: true,
|
||||||
|
@ -63,9 +66,11 @@ const instance = {
|
||||||
case 'name':
|
case 'name':
|
||||||
dispatch('setPageTitle')
|
dispatch('setPageTitle')
|
||||||
break
|
break
|
||||||
case 'theme':
|
|
||||||
setPreset(value, commit)
|
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
setTheme ({ commit }, themeName) {
|
||||||
|
commit('setInstanceOption', { name: 'theme', value: themeName })
|
||||||
|
return setPreset(themeName, commit)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -296,7 +296,7 @@ const addNewNotifications = (state, { dispatch, notifications, older, visibleNot
|
||||||
notifObj.image = action.attachments[0].url
|
notifObj.image = action.attachments[0].url
|
||||||
}
|
}
|
||||||
|
|
||||||
if (notification.fresh && !state.notifications.desktopNotificationSilence && visibleNotificationTypes.includes(notification.ntype)) {
|
if (!notification.seen && !state.notifications.desktopNotificationSilence && visibleNotificationTypes.includes(notification.type)) {
|
||||||
let notification = new window.Notification(title, notifObj)
|
let notification = new window.Notification(title, notifObj)
|
||||||
// Chrome is known for not closing notifications automatically
|
// Chrome is known for not closing notifications automatically
|
||||||
// according to MDN, anyway.
|
// according to MDN, anyway.
|
||||||
|
|
|
@ -85,6 +85,12 @@ export const mutations = {
|
||||||
addNewUsers (state, users) {
|
addNewUsers (state, users) {
|
||||||
each(users, (user) => mergeOrAdd(state.users, state.usersObject, user))
|
each(users, (user) => mergeOrAdd(state.users, state.usersObject, user))
|
||||||
},
|
},
|
||||||
|
saveBlocks (state, blockIds) {
|
||||||
|
state.currentUser.blockIds = blockIds
|
||||||
|
},
|
||||||
|
saveMutes (state, muteIds) {
|
||||||
|
state.currentUser.muteIds = muteIds
|
||||||
|
},
|
||||||
setUserForStatus (state, status) {
|
setUserForStatus (state, status) {
|
||||||
status.user = state.usersObject[status.user.id]
|
status.user = state.usersObject[status.user.id]
|
||||||
},
|
},
|
||||||
|
@ -137,6 +143,38 @@ const users = {
|
||||||
store.rootState.api.backendInteractor.fetchUser({ id })
|
store.rootState.api.backendInteractor.fetchUser({ id })
|
||||||
.then((user) => store.commit('addNewUsers', [user]))
|
.then((user) => store.commit('addNewUsers', [user]))
|
||||||
},
|
},
|
||||||
|
fetchBlocks (store) {
|
||||||
|
return store.rootState.api.backendInteractor.fetchBlocks()
|
||||||
|
.then((blocks) => {
|
||||||
|
store.commit('saveBlocks', map(blocks, 'id'))
|
||||||
|
store.commit('addNewUsers', blocks)
|
||||||
|
return blocks
|
||||||
|
})
|
||||||
|
},
|
||||||
|
blockUser (store, id) {
|
||||||
|
return store.rootState.api.backendInteractor.blockUser(id)
|
||||||
|
.then((user) => store.commit('addNewUsers', [user]))
|
||||||
|
},
|
||||||
|
unblockUser (store, id) {
|
||||||
|
return store.rootState.api.backendInteractor.unblockUser(id)
|
||||||
|
.then((user) => store.commit('addNewUsers', [user]))
|
||||||
|
},
|
||||||
|
fetchMutes (store) {
|
||||||
|
return store.rootState.api.backendInteractor.fetchMutes()
|
||||||
|
.then((mutedUsers) => {
|
||||||
|
each(mutedUsers, (user) => { user.muted = true })
|
||||||
|
store.commit('addNewUsers', mutedUsers)
|
||||||
|
store.commit('saveMutes', map(mutedUsers, 'id'))
|
||||||
|
})
|
||||||
|
},
|
||||||
|
muteUser (store, id) {
|
||||||
|
return store.state.api.backendInteractor.setUserMute({ id, muted: true })
|
||||||
|
.then((user) => store.commit('addNewUsers', [user]))
|
||||||
|
},
|
||||||
|
unmuteUser (store, id) {
|
||||||
|
return store.state.api.backendInteractor.setUserMute({ id, muted: false })
|
||||||
|
.then((user) => store.commit('addNewUsers', [user]))
|
||||||
|
},
|
||||||
addFriends ({ rootState, commit }, fetchBy) {
|
addFriends ({ rootState, commit }, fetchBy) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const user = rootState.users.usersObject[fetchBy]
|
const user = rootState.users.usersObject[fetchBy]
|
||||||
|
@ -231,8 +269,14 @@ const users = {
|
||||||
store.commit('setToken', result.access_token)
|
store.commit('setToken', result.access_token)
|
||||||
store.dispatch('loginUser', result.access_token)
|
store.dispatch('loginUser', result.access_token)
|
||||||
} else {
|
} else {
|
||||||
let data = await response.json()
|
const data = await response.json()
|
||||||
let errors = humanizeErrors(JSON.parse(data.error))
|
let errors = JSON.parse(data.error)
|
||||||
|
// replace ap_id with username
|
||||||
|
if (errors.ap_id) {
|
||||||
|
errors.username = errors.ap_id
|
||||||
|
delete errors.ap_id
|
||||||
|
}
|
||||||
|
errors = humanizeErrors(errors)
|
||||||
store.commit('signUpFailure', errors)
|
store.commit('signUpFailure', errors)
|
||||||
throw Error(errors)
|
throw Error(errors)
|
||||||
}
|
}
|
||||||
|
@ -257,6 +301,8 @@ const users = {
|
||||||
const user = data
|
const user = data
|
||||||
// user.credentials = userCredentials
|
// user.credentials = userCredentials
|
||||||
user.credentials = accessToken
|
user.credentials = accessToken
|
||||||
|
user.blockIds = []
|
||||||
|
user.muteIds = []
|
||||||
commit('setCurrentUser', user)
|
commit('setCurrentUser', user)
|
||||||
commit('addNewUsers', [user])
|
commit('addNewUsers', [user])
|
||||||
|
|
||||||
|
@ -271,13 +317,10 @@ const users = {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start getting fresh posts.
|
// Start getting fresh posts.
|
||||||
store.dispatch('startFetching', 'friends')
|
store.dispatch('startFetching', { timeline: 'friends' })
|
||||||
|
|
||||||
// Get user mutes and follower info
|
// Get user mutes
|
||||||
store.rootState.api.backendInteractor.fetchMutes().then((mutedUsers) => {
|
store.dispatch('fetchMutes')
|
||||||
each(mutedUsers, (user) => { user.muted = true })
|
|
||||||
store.commit('addNewUsers', mutedUsers)
|
|
||||||
})
|
|
||||||
|
|
||||||
// Fetch our friends
|
// Fetch our friends
|
||||||
store.rootState.api.backendInteractor.fetchFriends({ id: user.id })
|
store.rootState.api.backendInteractor.fetchFriends({ id: user.id })
|
||||||
|
|
|
@ -18,6 +18,7 @@ const MENTIONS_URL = '/api/statuses/mentions.json'
|
||||||
const DM_TIMELINE_URL = '/api/statuses/dm_timeline.json'
|
const DM_TIMELINE_URL = '/api/statuses/dm_timeline.json'
|
||||||
const FOLLOWERS_URL = '/api/statuses/followers.json'
|
const FOLLOWERS_URL = '/api/statuses/followers.json'
|
||||||
const FRIENDS_URL = '/api/statuses/friends.json'
|
const FRIENDS_URL = '/api/statuses/friends.json'
|
||||||
|
const BLOCKS_URL = '/api/statuses/blocks.json'
|
||||||
const FOLLOWING_URL = '/api/friendships/create.json'
|
const FOLLOWING_URL = '/api/friendships/create.json'
|
||||||
const UNFOLLOWING_URL = '/api/friendships/destroy.json'
|
const UNFOLLOWING_URL = '/api/friendships/destroy.json'
|
||||||
const QVITTER_USER_PREF_URL = '/api/qvitter/set_profile_pref.json'
|
const QVITTER_USER_PREF_URL = '/api/qvitter/set_profile_pref.json'
|
||||||
|
@ -130,7 +131,7 @@ const updateBanner = ({credentials, params}) => {
|
||||||
// description
|
// description
|
||||||
const updateProfile = ({credentials, params}) => {
|
const updateProfile = ({credentials, params}) => {
|
||||||
// Always include these fields, because they might be empty or false
|
// Always include these fields, because they might be empty or false
|
||||||
const fields = ['description', 'locked', 'no_rich_text', 'hide_follows', 'hide_followers']
|
const fields = ['description', 'locked', 'no_rich_text', 'hide_follows', 'hide_followers', 'show_role']
|
||||||
let url = PROFILE_UPDATE_URL
|
let url = PROFILE_UPDATE_URL
|
||||||
|
|
||||||
const form = new FormData()
|
const form = new FormData()
|
||||||
|
@ -257,6 +258,13 @@ const fetchFriends = ({id, page, credentials}) => {
|
||||||
.then((data) => data.map(parseUser))
|
.then((data) => data.map(parseUser))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const exportFriends = ({id, credentials}) => {
|
||||||
|
let url = `${FRIENDS_URL}?user_id=${id}&all=true`
|
||||||
|
return fetch(url, { headers: authHeaders(credentials) })
|
||||||
|
.then((data) => data.json())
|
||||||
|
.then((data) => data.map(parseUser))
|
||||||
|
}
|
||||||
|
|
||||||
const fetchFollowers = ({id, page, credentials}) => {
|
const fetchFollowers = ({id, page, credentials}) => {
|
||||||
let url = `${FOLLOWERS_URL}?user_id=${id}`
|
let url = `${FOLLOWERS_URL}?user_id=${id}`
|
||||||
if (page) {
|
if (page) {
|
||||||
|
@ -512,6 +520,17 @@ const fetchMutes = ({credentials}) => {
|
||||||
}).then((data) => data.json())
|
}).then((data) => data.json())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const fetchBlocks = ({page, credentials}) => {
|
||||||
|
return fetch(BLOCKS_URL, {
|
||||||
|
headers: authHeaders(credentials)
|
||||||
|
}).then((data) => {
|
||||||
|
if (data.ok) {
|
||||||
|
return data.json()
|
||||||
|
}
|
||||||
|
throw new Error('Error fetching blocks', data)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
const suggestions = ({credentials}) => {
|
const suggestions = ({credentials}) => {
|
||||||
return fetch(SUGGESTIONS_URL, {
|
return fetch(SUGGESTIONS_URL, {
|
||||||
headers: authHeaders(credentials)
|
headers: authHeaders(credentials)
|
||||||
|
@ -536,6 +555,7 @@ const apiService = {
|
||||||
fetchConversation,
|
fetchConversation,
|
||||||
fetchStatus,
|
fetchStatus,
|
||||||
fetchFriends,
|
fetchFriends,
|
||||||
|
exportFriends,
|
||||||
fetchFollowers,
|
fetchFollowers,
|
||||||
followUser,
|
followUser,
|
||||||
unfollowUser,
|
unfollowUser,
|
||||||
|
@ -552,6 +572,7 @@ const apiService = {
|
||||||
fetchAllFollowing,
|
fetchAllFollowing,
|
||||||
setUserMute,
|
setUserMute,
|
||||||
fetchMutes,
|
fetchMutes,
|
||||||
|
fetchBlocks,
|
||||||
register,
|
register,
|
||||||
getCaptcha,
|
getCaptcha,
|
||||||
updateAvatar,
|
updateAvatar,
|
||||||
|
|
|
@ -14,6 +14,10 @@ const backendInteractorService = (credentials) => {
|
||||||
return apiService.fetchFriends({id, page, credentials})
|
return apiService.fetchFriends({id, page, credentials})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const exportFriends = ({id}) => {
|
||||||
|
return apiService.exportFriends({id, credentials})
|
||||||
|
}
|
||||||
|
|
||||||
const fetchFollowers = ({id, page}) => {
|
const fetchFollowers = ({id, page}) => {
|
||||||
return apiService.fetchFollowers({id, page, credentials})
|
return apiService.fetchFollowers({id, page, credentials})
|
||||||
}
|
}
|
||||||
|
@ -59,6 +63,7 @@ const backendInteractorService = (credentials) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const fetchMutes = () => apiService.fetchMutes({credentials})
|
const fetchMutes = () => apiService.fetchMutes({credentials})
|
||||||
|
const fetchBlocks = (params) => apiService.fetchBlocks({credentials, ...params})
|
||||||
const fetchFollowRequests = () => apiService.fetchFollowRequests({credentials})
|
const fetchFollowRequests = () => apiService.fetchFollowRequests({credentials})
|
||||||
|
|
||||||
const getCaptcha = () => apiService.getCaptcha()
|
const getCaptcha = () => apiService.getCaptcha()
|
||||||
|
@ -78,6 +83,7 @@ const backendInteractorService = (credentials) => {
|
||||||
fetchStatus,
|
fetchStatus,
|
||||||
fetchConversation,
|
fetchConversation,
|
||||||
fetchFriends,
|
fetchFriends,
|
||||||
|
exportFriends,
|
||||||
fetchFollowers,
|
fetchFollowers,
|
||||||
followUser,
|
followUser,
|
||||||
unfollowUser,
|
unfollowUser,
|
||||||
|
@ -89,6 +95,7 @@ const backendInteractorService = (credentials) => {
|
||||||
startFetching,
|
startFetching,
|
||||||
setUserMute,
|
setUserMute,
|
||||||
fetchMutes,
|
fetchMutes,
|
||||||
|
fetchBlocks,
|
||||||
register,
|
register,
|
||||||
getCaptcha,
|
getCaptcha,
|
||||||
updateAvatar,
|
updateAvatar,
|
||||||
|
|
|
@ -90,6 +90,8 @@ export const parseUser = (data) => {
|
||||||
output.statusnet_blocking = data.statusnet_blocking
|
output.statusnet_blocking = data.statusnet_blocking
|
||||||
|
|
||||||
output.is_local = data.is_local
|
output.is_local = data.is_local
|
||||||
|
output.role = data.role
|
||||||
|
output.show_role = data.show_role
|
||||||
|
|
||||||
output.follows_you = data.follows_you
|
output.follows_you = data.follows_you
|
||||||
|
|
||||||
|
@ -115,6 +117,9 @@ export const parseUser = (data) => {
|
||||||
output.statuses_count = data.statuses_count
|
output.statuses_count = data.statuses_count
|
||||||
output.friends = []
|
output.friends = []
|
||||||
output.followers = []
|
output.followers = []
|
||||||
|
if (data.pleroma) {
|
||||||
|
output.follow_request_count = data.pleroma.follow_request_count
|
||||||
|
}
|
||||||
|
|
||||||
return output
|
return output
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,74 @@
|
||||||
|
const fetchUser = (attempt, user, store) => new Promise((resolve, reject) => {
|
||||||
|
setTimeout(() => {
|
||||||
|
store.state.api.backendInteractor.fetchUser({ id: user.id })
|
||||||
|
.then((user) => store.commit('addNewUsers', [user]))
|
||||||
|
.then(() => resolve([user.following, attempt]))
|
||||||
|
.catch((e) => reject(e))
|
||||||
|
}, 500)
|
||||||
|
}).then(([following, attempt]) => {
|
||||||
|
if (!following && attempt <= 3) {
|
||||||
|
// If we BE reports that we still not following that user - retry,
|
||||||
|
// increment attempts by one
|
||||||
|
return fetchUser(++attempt, user, store)
|
||||||
|
} else {
|
||||||
|
// If we run out of attempts, just return whatever status is.
|
||||||
|
return following
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export const requestFollow = (user, store) => new Promise((resolve, reject) => {
|
||||||
|
store.state.api.backendInteractor.followUser(user.id)
|
||||||
|
.then((updated) => {
|
||||||
|
store.commit('addNewUsers', [updated])
|
||||||
|
|
||||||
|
// For locked users we just mark it that we sent the follow request
|
||||||
|
if (updated.locked) {
|
||||||
|
resolve({
|
||||||
|
sent: true,
|
||||||
|
updated
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (updated.following) {
|
||||||
|
// If we get result immediately, just stop.
|
||||||
|
resolve({
|
||||||
|
sent: false,
|
||||||
|
updated
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// But usually we don't get result immediately, so we ask server
|
||||||
|
// for updated user profile to confirm if we are following them
|
||||||
|
// Sometimes it takes several tries. Sometimes we end up not following
|
||||||
|
// user anyway, probably because they locked themselves and we
|
||||||
|
// don't know that yet.
|
||||||
|
// Recursive Promise, it will call itself up to 3 times.
|
||||||
|
|
||||||
|
return fetchUser(1, user, store)
|
||||||
|
.then((following) => {
|
||||||
|
if (following) {
|
||||||
|
// We confirmed and everything's good.
|
||||||
|
resolve({
|
||||||
|
sent: false,
|
||||||
|
updated
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
// If after all the tries, just treat it as if user is locked
|
||||||
|
resolve({
|
||||||
|
sent: false,
|
||||||
|
updated
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
export const requestUnfollow = (user, store) => new Promise((resolve, reject) => {
|
||||||
|
store.state.api.backendInteractor.unfollowUser(user.id)
|
||||||
|
.then((updated) => {
|
||||||
|
store.commit('addNewUsers', [updated])
|
||||||
|
resolve({
|
||||||
|
updated
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
|
@ -0,0 +1,23 @@
|
||||||
|
export const mentionMatchesUrl = (attention, url) => {
|
||||||
|
if (url === attention.statusnet_profile_url) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
const [namepart, instancepart] = attention.screen_name.split('@')
|
||||||
|
const matchstring = new RegExp('://' + instancepart + '/.*' + namepart + '$', 'g')
|
||||||
|
|
||||||
|
return !!url.match(matchstring)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract tag name from pleroma or mastodon url.
|
||||||
|
* i.e https://bikeshed.party/tag/photo or https://quey.org/tags/sky
|
||||||
|
* @param {string} url
|
||||||
|
*/
|
||||||
|
export const extractTagFromUrl = (url) => {
|
||||||
|
const regex = /tag[s]*\/(\w+)$/g
|
||||||
|
const result = regex.exec(url)
|
||||||
|
if (!result) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return result[1]
|
||||||
|
}
|
|
@ -1,9 +0,0 @@
|
||||||
|
|
||||||
export const mentionMatchesUrl = (attention, url) => {
|
|
||||||
if (url === attention.statusnet_profile_url) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
const [namepart, instancepart] = attention.screen_name.split('@')
|
|
||||||
const matchstring = new RegExp('://' + instancepart + '/.*' + namepart + '$', 'g')
|
|
||||||
return !!url.match(matchstring)
|
|
||||||
}
|
|
|
@ -480,7 +480,7 @@ const getThemes = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const setPreset = (val, commit) => {
|
const setPreset = (val, commit) => {
|
||||||
getThemes().then((themes) => {
|
return getThemes().then((themes) => {
|
||||||
const theme = themes[val] ? themes[val] : themes['pleroma-dark']
|
const theme = themes[val] ? themes[val] : themes['pleroma-dark']
|
||||||
const isV1 = Array.isArray(theme)
|
const isV1 = Array.isArray(theme)
|
||||||
const data = isV1 ? {} : theme.theme
|
const data = isV1 ? {} : theme.theme
|
||||||
|
|
BIN
static/bg2.jpg
BIN
static/bg2.jpg
Binary file not shown.
Before Width: | Height: | Size: 224 KiB |
BIN
static/bgalt.jpg
BIN
static/bgalt.jpg
Binary file not shown.
Before Width: | Height: | Size: 323 KiB |
|
@ -13,11 +13,13 @@
|
||||||
"collapseMessageWithSubject": false,
|
"collapseMessageWithSubject": false,
|
||||||
"scopeCopy": true,
|
"scopeCopy": true,
|
||||||
"subjectLineBehavior": "email",
|
"subjectLineBehavior": "email",
|
||||||
|
"postContentType": "text/plain",
|
||||||
"alwaysShowSubjectInput": true,
|
"alwaysShowSubjectInput": true,
|
||||||
"hidePostStats": false,
|
"hidePostStats": false,
|
||||||
"hideUserStats": false,
|
"hideUserStats": false,
|
||||||
"loginMethod": "password",
|
"loginMethod": "password",
|
||||||
"webPushNotifications": false,
|
"webPushNotifications": false,
|
||||||
"noAttachmentLinks": false,
|
"noAttachmentLinks": false,
|
||||||
"nsfwCensorImage": ""
|
"nsfwCensorImage": "",
|
||||||
|
"showFeaturesPanel": true
|
||||||
}
|
}
|
||||||
|
|
File diff suppressed because one or more lines are too long
|
@ -241,7 +241,7 @@ describe('API Entities normalizer', () => {
|
||||||
notice: makeMockStatusQvitter({ id: 444 }),
|
notice: makeMockStatusQvitter({ id: 444 }),
|
||||||
from_profile: makeMockUserQvitter({ id: 'spurdo' })
|
from_profile: makeMockUserQvitter({ id: 'spurdo' })
|
||||||
})
|
})
|
||||||
expect(parseNotification(notif)).to.have.property('id', '123')
|
expect(parseNotification(notif)).to.have.property('id', 123)
|
||||||
expect(parseNotification(notif)).to.have.property('seen', false)
|
expect(parseNotification(notif)).to.have.property('seen', false)
|
||||||
expect(parseNotification(notif)).to.have.deep.property('status.id', '444')
|
expect(parseNotification(notif)).to.have.deep.property('status.id', '444')
|
||||||
expect(parseNotification(notif)).to.have.deep.property('action.id', '444')
|
expect(parseNotification(notif)).to.have.deep.property('action.id', '444')
|
||||||
|
@ -259,7 +259,7 @@ describe('API Entities normalizer', () => {
|
||||||
is_seen: 1,
|
is_seen: 1,
|
||||||
from_profile: makeMockUserQvitter({ id: 'spurdo' })
|
from_profile: makeMockUserQvitter({ id: 'spurdo' })
|
||||||
})
|
})
|
||||||
expect(parseNotification(notif)).to.have.property('id', '123')
|
expect(parseNotification(notif)).to.have.property('id', 123)
|
||||||
expect(parseNotification(notif)).to.have.property('type', 'like')
|
expect(parseNotification(notif)).to.have.property('type', 'like')
|
||||||
expect(parseNotification(notif)).to.have.property('seen', true)
|
expect(parseNotification(notif)).to.have.property('seen', true)
|
||||||
expect(parseNotification(notif)).to.have.deep.property('status.id', '4412')
|
expect(parseNotification(notif)).to.have.deep.property('status.id', '4412')
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import * as MentionMatcher from 'src/services/mention_matcher/mention_matcher.js'
|
import * as MatcherService from 'src/services/matcher/matcher.service.js'
|
||||||
|
|
||||||
const localAttn = () => ({
|
const localAttn = () => ({
|
||||||
id: 123,
|
id: 123,
|
||||||
|
@ -16,48 +16,67 @@ const externalAttn = () => ({
|
||||||
statusnet_profile_url: 'https://instance.com/users/person'
|
statusnet_profile_url: 'https://instance.com/users/person'
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('MentionMatcher', () => {
|
describe('MatcherService', () => {
|
||||||
describe.only('mentionMatchesUrl', () => {
|
describe('mentionMatchesUrl', () => {
|
||||||
it('should match local mention', () => {
|
it('should match local mention', () => {
|
||||||
const attention = localAttn()
|
const attention = localAttn()
|
||||||
const url = 'https://instance.com/users/person'
|
const url = 'https://instance.com/users/person'
|
||||||
|
|
||||||
expect(MentionMatcher.mentionMatchesUrl(attention, url)).to.eql(true)
|
expect(MatcherService.mentionMatchesUrl(attention, url)).to.eql(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should not match a local mention with same name but different instance', () => {
|
it('should not match a local mention with same name but different instance', () => {
|
||||||
const attention = localAttn()
|
const attention = localAttn()
|
||||||
const url = 'https://website.com/users/person'
|
const url = 'https://website.com/users/person'
|
||||||
|
|
||||||
expect(MentionMatcher.mentionMatchesUrl(attention, url)).to.eql(false)
|
expect(MatcherService.mentionMatchesUrl(attention, url)).to.eql(false)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should match external pleroma mention', () => {
|
it('should match external pleroma mention', () => {
|
||||||
const attention = externalAttn()
|
const attention = externalAttn()
|
||||||
const url = 'https://instance.com/users/person'
|
const url = 'https://instance.com/users/person'
|
||||||
|
|
||||||
expect(MentionMatcher.mentionMatchesUrl(attention, url)).to.eql(true)
|
expect(MatcherService.mentionMatchesUrl(attention, url)).to.eql(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should not match external pleroma mention with same name but different instance', () => {
|
it('should not match external pleroma mention with same name but different instance', () => {
|
||||||
const attention = externalAttn()
|
const attention = externalAttn()
|
||||||
const url = 'https://website.com/users/person'
|
const url = 'https://website.com/users/person'
|
||||||
|
|
||||||
expect(MentionMatcher.mentionMatchesUrl(attention, url)).to.eql(false)
|
expect(MatcherService.mentionMatchesUrl(attention, url)).to.eql(false)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should match external mastodon mention', () => {
|
it('should match external mastodon mention', () => {
|
||||||
const attention = externalAttn()
|
const attention = externalAttn()
|
||||||
const url = 'https://instance.com/@person'
|
const url = 'https://instance.com/@person'
|
||||||
|
|
||||||
expect(MentionMatcher.mentionMatchesUrl(attention, url)).to.eql(true)
|
expect(MatcherService.mentionMatchesUrl(attention, url)).to.eql(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should not match external mastodon mention with same name but different instance', () => {
|
it('should not match external mastodon mention with same name but different instance', () => {
|
||||||
const attention = externalAttn()
|
const attention = externalAttn()
|
||||||
const url = 'https://website.com/@person'
|
const url = 'https://website.com/@person'
|
||||||
|
|
||||||
expect(MentionMatcher.mentionMatchesUrl(attention, url)).to.eql(false)
|
expect(MatcherService.mentionMatchesUrl(attention, url)).to.eql(false)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
describe('extractTagFromUrl', () => {
|
||||||
|
it('should return tag name from valid pleroma url', () => {
|
||||||
|
const url = 'https://website.com/tag/photo'
|
||||||
|
|
||||||
|
expect(MatcherService.extractTagFromUrl(url)).to.eql('photo')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return tag name from valid mastodon url', () => {
|
||||||
|
const url = 'https://website.com/tags/sky'
|
||||||
|
|
||||||
|
expect(MatcherService.extractTagFromUrl(url)).to.eql('sky')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should not return string but false if invalid url', () => {
|
||||||
|
const url = 'https://website.com/users/sky'
|
||||||
|
|
||||||
|
expect(MatcherService.extractTagFromUrl(url)).to.eql(false)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
Loading…
Reference in New Issue