Merge branch 'rc/2.0.5' into 'master'
Update MASTER for 2.0.5 patch See merge request pleroma/pleroma-fe!1105
This commit is contained in:
commit
5d49edc823
11
CHANGELOG.md
11
CHANGELOG.md
@ -6,6 +6,17 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
|||||||
### Changed
|
### Changed
|
||||||
- Removed the use of with_move parameters when fetching notifications
|
- Removed the use of with_move parameters when fetching notifications
|
||||||
|
|
||||||
|
## [2.0.5] - 2020-05-12
|
||||||
|
### Add
|
||||||
|
- Added private notifications option for push notifications
|
||||||
|
- 'Copy link' button for statuses (in the ellipsis menu)
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Registration page no longer requires email if the server is configured not to require it
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Status ellipsis menu closes properly when selecting certain options
|
||||||
|
|
||||||
## [2.0.3] - 2020-05-02
|
## [2.0.3] - 2020-05-02
|
||||||
### Fixed
|
### Fixed
|
||||||
- Show more/less works correctly with auto-collapsed subjects and long posts
|
- Show more/less works correctly with auto-collapsed subjects and long posts
|
||||||
|
@ -241,6 +241,9 @@ const getNodeInfo = async ({ store }) => {
|
|||||||
: federation.enabled
|
: federation.enabled
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const accountActivationRequired = metadata.accountActivationRequired
|
||||||
|
store.dispatch('setInstanceOption', { name: 'accountActivationRequired', value: accountActivationRequired })
|
||||||
|
|
||||||
const accounts = metadata.staffAccounts
|
const accounts = metadata.staffAccounts
|
||||||
resolveStaffAccounts({ store, accounts })
|
resolveStaffAccounts({ store, accounts })
|
||||||
} else {
|
} else {
|
||||||
@ -304,6 +307,9 @@ const afterStoreSetup = async ({ store, i18n }) => {
|
|||||||
getNodeInfo({ store })
|
getNodeInfo({ store })
|
||||||
])
|
])
|
||||||
|
|
||||||
|
// Start fetching things that don't need to block the UI
|
||||||
|
store.dispatch('fetchMutes')
|
||||||
|
|
||||||
const router = new VueRouter({
|
const router = new VueRouter({
|
||||||
mode: 'history',
|
mode: 'history',
|
||||||
routes: routes(store),
|
routes: routes(store),
|
||||||
|
@ -3,7 +3,7 @@ import Popover from '../popover/popover.vue'
|
|||||||
|
|
||||||
const AccountActions = {
|
const AccountActions = {
|
||||||
props: [
|
props: [
|
||||||
'user'
|
'user', 'relationship'
|
||||||
],
|
],
|
||||||
data () {
|
data () {
|
||||||
return { }
|
return { }
|
||||||
|
@ -9,16 +9,16 @@
|
|||||||
class="account-tools-popover"
|
class="account-tools-popover"
|
||||||
>
|
>
|
||||||
<div class="dropdown-menu">
|
<div class="dropdown-menu">
|
||||||
<template v-if="user.following">
|
<template v-if="relationship.following">
|
||||||
<button
|
<button
|
||||||
v-if="user.showing_reblogs"
|
v-if="relationship.showing_reblogs"
|
||||||
class="btn btn-default dropdown-item"
|
class="btn btn-default dropdown-item"
|
||||||
@click="hideRepeats"
|
@click="hideRepeats"
|
||||||
>
|
>
|
||||||
{{ $t('user_card.hide_repeats') }}
|
{{ $t('user_card.hide_repeats') }}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
v-if="!user.showing_reblogs"
|
v-if="!relationship.showing_reblogs"
|
||||||
class="btn btn-default dropdown-item"
|
class="btn btn-default dropdown-item"
|
||||||
@click="showRepeats"
|
@click="showRepeats"
|
||||||
>
|
>
|
||||||
@ -30,7 +30,7 @@
|
|||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
<button
|
<button
|
||||||
v-if="user.statusnet_blocking"
|
v-if="relationship.blocking"
|
||||||
class="btn btn-default btn-block dropdown-item"
|
class="btn btn-default btn-block dropdown-item"
|
||||||
@click="unblockUser"
|
@click="unblockUser"
|
||||||
>
|
>
|
||||||
|
@ -12,7 +12,7 @@
|
|||||||
class="basic-user-card-expanded-content"
|
class="basic-user-card-expanded-content"
|
||||||
>
|
>
|
||||||
<UserCard
|
<UserCard
|
||||||
:user="user"
|
:user-id="user.id"
|
||||||
:rounded="true"
|
:rounded="true"
|
||||||
:bordered="true"
|
:bordered="true"
|
||||||
/>
|
/>
|
||||||
|
@ -11,8 +11,11 @@ const BlockCard = {
|
|||||||
user () {
|
user () {
|
||||||
return this.$store.getters.findUser(this.userId)
|
return this.$store.getters.findUser(this.userId)
|
||||||
},
|
},
|
||||||
|
relationship () {
|
||||||
|
return this.$store.getters.relationship(this.userId)
|
||||||
|
},
|
||||||
blocked () {
|
blocked () {
|
||||||
return this.user.statusnet_blocking
|
return this.relationship.blocking
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
|
@ -29,6 +29,11 @@ const ExtraButtons = {
|
|||||||
this.$store.dispatch('unmuteConversation', this.status.id)
|
this.$store.dispatch('unmuteConversation', this.status.id)
|
||||||
.then(() => this.$emit('onSuccess'))
|
.then(() => this.$emit('onSuccess'))
|
||||||
.catch(err => this.$emit('onError', err.error.error))
|
.catch(err => this.$emit('onError', err.error.error))
|
||||||
|
},
|
||||||
|
copyLink () {
|
||||||
|
navigator.clipboard.writeText(this.statusLink)
|
||||||
|
.then(() => this.$emit('onSuccess'))
|
||||||
|
.catch(err => this.$emit('onError', err.error.error))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@ -46,6 +51,9 @@ const ExtraButtons = {
|
|||||||
},
|
},
|
||||||
canMute () {
|
canMute () {
|
||||||
return !!this.currentUser
|
return !!this.currentUser
|
||||||
|
},
|
||||||
|
statusLink () {
|
||||||
|
return `${this.$store.state.instance.server}${this.$router.resolve({ name: 'conversation', params: { id: this.status.id } }).href}`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,11 +1,13 @@
|
|||||||
<template>
|
<template>
|
||||||
<Popover
|
<Popover
|
||||||
v-if="canDelete || canMute || canPin"
|
|
||||||
trigger="click"
|
trigger="click"
|
||||||
placement="top"
|
placement="top"
|
||||||
class="extra-button-popover"
|
class="extra-button-popover"
|
||||||
>
|
>
|
||||||
<div slot="content">
|
<div
|
||||||
|
slot="content"
|
||||||
|
slot-scope="{close}"
|
||||||
|
>
|
||||||
<div class="dropdown-menu">
|
<div class="dropdown-menu">
|
||||||
<button
|
<button
|
||||||
v-if="canMute && !status.thread_muted"
|
v-if="canMute && !status.thread_muted"
|
||||||
@ -23,28 +25,35 @@
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
v-if="!status.pinned && canPin"
|
v-if="!status.pinned && canPin"
|
||||||
v-close-popover
|
|
||||||
class="dropdown-item dropdown-item-icon"
|
class="dropdown-item dropdown-item-icon"
|
||||||
@click.prevent="pinStatus"
|
@click.prevent="pinStatus"
|
||||||
|
@click="close"
|
||||||
>
|
>
|
||||||
<i class="icon-pin" /><span>{{ $t("status.pin") }}</span>
|
<i class="icon-pin" /><span>{{ $t("status.pin") }}</span>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
v-if="status.pinned && canPin"
|
v-if="status.pinned && canPin"
|
||||||
v-close-popover
|
|
||||||
class="dropdown-item dropdown-item-icon"
|
class="dropdown-item dropdown-item-icon"
|
||||||
@click.prevent="unpinStatus"
|
@click.prevent="unpinStatus"
|
||||||
|
@click="close"
|
||||||
>
|
>
|
||||||
<i class="icon-pin" /><span>{{ $t("status.unpin") }}</span>
|
<i class="icon-pin" /><span>{{ $t("status.unpin") }}</span>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
v-if="canDelete"
|
v-if="canDelete"
|
||||||
v-close-popover
|
|
||||||
class="dropdown-item dropdown-item-icon"
|
class="dropdown-item dropdown-item-icon"
|
||||||
@click.prevent="deleteStatus"
|
@click.prevent="deleteStatus"
|
||||||
|
@click="close"
|
||||||
>
|
>
|
||||||
<i class="icon-cancel" /><span>{{ $t("status.delete") }}</span>
|
<i class="icon-cancel" /><span>{{ $t("status.delete") }}</span>
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
class="dropdown-item dropdown-item-icon"
|
||||||
|
@click.prevent="copyLink"
|
||||||
|
@click="close"
|
||||||
|
>
|
||||||
|
<i class="icon-share" /><span>{{ $t("status.copy_link") }}</span>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<i
|
<i
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { requestFollow, requestUnfollow } from '../../services/follow_manipulate/follow_manipulate'
|
import { requestFollow, requestUnfollow } from '../../services/follow_manipulate/follow_manipulate'
|
||||||
export default {
|
export default {
|
||||||
props: ['user', 'labelFollowing', 'buttonClass'],
|
props: ['relationship', 'labelFollowing', 'buttonClass'],
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
inProgress: false
|
inProgress: false
|
||||||
@ -8,12 +8,12 @@ export default {
|
|||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
isPressed () {
|
isPressed () {
|
||||||
return this.inProgress || this.user.following
|
return this.inProgress || this.relationship.following
|
||||||
},
|
},
|
||||||
title () {
|
title () {
|
||||||
if (this.inProgress || this.user.following) {
|
if (this.inProgress || this.relationship.following) {
|
||||||
return this.$t('user_card.follow_unfollow')
|
return this.$t('user_card.follow_unfollow')
|
||||||
} else if (this.user.requested) {
|
} else if (this.relationship.requested) {
|
||||||
return this.$t('user_card.follow_again')
|
return this.$t('user_card.follow_again')
|
||||||
} else {
|
} else {
|
||||||
return this.$t('user_card.follow')
|
return this.$t('user_card.follow')
|
||||||
@ -22,9 +22,9 @@ export default {
|
|||||||
label () {
|
label () {
|
||||||
if (this.inProgress) {
|
if (this.inProgress) {
|
||||||
return this.$t('user_card.follow_progress')
|
return this.$t('user_card.follow_progress')
|
||||||
} else if (this.user.following) {
|
} else if (this.relationship.following) {
|
||||||
return this.labelFollowing || this.$t('user_card.following')
|
return this.labelFollowing || this.$t('user_card.following')
|
||||||
} else if (this.user.requested) {
|
} else if (this.relationship.requested) {
|
||||||
return this.$t('user_card.follow_sent')
|
return this.$t('user_card.follow_sent')
|
||||||
} else {
|
} else {
|
||||||
return this.$t('user_card.follow')
|
return this.$t('user_card.follow')
|
||||||
@ -33,20 +33,20 @@ export default {
|
|||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
onClick () {
|
onClick () {
|
||||||
this.user.following ? this.unfollow() : this.follow()
|
this.relationship.following ? this.unfollow() : this.follow()
|
||||||
},
|
},
|
||||||
follow () {
|
follow () {
|
||||||
this.inProgress = true
|
this.inProgress = true
|
||||||
requestFollow(this.user, this.$store).then(() => {
|
requestFollow(this.relationship.id, this.$store).then(() => {
|
||||||
this.inProgress = false
|
this.inProgress = false
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
unfollow () {
|
unfollow () {
|
||||||
const store = this.$store
|
const store = this.$store
|
||||||
this.inProgress = true
|
this.inProgress = true
|
||||||
requestUnfollow(this.user, store).then(() => {
|
requestUnfollow(this.relationship.id, store).then(() => {
|
||||||
this.inProgress = false
|
this.inProgress = false
|
||||||
store.commit('removeStatus', { timeline: 'friends', userId: this.user.id })
|
store.commit('removeStatus', { timeline: 'friends', userId: this.relationship.id })
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -18,6 +18,9 @@ const FollowCard = {
|
|||||||
},
|
},
|
||||||
loggedIn () {
|
loggedIn () {
|
||||||
return this.$store.state.users.currentUser
|
return this.$store.state.users.currentUser
|
||||||
|
},
|
||||||
|
relationship () {
|
||||||
|
return this.$store.getters.relationship(this.user.id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,14 +2,14 @@
|
|||||||
<basic-user-card :user="user">
|
<basic-user-card :user="user">
|
||||||
<div class="follow-card-content-container">
|
<div class="follow-card-content-container">
|
||||||
<span
|
<span
|
||||||
v-if="!noFollowsYou && user.follows_you"
|
v-if="!noFollowsYou && relationship.followed_by"
|
||||||
class="faint"
|
class="faint"
|
||||||
>
|
>
|
||||||
{{ isMe ? $t('user_card.its_you') : $t('user_card.follows_you') }}
|
{{ isMe ? $t('user_card.its_you') : $t('user_card.follows_you') }}
|
||||||
</span>
|
</span>
|
||||||
<template v-if="!loggedIn">
|
<template v-if="!loggedIn">
|
||||||
<div
|
<div
|
||||||
v-if="!user.following"
|
v-if="!relationship.following"
|
||||||
class="follow-card-follow-button"
|
class="follow-card-follow-button"
|
||||||
>
|
>
|
||||||
<RemoteFollow :user="user" />
|
<RemoteFollow :user="user" />
|
||||||
@ -17,9 +17,9 @@
|
|||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<FollowButton
|
<FollowButton
|
||||||
:user="user"
|
:relationship="relationship"
|
||||||
class="follow-card-follow-button"
|
|
||||||
:label-following="$t('user_card.follow_unfollow')"
|
:label-following="$t('user_card.follow_unfollow')"
|
||||||
|
class="follow-card-follow-button"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
@ -11,8 +11,11 @@ const MuteCard = {
|
|||||||
user () {
|
user () {
|
||||||
return this.$store.getters.findUser(this.userId)
|
return this.$store.getters.findUser(this.userId)
|
||||||
},
|
},
|
||||||
|
relationship () {
|
||||||
|
return this.$store.getters.relationship(this.userId)
|
||||||
|
},
|
||||||
muted () {
|
muted () {
|
||||||
return this.user.muted
|
return this.relationship.muting
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
@ -21,13 +24,13 @@ const MuteCard = {
|
|||||||
methods: {
|
methods: {
|
||||||
unmuteUser () {
|
unmuteUser () {
|
||||||
this.progress = true
|
this.progress = true
|
||||||
this.$store.dispatch('unmuteUser', this.user.id).then(() => {
|
this.$store.dispatch('unmuteUser', this.userId).then(() => {
|
||||||
this.progress = false
|
this.progress = false
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
muteUser () {
|
muteUser () {
|
||||||
this.progress = true
|
this.progress = true
|
||||||
this.$store.dispatch('muteUser', this.user.id).then(() => {
|
this.$store.dispatch('muteUser', this.userId).then(() => {
|
||||||
this.progress = false
|
this.progress = false
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -75,7 +75,7 @@ const Notification = {
|
|||||||
return this.generateUserProfileLink(this.targetUser)
|
return this.generateUserProfileLink(this.targetUser)
|
||||||
},
|
},
|
||||||
needMute () {
|
needMute () {
|
||||||
return this.user.muted
|
return this.$store.getters.relationship(this.user.id).muting
|
||||||
},
|
},
|
||||||
isStatusNotification () {
|
isStatusNotification () {
|
||||||
return isStatusNotification(this.notification.type)
|
return isStatusNotification(this.notification.type)
|
||||||
|
@ -40,7 +40,7 @@
|
|||||||
<div class="notification-right">
|
<div class="notification-right">
|
||||||
<UserCard
|
<UserCard
|
||||||
v-if="userExpanded"
|
v-if="userExpanded"
|
||||||
:user="getUser(notification)"
|
:user-id="getUser(notification).id"
|
||||||
:rounded="true"
|
:rounded="true"
|
||||||
:bordered="true"
|
:bordered="true"
|
||||||
/>
|
/>
|
||||||
|
@ -2,7 +2,7 @@ import Popover from '../popover/popover.vue'
|
|||||||
import { mapGetters } from 'vuex'
|
import { mapGetters } from 'vuex'
|
||||||
|
|
||||||
const ReactButton = {
|
const ReactButton = {
|
||||||
props: ['status', 'loggedIn'],
|
props: ['status'],
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
filterWord: ''
|
filterWord: ''
|
||||||
|
@ -37,7 +37,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<i
|
<i
|
||||||
v-if="loggedIn"
|
|
||||||
slot="trigger"
|
slot="trigger"
|
||||||
class="icon-smile button-icon add-reaction-button"
|
class="icon-smile button-icon add-reaction-button"
|
||||||
:title="$t('tool_tip.add_reaction')"
|
:title="$t('tool_tip.add_reaction')"
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { validationMixin } from 'vuelidate'
|
import { validationMixin } from 'vuelidate'
|
||||||
import { required, sameAs } from 'vuelidate/lib/validators'
|
import { required, requiredIf, sameAs } from 'vuelidate/lib/validators'
|
||||||
import { mapActions, mapState } from 'vuex'
|
import { mapActions, mapState } from 'vuex'
|
||||||
|
|
||||||
const registration = {
|
const registration = {
|
||||||
@ -14,9 +14,10 @@ const registration = {
|
|||||||
},
|
},
|
||||||
captcha: {}
|
captcha: {}
|
||||||
}),
|
}),
|
||||||
validations: {
|
validations () {
|
||||||
|
return {
|
||||||
user: {
|
user: {
|
||||||
email: { required },
|
email: { required: requiredIf(() => this.accountActivationRequired) },
|
||||||
username: { required },
|
username: { required },
|
||||||
fullname: { required },
|
fullname: { required },
|
||||||
password: { required },
|
password: { required },
|
||||||
@ -25,6 +26,7 @@ const registration = {
|
|||||||
sameAsPassword: sameAs('password')
|
sameAsPassword: sameAs('password')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
created () {
|
created () {
|
||||||
if ((!this.registrationOpen && !this.token) || this.signedIn) {
|
if ((!this.registrationOpen && !this.token) || this.signedIn) {
|
||||||
@ -43,7 +45,8 @@ const registration = {
|
|||||||
signedIn: (state) => !!state.users.currentUser,
|
signedIn: (state) => !!state.users.currentUser,
|
||||||
isPending: (state) => state.users.signUpPending,
|
isPending: (state) => state.users.signUpPending,
|
||||||
serverValidationErrors: (state) => state.users.signUpErrors,
|
serverValidationErrors: (state) => state.users.signUpErrors,
|
||||||
termsOfService: (state) => state.instance.tos
|
termsOfService: (state) => state.instance.tos,
|
||||||
|
accountActivationRequired: (state) => state.instance.accountActivationRequired
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
@ -19,7 +19,7 @@
|
|||||||
>
|
>
|
||||||
<UserCard
|
<UserCard
|
||||||
v-if="currentUser"
|
v-if="currentUser"
|
||||||
:user="currentUser"
|
:user-id="currentUser.id"
|
||||||
:hide-bio="true"
|
:hide-bio="true"
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
|
@ -1,23 +1,17 @@
|
|||||||
import Attachment from '../attachment/attachment.vue'
|
|
||||||
import FavoriteButton from '../favorite_button/favorite_button.vue'
|
import FavoriteButton from '../favorite_button/favorite_button.vue'
|
||||||
import ReactButton from '../react_button/react_button.vue'
|
import ReactButton from '../react_button/react_button.vue'
|
||||||
import RetweetButton from '../retweet_button/retweet_button.vue'
|
import RetweetButton from '../retweet_button/retweet_button.vue'
|
||||||
import Poll from '../poll/poll.vue'
|
|
||||||
import ExtraButtons from '../extra_buttons/extra_buttons.vue'
|
import ExtraButtons from '../extra_buttons/extra_buttons.vue'
|
||||||
import PostStatusForm from '../post_status_form/post_status_form.vue'
|
import PostStatusForm from '../post_status_form/post_status_form.vue'
|
||||||
import UserCard from '../user_card/user_card.vue'
|
import UserCard from '../user_card/user_card.vue'
|
||||||
import UserAvatar from '../user_avatar/user_avatar.vue'
|
import UserAvatar from '../user_avatar/user_avatar.vue'
|
||||||
import Gallery from '../gallery/gallery.vue'
|
|
||||||
import LinkPreview from '../link-preview/link-preview.vue'
|
|
||||||
import AvatarList from '../avatar_list/avatar_list.vue'
|
import AvatarList from '../avatar_list/avatar_list.vue'
|
||||||
import Timeago from '../timeago/timeago.vue'
|
import Timeago from '../timeago/timeago.vue'
|
||||||
|
import StatusContent from '../status_content/status_content.vue'
|
||||||
import StatusPopover from '../status_popover/status_popover.vue'
|
import StatusPopover from '../status_popover/status_popover.vue'
|
||||||
import EmojiReactions from '../emoji_reactions/emoji_reactions.vue'
|
import EmojiReactions from '../emoji_reactions/emoji_reactions.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 { processHtml } from 'src/services/tiny_post_html_processor/tiny_post_html_processor.service.js'
|
|
||||||
import { highlightClass, highlightStyle } from '../../services/user_highlighter/user_highlighter.js'
|
import { highlightClass, highlightStyle } from '../../services/user_highlighter/user_highlighter.js'
|
||||||
import { mentionMatchesUrl, extractTagFromUrl } from 'src/services/matcher/matcher.service.js'
|
|
||||||
import { filter, unescape, uniqBy } from 'lodash'
|
import { filter, unescape, uniqBy } from 'lodash'
|
||||||
import { mapGetters, mapState } from 'vuex'
|
import { mapGetters, mapState } from 'vuex'
|
||||||
|
|
||||||
@ -43,17 +37,10 @@ const Status = {
|
|||||||
replying: false,
|
replying: false,
|
||||||
unmuted: false,
|
unmuted: false,
|
||||||
userExpanded: false,
|
userExpanded: false,
|
||||||
showingTall: this.inConversation && this.focused,
|
error: null
|
||||||
showingLongSubject: false,
|
|
||||||
error: null,
|
|
||||||
// not as computed because it sets the initial state which will be changed later
|
|
||||||
expandingSubject: !this.$store.getters.mergedConfig.collapseMessageWithSubject
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
localCollapseSubjectDefault () {
|
|
||||||
return this.mergedConfig.collapseMessageWithSubject
|
|
||||||
},
|
|
||||||
muteWords () {
|
muteWords () {
|
||||||
return this.mergedConfig.muteWords
|
return this.mergedConfig.muteWords
|
||||||
},
|
},
|
||||||
@ -79,10 +66,6 @@ const Status = {
|
|||||||
const highlight = this.mergedConfig.highlight
|
const highlight = this.mergedConfig.highlight
|
||||||
return highlightStyle(highlight[user.screen_name])
|
return highlightStyle(highlight[user.screen_name])
|
||||||
},
|
},
|
||||||
hideAttachments () {
|
|
||||||
return (this.mergedConfig.hideAttachments && !this.inConversation) ||
|
|
||||||
(this.mergedConfig.hideAttachmentsInConv && this.inConversation)
|
|
||||||
},
|
|
||||||
userProfileLink () {
|
userProfileLink () {
|
||||||
return this.generateUserProfileLink(this.status.user.id, this.status.user.screen_name)
|
return this.generateUserProfileLink(this.status.user.id, this.status.user.screen_name)
|
||||||
},
|
},
|
||||||
@ -118,7 +101,13 @@ const Status = {
|
|||||||
|
|
||||||
return hits
|
return hits
|
||||||
},
|
},
|
||||||
muted () { return !this.unmuted && ((!(this.inProfile && this.status.user.id === this.profileUserId) && this.status.user.muted) || (!this.inConversation && this.status.thread_muted) || this.muteWordHits.length > 0) },
|
muted () {
|
||||||
|
const relationship = this.$store.getters.relationship(this.status.user.id)
|
||||||
|
return !this.unmuted && (
|
||||||
|
(!(this.inProfile && this.status.user.id === this.profileUserId) && relationship.muting) ||
|
||||||
|
(!this.inConversation && this.status.thread_muted) ||
|
||||||
|
this.muteWordHits.length > 0)
|
||||||
|
},
|
||||||
hideFilteredStatuses () {
|
hideFilteredStatuses () {
|
||||||
return this.mergedConfig.hideFilteredStatuses
|
return this.mergedConfig.hideFilteredStatuses
|
||||||
},
|
},
|
||||||
@ -135,20 +124,6 @@ const Status = {
|
|||||||
// use conversation highlight only when in conversation
|
// use conversation highlight only when in conversation
|
||||||
return this.status.id === this.highlight
|
return this.status.id === this.highlight
|
||||||
},
|
},
|
||||||
// This is a bit hacky, but we want to approximate post height before rendering
|
|
||||||
// so we count newlines (masto uses <p> for paragraphs, GS uses <br> between them)
|
|
||||||
// as well as approximate line count by counting characters and approximating ~80
|
|
||||||
// per line.
|
|
||||||
//
|
|
||||||
// Using max-height + overflow: auto for status components resulted in false positives
|
|
||||||
// very often with japanese characters, and it was very annoying.
|
|
||||||
tallStatus () {
|
|
||||||
const lengthScore = this.status.statusnet_html.split(/<p|<br/).length + this.status.text.length / 80
|
|
||||||
return lengthScore > 20
|
|
||||||
},
|
|
||||||
longSubject () {
|
|
||||||
return this.status.summary.length > 900
|
|
||||||
},
|
|
||||||
isReply () {
|
isReply () {
|
||||||
return !!(this.status.in_reply_to_status_id && this.status.in_reply_to_user_id)
|
return !!(this.status.in_reply_to_status_id && this.status.in_reply_to_user_id)
|
||||||
},
|
},
|
||||||
@ -178,8 +153,11 @@ const Status = {
|
|||||||
if (this.status.user.id === this.status.attentions[i].id) {
|
if (this.status.user.id === this.status.attentions[i].id) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
const taggedUser = this.$store.getters.findUser(this.status.attentions[i].id)
|
// There's zero guarantee of this working. If we happen to have that user and their
|
||||||
if (checkFollowing && taggedUser && taggedUser.following) {
|
// relationship in store then it will work, but there's kinda little chance of having
|
||||||
|
// them for people you're not following.
|
||||||
|
const relationship = this.$store.state.users.relationships[this.status.attentions[i].id]
|
||||||
|
if (checkFollowing && relationship && relationship.following) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
if (this.status.attentions[i].id === this.currentUser.id) {
|
if (this.status.attentions[i].id === this.currentUser.id) {
|
||||||
@ -188,32 +166,6 @@ const Status = {
|
|||||||
}
|
}
|
||||||
return this.status.attentions.length > 0
|
return this.status.attentions.length > 0
|
||||||
},
|
},
|
||||||
|
|
||||||
// When a status has a subject and is also tall, we should only have one show more/less button. If the default is to collapse statuses with subjects, we just treat it like a status with a subject; otherwise, we just treat it like a tall status.
|
|
||||||
mightHideBecauseSubject () {
|
|
||||||
return this.status.summary && (!this.tallStatus || this.localCollapseSubjectDefault)
|
|
||||||
},
|
|
||||||
mightHideBecauseTall () {
|
|
||||||
return this.tallStatus && (!this.status.summary || !this.localCollapseSubjectDefault)
|
|
||||||
},
|
|
||||||
hideSubjectStatus () {
|
|
||||||
return this.mightHideBecauseSubject && !this.expandingSubject
|
|
||||||
},
|
|
||||||
hideTallStatus () {
|
|
||||||
return this.mightHideBecauseTall && !this.showingTall
|
|
||||||
},
|
|
||||||
showingMore () {
|
|
||||||
return (this.mightHideBecauseTall && this.showingTall) || (this.mightHideBecauseSubject && this.expandingSubject)
|
|
||||||
},
|
|
||||||
nsfwClickthrough () {
|
|
||||||
if (!this.status.nsfw) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if (this.status.summary && this.localCollapseSubjectDefault) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
},
|
|
||||||
replySubject () {
|
replySubject () {
|
||||||
if (!this.status.summary) return ''
|
if (!this.status.summary) return ''
|
||||||
const decodedSummary = unescape(this.status.summary)
|
const decodedSummary = unescape(this.status.summary)
|
||||||
@ -227,83 +179,6 @@ const Status = {
|
|||||||
return ''
|
return ''
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
attachmentSize () {
|
|
||||||
if ((this.mergedConfig.hideAttachments && !this.inConversation) ||
|
|
||||||
(this.mergedConfig.hideAttachmentsInConv && this.inConversation) ||
|
|
||||||
(this.status.attachments.length > this.maxThumbnails)) {
|
|
||||||
return 'hide'
|
|
||||||
} else if (this.compact) {
|
|
||||||
return 'small'
|
|
||||||
}
|
|
||||||
return 'normal'
|
|
||||||
},
|
|
||||||
galleryTypes () {
|
|
||||||
if (this.attachmentSize === 'hide') {
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
return this.mergedConfig.playVideosInModal
|
|
||||||
? ['image', 'video']
|
|
||||||
: ['image']
|
|
||||||
},
|
|
||||||
galleryAttachments () {
|
|
||||||
return this.status.attachments.filter(
|
|
||||||
file => fileType.fileMatchesSomeType(this.galleryTypes, file)
|
|
||||||
)
|
|
||||||
},
|
|
||||||
nonGalleryAttachments () {
|
|
||||||
return this.status.attachments.filter(
|
|
||||||
file => !fileType.fileMatchesSomeType(this.galleryTypes, file)
|
|
||||||
)
|
|
||||||
},
|
|
||||||
hasImageAttachments () {
|
|
||||||
return this.status.attachments.some(
|
|
||||||
file => fileType.fileType(file.mimetype) === 'image'
|
|
||||||
)
|
|
||||||
},
|
|
||||||
hasVideoAttachments () {
|
|
||||||
return this.status.attachments.some(
|
|
||||||
file => fileType.fileType(file.mimetype) === 'video'
|
|
||||||
)
|
|
||||||
},
|
|
||||||
maxThumbnails () {
|
|
||||||
return this.mergedConfig.maxThumbnails
|
|
||||||
},
|
|
||||||
postBodyHtml () {
|
|
||||||
const html = this.status.statusnet_html
|
|
||||||
|
|
||||||
if (this.mergedConfig.greentext) {
|
|
||||||
try {
|
|
||||||
if (html.includes('>')) {
|
|
||||||
// This checks if post has '>' at the beginning, excluding mentions so that @mention >impying works
|
|
||||||
return processHtml(html, (string) => {
|
|
||||||
if (string.includes('>') &&
|
|
||||||
string
|
|
||||||
.replace(/<[^>]+?>/gi, '') // remove all tags
|
|
||||||
.replace(/@\w+/gi, '') // remove mentions (even failed ones)
|
|
||||||
.trim()
|
|
||||||
.startsWith('>')) {
|
|
||||||
return `<span class='greentext'>${string}</span>`
|
|
||||||
} else {
|
|
||||||
return string
|
|
||||||
}
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
return html
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.err('Failed to process status html', e)
|
|
||||||
return html
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return html
|
|
||||||
}
|
|
||||||
},
|
|
||||||
contentHtml () {
|
|
||||||
if (!this.status.summary_html) {
|
|
||||||
return this.postBodyHtml
|
|
||||||
}
|
|
||||||
return this.status.summary_html + '<br />' + this.postBodyHtml
|
|
||||||
},
|
|
||||||
combinedFavsAndRepeatsUsers () {
|
combinedFavsAndRepeatsUsers () {
|
||||||
// Use the status from the global status repository since favs and repeats are saved in it
|
// Use the status from the global status repository since favs and repeats are saved in it
|
||||||
const combinedUsers = [].concat(
|
const combinedUsers = [].concat(
|
||||||
@ -312,9 +187,6 @@ const Status = {
|
|||||||
)
|
)
|
||||||
return uniqBy(combinedUsers, 'id')
|
return uniqBy(combinedUsers, 'id')
|
||||||
},
|
},
|
||||||
ownStatus () {
|
|
||||||
return this.status.user.id === this.currentUser.id
|
|
||||||
},
|
|
||||||
tags () {
|
tags () {
|
||||||
return this.status.tags.filter(tagObj => tagObj.hasOwnProperty('name')).map(tagObj => tagObj.name).join(' ')
|
return this.status.tags.filter(tagObj => tagObj.hasOwnProperty('name')).map(tagObj => tagObj.name).join(' ')
|
||||||
},
|
},
|
||||||
@ -328,21 +200,18 @@ const Status = {
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
Attachment,
|
|
||||||
FavoriteButton,
|
FavoriteButton,
|
||||||
ReactButton,
|
ReactButton,
|
||||||
RetweetButton,
|
RetweetButton,
|
||||||
ExtraButtons,
|
ExtraButtons,
|
||||||
PostStatusForm,
|
PostStatusForm,
|
||||||
Poll,
|
|
||||||
UserCard,
|
UserCard,
|
||||||
UserAvatar,
|
UserAvatar,
|
||||||
Gallery,
|
|
||||||
LinkPreview,
|
|
||||||
AvatarList,
|
AvatarList,
|
||||||
Timeago,
|
Timeago,
|
||||||
StatusPopover,
|
StatusPopover,
|
||||||
EmojiReactions
|
EmojiReactions,
|
||||||
|
StatusContent
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
visibilityIcon (visibility) {
|
visibilityIcon (visibility) {
|
||||||
@ -363,32 +232,6 @@ const Status = {
|
|||||||
clearError () {
|
clearError () {
|
||||||
this.error = undefined
|
this.error = undefined
|
||||||
},
|
},
|
||||||
linkClicked (event) {
|
|
||||||
const target = event.target.closest('.status-content a')
|
|
||||||
if (target) {
|
|
||||||
if (target.className.match(/mention/)) {
|
|
||||||
const href = target.href
|
|
||||||
const attn = this.status.attentions.find(attn => mentionMatchesUrl(attn, href))
|
|
||||||
if (attn) {
|
|
||||||
event.stopPropagation()
|
|
||||||
event.preventDefault()
|
|
||||||
const link = this.generateUserProfileLink(attn.id, attn.screen_name)
|
|
||||||
this.$router.push(link)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (target.rel.match(/(?:^|\s)tag(?:$|\s)/) || 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')
|
|
||||||
}
|
|
||||||
},
|
|
||||||
toggleReplying () {
|
toggleReplying () {
|
||||||
this.replying = !this.replying
|
this.replying = !this.replying
|
||||||
},
|
},
|
||||||
@ -406,22 +249,8 @@ const Status = {
|
|||||||
toggleUserExpanded () {
|
toggleUserExpanded () {
|
||||||
this.userExpanded = !this.userExpanded
|
this.userExpanded = !this.userExpanded
|
||||||
},
|
},
|
||||||
toggleShowMore () {
|
|
||||||
if (this.mightHideBecauseTall) {
|
|
||||||
this.showingTall = !this.showingTall
|
|
||||||
} else if (this.mightHideBecauseSubject) {
|
|
||||||
this.expandingSubject = !this.expandingSubject
|
|
||||||
}
|
|
||||||
},
|
|
||||||
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 () {
|
|
||||||
const attachments = this.attachmentSize === 'hide' ? this.status.attachments : this.galleryAttachments
|
|
||||||
return () => this.$store.dispatch('setMedia', attachments)
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
|
@ -94,7 +94,7 @@
|
|||||||
<div class="status-body">
|
<div class="status-body">
|
||||||
<UserCard
|
<UserCard
|
||||||
v-if="userExpanded"
|
v-if="userExpanded"
|
||||||
:user="status.user"
|
:user-id="status.user.id"
|
||||||
:rounded="true"
|
:rounded="true"
|
||||||
:bordered="true"
|
:bordered="true"
|
||||||
class="status-usercard"
|
class="status-usercard"
|
||||||
@ -226,118 +226,12 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<StatusContent
|
||||||
v-if="longSubject"
|
:status="status"
|
||||||
class="status-content-wrapper"
|
:no-heading="noHeading"
|
||||||
:class="{ 'tall-status': !showingLongSubject }"
|
:highlight="highlight"
|
||||||
>
|
:focused="isFocused"
|
||||||
<a
|
|
||||||
v-if="!showingLongSubject"
|
|
||||||
class="tall-status-hider"
|
|
||||||
:class="{ 'tall-status-hider_focused': isFocused }"
|
|
||||||
href="#"
|
|
||||||
@click.prevent="showingLongSubject=true"
|
|
||||||
>{{ $t("general.show_more") }}</a>
|
|
||||||
<div
|
|
||||||
class="status-content media-body"
|
|
||||||
@click.prevent="linkClicked"
|
|
||||||
v-html="contentHtml"
|
|
||||||
/>
|
/>
|
||||||
<a
|
|
||||||
v-if="showingLongSubject"
|
|
||||||
href="#"
|
|
||||||
class="status-unhider"
|
|
||||||
@click.prevent="showingLongSubject=false"
|
|
||||||
>{{ $t("general.show_less") }}</a>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
v-else
|
|
||||||
:class="{'tall-status': hideTallStatus}"
|
|
||||||
class="status-content-wrapper"
|
|
||||||
>
|
|
||||||
<a
|
|
||||||
v-if="hideTallStatus"
|
|
||||||
class="tall-status-hider"
|
|
||||||
:class="{ 'tall-status-hider_focused': isFocused }"
|
|
||||||
href="#"
|
|
||||||
@click.prevent="toggleShowMore"
|
|
||||||
>{{ $t("general.show_more") }}</a>
|
|
||||||
<div
|
|
||||||
v-if="!hideSubjectStatus"
|
|
||||||
class="status-content media-body"
|
|
||||||
@click.prevent="linkClicked"
|
|
||||||
v-html="contentHtml"
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
v-else
|
|
||||||
class="status-content media-body"
|
|
||||||
@click.prevent="linkClicked"
|
|
||||||
v-html="status.summary_html"
|
|
||||||
/>
|
|
||||||
<a
|
|
||||||
v-if="hideSubjectStatus"
|
|
||||||
href="#"
|
|
||||||
class="cw-status-hider"
|
|
||||||
@click.prevent="toggleShowMore"
|
|
||||||
>
|
|
||||||
{{ $t("general.show_more") }}
|
|
||||||
<span
|
|
||||||
v-if="hasImageAttachments"
|
|
||||||
class="icon-picture"
|
|
||||||
/>
|
|
||||||
<span
|
|
||||||
v-if="hasVideoAttachments"
|
|
||||||
class="icon-video"
|
|
||||||
/>
|
|
||||||
<span
|
|
||||||
v-if="status.card"
|
|
||||||
class="icon-link"
|
|
||||||
/>
|
|
||||||
</a>
|
|
||||||
<a
|
|
||||||
v-if="showingMore"
|
|
||||||
href="#"
|
|
||||||
class="status-unhider"
|
|
||||||
@click.prevent="toggleShowMore"
|
|
||||||
>{{ $t("general.show_less") }}</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-if="status.poll && status.poll.options">
|
|
||||||
<poll :base-poll="status.poll" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
v-if="status.attachments && (!hideSubjectStatus || showingLongSubject)"
|
|
||||||
class="attachments media-body"
|
|
||||||
>
|
|
||||||
<attachment
|
|
||||||
v-for="attachment in nonGalleryAttachments"
|
|
||||||
:key="attachment.id"
|
|
||||||
class="non-gallery"
|
|
||||||
:size="attachmentSize"
|
|
||||||
:nsfw="nsfwClickthrough"
|
|
||||||
:attachment="attachment"
|
|
||||||
:allow-play="true"
|
|
||||||
:set-media="setMedia()"
|
|
||||||
/>
|
|
||||||
<gallery
|
|
||||||
v-if="galleryAttachments.length > 0"
|
|
||||||
:nsfw="nsfwClickthrough"
|
|
||||||
:attachments="galleryAttachments"
|
|
||||||
:set-media="setMedia()"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
v-if="status.card && !hideSubjectStatus && !noHeading"
|
|
||||||
class="link-preview media-body"
|
|
||||||
>
|
|
||||||
<link-preview
|
|
||||||
:card="status.card"
|
|
||||||
:size="attachmentSize"
|
|
||||||
:nsfw="nsfwClickthrough"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<transition name="fade">
|
<transition name="fade">
|
||||||
<div
|
<div
|
||||||
@ -404,7 +298,7 @@
|
|||||||
:status="status"
|
:status="status"
|
||||||
/>
|
/>
|
||||||
<ReactButton
|
<ReactButton
|
||||||
:logged-in="loggedIn"
|
v-if="loggedIn"
|
||||||
:status="status"
|
:status="status"
|
||||||
/>
|
/>
|
||||||
<extra-buttons
|
<extra-buttons
|
||||||
@ -630,105 +524,6 @@ $status-margin: 0.75em;
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.tall-status {
|
|
||||||
position: relative;
|
|
||||||
height: 220px;
|
|
||||||
overflow-x: hidden;
|
|
||||||
overflow-y: hidden;
|
|
||||||
z-index: 1;
|
|
||||||
.status-content {
|
|
||||||
height: 100%;
|
|
||||||
mask: linear-gradient(to top, white, transparent) bottom/100% 70px no-repeat,
|
|
||||||
linear-gradient(to top, white, white);
|
|
||||||
/* Autoprefixed seem to ignore this one, and also syntax is different */
|
|
||||||
-webkit-mask-composite: xor;
|
|
||||||
mask-composite: exclude;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.tall-status-hider {
|
|
||||||
display: inline-block;
|
|
||||||
word-break: break-all;
|
|
||||||
position: absolute;
|
|
||||||
height: 70px;
|
|
||||||
margin-top: 150px;
|
|
||||||
width: 100%;
|
|
||||||
text-align: center;
|
|
||||||
line-height: 110px;
|
|
||||||
z-index: 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-unhider, .cw-status-hider {
|
|
||||||
width: 100%;
|
|
||||||
text-align: center;
|
|
||||||
display: inline-block;
|
|
||||||
word-break: break-all;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-content {
|
|
||||||
font-family: var(--postFont, sans-serif);
|
|
||||||
line-height: 1.4em;
|
|
||||||
white-space: pre-wrap;
|
|
||||||
|
|
||||||
a {
|
|
||||||
color: $fallback--link;
|
|
||||||
color: var(--postLink, $fallback--link);
|
|
||||||
}
|
|
||||||
|
|
||||||
img, video {
|
|
||||||
max-width: 100%;
|
|
||||||
max-height: 400px;
|
|
||||||
vertical-align: middle;
|
|
||||||
object-fit: contain;
|
|
||||||
|
|
||||||
&.emoji {
|
|
||||||
width: 32px;
|
|
||||||
height: 32px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
blockquote {
|
|
||||||
margin: 0.2em 0 0.2em 2em;
|
|
||||||
font-style: italic;
|
|
||||||
}
|
|
||||||
|
|
||||||
pre {
|
|
||||||
overflow: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
code, samp, kbd, var, pre {
|
|
||||||
font-family: var(--postCodeFont, monospace);
|
|
||||||
}
|
|
||||||
|
|
||||||
p {
|
|
||||||
margin: 0 0 1em 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
p:last-child {
|
|
||||||
margin: 0 0 0 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
font-size: 1.1em;
|
|
||||||
line-height: 1.2em;
|
|
||||||
margin: 1.4em 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
h2 {
|
|
||||||
font-size: 1.1em;
|
|
||||||
margin: 1.0em 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
h3 {
|
|
||||||
font-size: 1em;
|
|
||||||
margin: 1.2em 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
h4 {
|
|
||||||
margin: 1.1em 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.retweet-info {
|
.retweet-info {
|
||||||
padding: 0.4em $status-margin;
|
padding: 0.4em $status-margin;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
@ -790,11 +585,6 @@ $status-margin: 0.75em;
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.greentext {
|
|
||||||
color: $fallback--cGreen;
|
|
||||||
color: var(--cGreen, $fallback--cGreen);
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-conversation {
|
.status-conversation {
|
||||||
border-left-style: solid;
|
border-left-style: solid;
|
||||||
}
|
}
|
||||||
@ -866,14 +656,6 @@ a.unmute {
|
|||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.timeline :not(.panel-disabled) > {
|
|
||||||
.status-el:last-child {
|
|
||||||
border-radius: 0 0 $fallback--panelRadius $fallback--panelRadius;
|
|
||||||
border-radius: 0 0 var(--panelRadius, $fallback--panelRadius) var(--panelRadius, $fallback--panelRadius);
|
|
||||||
border-bottom: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.favs-repeated-users {
|
.favs-repeated-users {
|
||||||
margin-top: $status-margin;
|
margin-top: $status-margin;
|
||||||
|
|
||||||
|
210
src/components/status_content/status_content.js
Normal file
210
src/components/status_content/status_content.js
Normal file
@ -0,0 +1,210 @@
|
|||||||
|
import Attachment from '../attachment/attachment.vue'
|
||||||
|
import Poll from '../poll/poll.vue'
|
||||||
|
import Gallery from '../gallery/gallery.vue'
|
||||||
|
import LinkPreview from '../link-preview/link-preview.vue'
|
||||||
|
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
|
||||||
|
import fileType from 'src/services/file_type/file_type.service'
|
||||||
|
import { processHtml } from 'src/services/tiny_post_html_processor/tiny_post_html_processor.service.js'
|
||||||
|
import { mentionMatchesUrl, extractTagFromUrl } from 'src/services/matcher/matcher.service.js'
|
||||||
|
import { mapGetters, mapState } from 'vuex'
|
||||||
|
|
||||||
|
const StatusContent = {
|
||||||
|
name: 'StatusContent',
|
||||||
|
props: [
|
||||||
|
'status',
|
||||||
|
'focused',
|
||||||
|
'noHeading',
|
||||||
|
'fullContent'
|
||||||
|
],
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
showingTall: this.inConversation && this.focused,
|
||||||
|
showingLongSubject: false,
|
||||||
|
// not as computed because it sets the initial state which will be changed later
|
||||||
|
expandingSubject: !this.$store.getters.mergedConfig.collapseMessageWithSubject
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
localCollapseSubjectDefault () {
|
||||||
|
return this.mergedConfig.collapseMessageWithSubject
|
||||||
|
},
|
||||||
|
hideAttachments () {
|
||||||
|
return (this.mergedConfig.hideAttachments && !this.inConversation) ||
|
||||||
|
(this.mergedConfig.hideAttachmentsInConv && this.inConversation)
|
||||||
|
},
|
||||||
|
// This is a bit hacky, but we want to approximate post height before rendering
|
||||||
|
// so we count newlines (masto uses <p> for paragraphs, GS uses <br> between them)
|
||||||
|
// as well as approximate line count by counting characters and approximating ~80
|
||||||
|
// per line.
|
||||||
|
//
|
||||||
|
// Using max-height + overflow: auto for status components resulted in false positives
|
||||||
|
// very often with japanese characters, and it was very annoying.
|
||||||
|
tallStatus () {
|
||||||
|
const lengthScore = this.status.statusnet_html.split(/<p|<br/).length + this.status.text.length / 80
|
||||||
|
return lengthScore > 20
|
||||||
|
},
|
||||||
|
longSubject () {
|
||||||
|
return this.status.summary.length > 900
|
||||||
|
},
|
||||||
|
// When a status has a subject and is also tall, we should only have one show more/less button. If the default is to collapse statuses with subjects, we just treat it like a status with a subject; otherwise, we just treat it like a tall status.
|
||||||
|
mightHideBecauseSubject () {
|
||||||
|
return this.status.summary && (!this.tallStatus || this.localCollapseSubjectDefault)
|
||||||
|
},
|
||||||
|
mightHideBecauseTall () {
|
||||||
|
return this.tallStatus && (!this.status.summary || !this.localCollapseSubjectDefault)
|
||||||
|
},
|
||||||
|
hideSubjectStatus () {
|
||||||
|
return this.mightHideBecauseSubject && !this.expandingSubject
|
||||||
|
},
|
||||||
|
hideTallStatus () {
|
||||||
|
return this.mightHideBecauseTall && !this.showingTall
|
||||||
|
},
|
||||||
|
showingMore () {
|
||||||
|
return (this.mightHideBecauseTall && this.showingTall) || (this.mightHideBecauseSubject && this.expandingSubject)
|
||||||
|
},
|
||||||
|
nsfwClickthrough () {
|
||||||
|
if (!this.status.nsfw) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (this.status.summary && this.localCollapseSubjectDefault) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
attachmentSize () {
|
||||||
|
if ((this.mergedConfig.hideAttachments && !this.inConversation) ||
|
||||||
|
(this.mergedConfig.hideAttachmentsInConv && this.inConversation) ||
|
||||||
|
(this.status.attachments.length > this.maxThumbnails)) {
|
||||||
|
return 'hide'
|
||||||
|
} else if (this.compact) {
|
||||||
|
return 'small'
|
||||||
|
}
|
||||||
|
return 'normal'
|
||||||
|
},
|
||||||
|
galleryTypes () {
|
||||||
|
if (this.attachmentSize === 'hide') {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
return this.mergedConfig.playVideosInModal
|
||||||
|
? ['image', 'video']
|
||||||
|
: ['image']
|
||||||
|
},
|
||||||
|
galleryAttachments () {
|
||||||
|
return this.status.attachments.filter(
|
||||||
|
file => fileType.fileMatchesSomeType(this.galleryTypes, file)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
nonGalleryAttachments () {
|
||||||
|
return this.status.attachments.filter(
|
||||||
|
file => !fileType.fileMatchesSomeType(this.galleryTypes, file)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
hasImageAttachments () {
|
||||||
|
return this.status.attachments.some(
|
||||||
|
file => fileType.fileType(file.mimetype) === 'image'
|
||||||
|
)
|
||||||
|
},
|
||||||
|
hasVideoAttachments () {
|
||||||
|
return this.status.attachments.some(
|
||||||
|
file => fileType.fileType(file.mimetype) === 'video'
|
||||||
|
)
|
||||||
|
},
|
||||||
|
maxThumbnails () {
|
||||||
|
return this.mergedConfig.maxThumbnails
|
||||||
|
},
|
||||||
|
postBodyHtml () {
|
||||||
|
const html = this.status.statusnet_html
|
||||||
|
|
||||||
|
if (this.mergedConfig.greentext) {
|
||||||
|
try {
|
||||||
|
if (html.includes('>')) {
|
||||||
|
// This checks if post has '>' at the beginning, excluding mentions so that @mention >impying works
|
||||||
|
return processHtml(html, (string) => {
|
||||||
|
if (string.includes('>') &&
|
||||||
|
string
|
||||||
|
.replace(/<[^>]+?>/gi, '') // remove all tags
|
||||||
|
.replace(/@\w+/gi, '') // remove mentions (even failed ones)
|
||||||
|
.trim()
|
||||||
|
.startsWith('>')) {
|
||||||
|
return `<span class='greentext'>${string}</span>`
|
||||||
|
} else {
|
||||||
|
return string
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
return html
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.err('Failed to process status html', e)
|
||||||
|
return html
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return html
|
||||||
|
}
|
||||||
|
},
|
||||||
|
contentHtml () {
|
||||||
|
if (!this.status.summary_html) {
|
||||||
|
return this.postBodyHtml
|
||||||
|
}
|
||||||
|
return this.status.summary_html + '<br />' + this.postBodyHtml
|
||||||
|
},
|
||||||
|
...mapGetters(['mergedConfig']),
|
||||||
|
...mapState({
|
||||||
|
betterShadow: state => state.interface.browserSupport.cssFilter,
|
||||||
|
currentUser: state => state.users.currentUser
|
||||||
|
})
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
Attachment,
|
||||||
|
Poll,
|
||||||
|
Gallery,
|
||||||
|
LinkPreview
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
linkClicked (event) {
|
||||||
|
const target = event.target.closest('.status-content a')
|
||||||
|
if (target) {
|
||||||
|
if (target.className.match(/mention/)) {
|
||||||
|
const href = target.href
|
||||||
|
const attn = this.status.attentions.find(attn => mentionMatchesUrl(attn, href))
|
||||||
|
if (attn) {
|
||||||
|
event.stopPropagation()
|
||||||
|
event.preventDefault()
|
||||||
|
const link = this.generateUserProfileLink(attn.id, attn.screen_name)
|
||||||
|
this.$router.push(link)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (target.rel.match(/(?:^|\s)tag(?:$|\s)/) || 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')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
toggleShowMore () {
|
||||||
|
if (this.mightHideBecauseTall) {
|
||||||
|
this.showingTall = !this.showingTall
|
||||||
|
} else if (this.mightHideBecauseSubject) {
|
||||||
|
this.expandingSubject = !this.expandingSubject
|
||||||
|
}
|
||||||
|
},
|
||||||
|
generateUserProfileLink (id, name) {
|
||||||
|
return generateProfileLink(id, name, this.$store.state.instance.restrictedNicknames)
|
||||||
|
},
|
||||||
|
generateTagLink (tag) {
|
||||||
|
return `/tag/${tag}`
|
||||||
|
},
|
||||||
|
setMedia () {
|
||||||
|
const attachments = this.attachmentSize === 'hide' ? this.status.attachments : this.galleryAttachments
|
||||||
|
return () => this.$store.dispatch('setMedia', attachments)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default StatusContent
|
240
src/components/status_content/status_content.vue
Normal file
240
src/components/status_content/status_content.vue
Normal file
@ -0,0 +1,240 @@
|
|||||||
|
<template>
|
||||||
|
<!-- eslint-disable vue/no-v-html -->
|
||||||
|
<div class="status-body">
|
||||||
|
<slot name="header" />
|
||||||
|
<div
|
||||||
|
v-if="longSubject"
|
||||||
|
class="status-content-wrapper"
|
||||||
|
:class="{ 'tall-status': !showingLongSubject }"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
v-if="!showingLongSubject"
|
||||||
|
class="tall-status-hider"
|
||||||
|
:class="{ 'tall-status-hider_focused': focused }"
|
||||||
|
href="#"
|
||||||
|
@click.prevent="showingLongSubject=true"
|
||||||
|
>
|
||||||
|
{{ $t("general.show_more") }}
|
||||||
|
<span
|
||||||
|
v-if="hasImageAttachments"
|
||||||
|
class="icon-picture"
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
v-if="hasVideoAttachments"
|
||||||
|
class="icon-video"
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
v-if="status.card"
|
||||||
|
class="icon-link"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
<div
|
||||||
|
class="status-content media-body"
|
||||||
|
@click.prevent="linkClicked"
|
||||||
|
v-html="contentHtml"
|
||||||
|
/>
|
||||||
|
<a
|
||||||
|
v-if="showingLongSubject"
|
||||||
|
href="#"
|
||||||
|
class="status-unhider"
|
||||||
|
@click.prevent="showingLongSubject=false"
|
||||||
|
>{{ $t("general.show_less") }}</a>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-else
|
||||||
|
:class="{'tall-status': hideTallStatus}"
|
||||||
|
class="status-content-wrapper"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
v-if="hideTallStatus"
|
||||||
|
class="tall-status-hider"
|
||||||
|
:class="{ 'tall-status-hider_focused': focused }"
|
||||||
|
href="#"
|
||||||
|
@click.prevent="toggleShowMore"
|
||||||
|
>{{ $t("general.show_more") }}</a>
|
||||||
|
<div
|
||||||
|
v-if="!hideSubjectStatus"
|
||||||
|
class="status-content media-body"
|
||||||
|
@click.prevent="linkClicked"
|
||||||
|
v-html="contentHtml"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
v-else
|
||||||
|
class="status-content media-body"
|
||||||
|
@click.prevent="linkClicked"
|
||||||
|
v-html="status.summary_html"
|
||||||
|
/>
|
||||||
|
<a
|
||||||
|
v-if="hideSubjectStatus"
|
||||||
|
href="#"
|
||||||
|
class="cw-status-hider"
|
||||||
|
@click.prevent="toggleShowMore"
|
||||||
|
>{{ $t("general.show_more") }}</a>
|
||||||
|
<a
|
||||||
|
v-if="showingMore"
|
||||||
|
href="#"
|
||||||
|
class="status-unhider"
|
||||||
|
@click.prevent="toggleShowMore"
|
||||||
|
>{{ $t("general.show_less") }}</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="status.poll && status.poll.options">
|
||||||
|
<poll :base-poll="status.poll" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="status.attachments.length !== 0 && (!hideSubjectStatus || showingLongSubject)"
|
||||||
|
class="attachments media-body"
|
||||||
|
>
|
||||||
|
<attachment
|
||||||
|
v-for="attachment in nonGalleryAttachments"
|
||||||
|
:key="attachment.id"
|
||||||
|
class="non-gallery"
|
||||||
|
:size="attachmentSize"
|
||||||
|
:nsfw="nsfwClickthrough"
|
||||||
|
:attachment="attachment"
|
||||||
|
:allow-play="true"
|
||||||
|
:set-media="setMedia()"
|
||||||
|
/>
|
||||||
|
<gallery
|
||||||
|
v-if="galleryAttachments.length > 0"
|
||||||
|
:nsfw="nsfwClickthrough"
|
||||||
|
:attachments="galleryAttachments"
|
||||||
|
:set-media="setMedia()"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="status.card && !hideSubjectStatus && !noHeading"
|
||||||
|
class="link-preview media-body"
|
||||||
|
>
|
||||||
|
<link-preview
|
||||||
|
:card="status.card"
|
||||||
|
:size="attachmentSize"
|
||||||
|
:nsfw="nsfwClickthrough"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<slot name="footer" />
|
||||||
|
</div>
|
||||||
|
<!-- eslint-enable vue/no-v-html -->
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script src="./status_content.js" ></script>
|
||||||
|
<style lang="scss">
|
||||||
|
@import '../../_variables.scss';
|
||||||
|
|
||||||
|
$status-margin: 0.75em;
|
||||||
|
|
||||||
|
.status-body {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
|
||||||
|
.tall-status {
|
||||||
|
position: relative;
|
||||||
|
height: 220px;
|
||||||
|
overflow-x: hidden;
|
||||||
|
overflow-y: hidden;
|
||||||
|
z-index: 1;
|
||||||
|
.status-content {
|
||||||
|
height: 100%;
|
||||||
|
mask: linear-gradient(to top, white, transparent) bottom/100% 70px no-repeat,
|
||||||
|
linear-gradient(to top, white, white);
|
||||||
|
/* Autoprefixed seem to ignore this one, and also syntax is different */
|
||||||
|
-webkit-mask-composite: xor;
|
||||||
|
mask-composite: exclude;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tall-status-hider {
|
||||||
|
display: inline-block;
|
||||||
|
word-break: break-all;
|
||||||
|
position: absolute;
|
||||||
|
height: 70px;
|
||||||
|
margin-top: 150px;
|
||||||
|
width: 100%;
|
||||||
|
text-align: center;
|
||||||
|
line-height: 110px;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-unhider, .cw-status-hider {
|
||||||
|
width: 100%;
|
||||||
|
text-align: center;
|
||||||
|
display: inline-block;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-content {
|
||||||
|
font-family: var(--postFont, sans-serif);
|
||||||
|
line-height: 1.4em;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
|
||||||
|
img, video {
|
||||||
|
max-width: 100%;
|
||||||
|
max-height: 400px;
|
||||||
|
vertical-align: middle;
|
||||||
|
object-fit: contain;
|
||||||
|
|
||||||
|
&.emoji {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
blockquote {
|
||||||
|
margin: 0.2em 0 0.2em 2em;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre {
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
code, samp, kbd, var, pre {
|
||||||
|
font-family: var(--postCodeFont, monospace);
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin: 0 0 1em 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
p:last-child {
|
||||||
|
margin: 0 0 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 1.1em;
|
||||||
|
line-height: 1.2em;
|
||||||
|
margin: 1.4em 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
font-size: 1.1em;
|
||||||
|
margin: 1.0em 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
font-size: 1em;
|
||||||
|
margin: 1.2em 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
h4 {
|
||||||
|
margin: 1.1em 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.greentext {
|
||||||
|
color: $fallback--cGreen;
|
||||||
|
color: var(--cGreen, $fallback--cGreen);
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeline :not(.panel-disabled) > {
|
||||||
|
.status-el:last-child {
|
||||||
|
border-radius: 0 0 $fallback--panelRadius $fallback--panelRadius;
|
||||||
|
border-radius: 0 0 var(--panelRadius, $fallback--panelRadius) var(--panelRadius, $fallback--panelRadius);
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
@ -9,7 +9,7 @@ import { mapGetters } from 'vuex'
|
|||||||
|
|
||||||
export default {
|
export default {
|
||||||
props: [
|
props: [
|
||||||
'user', 'switcher', 'selected', 'hideBio', 'rounded', 'bordered', 'allowZoomingAvatar'
|
'userId', 'switcher', 'selected', 'hideBio', 'rounded', 'bordered', 'allowZoomingAvatar'
|
||||||
],
|
],
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
@ -21,6 +21,12 @@ export default {
|
|||||||
this.$store.dispatch('fetchUserRelationship', this.user.id)
|
this.$store.dispatch('fetchUserRelationship', this.user.id)
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
user () {
|
||||||
|
return this.$store.getters.findUser(this.userId)
|
||||||
|
},
|
||||||
|
relationship () {
|
||||||
|
return this.$store.getters.relationship(this.userId)
|
||||||
|
},
|
||||||
classes () {
|
classes () {
|
||||||
return [{
|
return [{
|
||||||
'user-card-rounded-t': this.rounded === 'top', // set border-top-left-radius and border-top-right-radius
|
'user-card-rounded-t': this.rounded === 'top', // set border-top-left-radius and border-top-right-radius
|
||||||
|
@ -69,6 +69,7 @@
|
|||||||
<AccountActions
|
<AccountActions
|
||||||
v-if="isOtherUser && loggedIn"
|
v-if="isOtherUser && loggedIn"
|
||||||
:user="user"
|
:user="user"
|
||||||
|
:relationship="relationship"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="bottom-line">
|
<div class="bottom-line">
|
||||||
@ -92,7 +93,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="user-meta">
|
<div class="user-meta">
|
||||||
<div
|
<div
|
||||||
v-if="user.follows_you && loggedIn && isOtherUser"
|
v-if="relationship.followed_by && loggedIn && isOtherUser"
|
||||||
class="following"
|
class="following"
|
||||||
>
|
>
|
||||||
{{ $t('user_card.follows_you') }}
|
{{ $t('user_card.follows_you') }}
|
||||||
@ -139,10 +140,10 @@
|
|||||||
class="user-interactions"
|
class="user-interactions"
|
||||||
>
|
>
|
||||||
<div class="btn-group">
|
<div class="btn-group">
|
||||||
<FollowButton :user="user" />
|
<FollowButton :relationship="relationship" />
|
||||||
<template v-if="user.following">
|
<template v-if="relationship.following">
|
||||||
<ProgressButton
|
<ProgressButton
|
||||||
v-if="!user.subscribed"
|
v-if="!relationship.subscribing"
|
||||||
class="btn btn-default"
|
class="btn btn-default"
|
||||||
:click="subscribeUser"
|
:click="subscribeUser"
|
||||||
:title="$t('user_card.subscribe')"
|
:title="$t('user_card.subscribe')"
|
||||||
@ -161,7 +162,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<button
|
<button
|
||||||
v-if="user.muted"
|
v-if="relationship.muting"
|
||||||
class="btn btn-default btn-block toggled"
|
class="btn btn-default btn-block toggled"
|
||||||
@click="unmuteUser"
|
@click="unmuteUser"
|
||||||
>
|
>
|
||||||
|
@ -6,7 +6,7 @@
|
|||||||
class="panel panel-default signed-in"
|
class="panel panel-default signed-in"
|
||||||
>
|
>
|
||||||
<UserCard
|
<UserCard
|
||||||
:user="user"
|
:user-id="user.id"
|
||||||
:hide-bio="true"
|
:hide-bio="true"
|
||||||
rounded="top"
|
rounded="top"
|
||||||
/>
|
/>
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
class="user-profile panel panel-default"
|
class="user-profile panel panel-default"
|
||||||
>
|
>
|
||||||
<UserCard
|
<UserCard
|
||||||
:user="user"
|
:user-id="userId"
|
||||||
:switcher="true"
|
:switcher="true"
|
||||||
:selected="timeline.viewing"
|
:selected="timeline.viewing"
|
||||||
:allow-zooming-avatar="true"
|
:allow-zooming-avatar="true"
|
||||||
|
@ -351,14 +351,14 @@ const UserSettings = {
|
|||||||
},
|
},
|
||||||
filterUnblockedUsers (userIds) {
|
filterUnblockedUsers (userIds) {
|
||||||
return reject(userIds, (userId) => {
|
return reject(userIds, (userId) => {
|
||||||
const user = this.$store.getters.findUser(userId)
|
const relationship = this.$store.getters.relationship(this.userId)
|
||||||
return !user || user.statusnet_blocking || user.id === this.$store.state.users.currentUser.id
|
return relationship.blocking || userId === this.$store.state.users.currentUser.id
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
filterUnMutedUsers (userIds) {
|
filterUnMutedUsers (userIds) {
|
||||||
return reject(userIds, (userId) => {
|
return reject(userIds, (userId) => {
|
||||||
const user = this.$store.getters.findUser(userId)
|
const relationship = this.$store.getters.relationship(this.userId)
|
||||||
return !user || user.muted || user.id === this.$store.state.users.currentUser.id
|
return relationship.muting || userId === this.$store.state.users.currentUser.id
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
queryUserIds (query) {
|
queryUserIds (query) {
|
||||||
|
@ -379,6 +379,7 @@
|
|||||||
:label="$t('settings.notifications')"
|
:label="$t('settings.notifications')"
|
||||||
>
|
>
|
||||||
<div class="setting-item">
|
<div class="setting-item">
|
||||||
|
<h2>{{ $t('settings.notification_setting_filters') }}</h2>
|
||||||
<div class="select-multiple">
|
<div class="select-multiple">
|
||||||
<span class="label">{{ $t('settings.notification_setting') }}</span>
|
<span class="label">{{ $t('settings.notification_setting') }}</span>
|
||||||
<ul class="option-list">
|
<ul class="option-list">
|
||||||
@ -404,6 +405,17 @@
|
|||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="setting-item">
|
||||||
|
<h2>{{ $t('settings.notification_setting_privacy') }}</h2>
|
||||||
|
<p>
|
||||||
|
<Checkbox v-model="notificationSettings.privacy_option">
|
||||||
|
{{ $t('settings.notification_setting_privacy_option') }}
|
||||||
|
</Checkbox>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="setting-item">
|
||||||
<p>{{ $t('settings.notification_mutes') }}</p>
|
<p>{{ $t('settings.notification_mutes') }}</p>
|
||||||
<p>{{ $t('settings.notification_blocks') }}</p>
|
<p>{{ $t('settings.notification_blocks') }}</p>
|
||||||
<button
|
<button
|
||||||
|
@ -405,11 +405,14 @@
|
|||||||
"fun": "Fun",
|
"fun": "Fun",
|
||||||
"greentext": "Meme arrows",
|
"greentext": "Meme arrows",
|
||||||
"notifications": "Notifications",
|
"notifications": "Notifications",
|
||||||
|
"notification_setting_filters": "Filters",
|
||||||
"notification_setting": "Receive notifications from:",
|
"notification_setting": "Receive notifications from:",
|
||||||
"notification_setting_follows": "Users you follow",
|
"notification_setting_follows": "Users you follow",
|
||||||
"notification_setting_non_follows": "Users you do not follow",
|
"notification_setting_non_follows": "Users you do not follow",
|
||||||
"notification_setting_followers": "Users who follow you",
|
"notification_setting_followers": "Users who follow you",
|
||||||
"notification_setting_non_followers": "Users who do not follow you",
|
"notification_setting_non_followers": "Users who do not follow you",
|
||||||
|
"notification_setting_privacy": "Privacy",
|
||||||
|
"notification_setting_privacy_option": "Hide the sender and contents of push notifications",
|
||||||
"notification_mutes": "To stop receiving notifications from a specific user, use a mute.",
|
"notification_mutes": "To stop receiving notifications from a specific user, use a mute.",
|
||||||
"notification_blocks": "Blocking a user stops all notifications as well as unsubscribes them.",
|
"notification_blocks": "Blocking a user stops all notifications as well as unsubscribes them.",
|
||||||
"enable_web_push_notifications": "Enable web push notifications",
|
"enable_web_push_notifications": "Enable web push notifications",
|
||||||
@ -617,7 +620,8 @@
|
|||||||
"replies_list": "Replies:",
|
"replies_list": "Replies:",
|
||||||
"mute_conversation": "Mute conversation",
|
"mute_conversation": "Mute conversation",
|
||||||
"unmute_conversation": "Unmute conversation",
|
"unmute_conversation": "Unmute conversation",
|
||||||
"status_unavailable": "Status unavailable"
|
"status_unavailable": "Status unavailable",
|
||||||
|
"copy_link": "Copy link to status"
|
||||||
},
|
},
|
||||||
"user_card": {
|
"user_card": {
|
||||||
"approve": "Approve",
|
"approve": "Approve",
|
||||||
|
@ -48,6 +48,11 @@ const unblockUser = (store, id) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const muteUser = (store, id) => {
|
const muteUser = (store, id) => {
|
||||||
|
const predictedRelationship = store.state.relationships[id] || { id }
|
||||||
|
predictedRelationship.muting = true
|
||||||
|
store.commit('updateUserRelationship', [predictedRelationship])
|
||||||
|
store.commit('addMuteId', id)
|
||||||
|
|
||||||
return store.rootState.api.backendInteractor.muteUser({ id })
|
return store.rootState.api.backendInteractor.muteUser({ id })
|
||||||
.then((relationship) => {
|
.then((relationship) => {
|
||||||
store.commit('updateUserRelationship', [relationship])
|
store.commit('updateUserRelationship', [relationship])
|
||||||
@ -56,6 +61,10 @@ const muteUser = (store, id) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const unmuteUser = (store, id) => {
|
const unmuteUser = (store, id) => {
|
||||||
|
const predictedRelationship = store.state.relationships[id] || { id }
|
||||||
|
predictedRelationship.muting = false
|
||||||
|
store.commit('updateUserRelationship', [predictedRelationship])
|
||||||
|
|
||||||
return store.rootState.api.backendInteractor.unmuteUser({ id })
|
return store.rootState.api.backendInteractor.unmuteUser({ id })
|
||||||
.then((relationship) => store.commit('updateUserRelationship', [relationship]))
|
.then((relationship) => store.commit('updateUserRelationship', [relationship]))
|
||||||
}
|
}
|
||||||
@ -83,10 +92,6 @@ const unmuteDomain = (store, domain) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const mutations = {
|
export const mutations = {
|
||||||
setMuted (state, { user: { id }, muted }) {
|
|
||||||
const user = state.usersObject[id]
|
|
||||||
set(user, 'muted', muted)
|
|
||||||
},
|
|
||||||
tagUser (state, { user: { id }, tag }) {
|
tagUser (state, { user: { id }, tag }) {
|
||||||
const user = state.usersObject[id]
|
const user = state.usersObject[id]
|
||||||
const tags = user.tags || []
|
const tags = user.tags || []
|
||||||
@ -146,26 +151,18 @@ export const mutations = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
addNewUsers (state, users) {
|
addNewUsers (state, users) {
|
||||||
each(users, (user) => mergeOrAdd(state.users, state.usersObject, user))
|
each(users, (user) => {
|
||||||
|
if (user.relationship) {
|
||||||
|
set(state.relationships, user.relationship.id, user.relationship)
|
||||||
|
}
|
||||||
|
mergeOrAdd(state.users, state.usersObject, user)
|
||||||
|
})
|
||||||
},
|
},
|
||||||
updateUserRelationship (state, relationships) {
|
updateUserRelationship (state, relationships) {
|
||||||
relationships.forEach((relationship) => {
|
relationships.forEach((relationship) => {
|
||||||
const user = state.usersObject[relationship.id]
|
set(state.relationships, relationship.id, relationship)
|
||||||
if (user) {
|
|
||||||
user.follows_you = relationship.followed_by
|
|
||||||
user.following = relationship.following
|
|
||||||
user.muted = relationship.muting
|
|
||||||
user.statusnet_blocking = relationship.blocking
|
|
||||||
user.subscribed = relationship.subscribing
|
|
||||||
user.showing_reblogs = relationship.showing_reblogs
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
updateBlocks (state, blockedUsers) {
|
|
||||||
// Reset statusnet_blocking of all fetched users
|
|
||||||
each(state.users, (user) => { user.statusnet_blocking = false })
|
|
||||||
each(blockedUsers, (user) => mergeOrAdd(state.users, state.usersObject, user))
|
|
||||||
},
|
|
||||||
saveBlockIds (state, blockIds) {
|
saveBlockIds (state, blockIds) {
|
||||||
state.currentUser.blockIds = blockIds
|
state.currentUser.blockIds = blockIds
|
||||||
},
|
},
|
||||||
@ -174,11 +171,6 @@ export const mutations = {
|
|||||||
state.currentUser.blockIds.push(blockId)
|
state.currentUser.blockIds.push(blockId)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
updateMutes (state, mutedUsers) {
|
|
||||||
// Reset muted of all fetched users
|
|
||||||
each(state.users, (user) => { user.muted = false })
|
|
||||||
each(mutedUsers, (user) => mergeOrAdd(state.users, state.usersObject, user))
|
|
||||||
},
|
|
||||||
saveMuteIds (state, muteIds) {
|
saveMuteIds (state, muteIds) {
|
||||||
state.currentUser.muteIds = muteIds
|
state.currentUser.muteIds = muteIds
|
||||||
},
|
},
|
||||||
@ -244,6 +236,10 @@ export const getters = {
|
|||||||
return state.usersObject[query.toLowerCase()]
|
return state.usersObject[query.toLowerCase()]
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
|
},
|
||||||
|
relationship: state => id => {
|
||||||
|
const rel = id && state.relationships[id]
|
||||||
|
return rel || { id, loading: true }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -254,7 +250,8 @@ export const defaultState = {
|
|||||||
users: [],
|
users: [],
|
||||||
usersObject: {},
|
usersObject: {},
|
||||||
signUpPending: false,
|
signUpPending: false,
|
||||||
signUpErrors: []
|
signUpErrors: [],
|
||||||
|
relationships: {}
|
||||||
}
|
}
|
||||||
|
|
||||||
const users = {
|
const users = {
|
||||||
@ -279,7 +276,7 @@ const users = {
|
|||||||
return store.rootState.api.backendInteractor.fetchBlocks()
|
return store.rootState.api.backendInteractor.fetchBlocks()
|
||||||
.then((blocks) => {
|
.then((blocks) => {
|
||||||
store.commit('saveBlockIds', map(blocks, 'id'))
|
store.commit('saveBlockIds', map(blocks, 'id'))
|
||||||
store.commit('updateBlocks', blocks)
|
store.commit('addNewUsers', blocks)
|
||||||
return blocks
|
return blocks
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
@ -298,8 +295,8 @@ const users = {
|
|||||||
fetchMutes (store) {
|
fetchMutes (store) {
|
||||||
return store.rootState.api.backendInteractor.fetchMutes()
|
return store.rootState.api.backendInteractor.fetchMutes()
|
||||||
.then((mutes) => {
|
.then((mutes) => {
|
||||||
store.commit('updateMutes', mutes)
|
|
||||||
store.commit('saveMuteIds', map(mutes, 'id'))
|
store.commit('saveMuteIds', map(mutes, 'id'))
|
||||||
|
store.commit('addNewUsers', mutes)
|
||||||
return mutes
|
return mutes
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
@ -416,7 +413,7 @@ const users = {
|
|||||||
},
|
},
|
||||||
addNewNotifications (store, { notifications }) {
|
addNewNotifications (store, { notifications }) {
|
||||||
const users = map(notifications, 'from_profile')
|
const users = map(notifications, 'from_profile')
|
||||||
const targetUsers = map(notifications, 'target')
|
const targetUsers = map(notifications, 'target').filter(_ => _)
|
||||||
const notificationIds = notifications.map(_ => _.id)
|
const notificationIds = notifications.map(_ => _.id)
|
||||||
store.commit('addNewUsers', users)
|
store.commit('addNewUsers', users)
|
||||||
store.commit('addNewUsers', targetUsers)
|
store.commit('addNewUsers', targetUsers)
|
||||||
|
@ -75,13 +75,7 @@ export const parseUser = (data) => {
|
|||||||
output.token = data.pleroma.chat_token
|
output.token = data.pleroma.chat_token
|
||||||
|
|
||||||
if (relationship) {
|
if (relationship) {
|
||||||
output.follows_you = relationship.followed_by
|
output.relationship = relationship
|
||||||
output.requested = relationship.requested
|
|
||||||
output.following = relationship.following
|
|
||||||
output.statusnet_blocking = relationship.blocking
|
|
||||||
output.muted = relationship.muting
|
|
||||||
output.showing_reblogs = relationship.showing_reblogs
|
|
||||||
output.subscribed = relationship.subscribing
|
|
||||||
}
|
}
|
||||||
|
|
||||||
output.allow_following_move = data.pleroma.allow_following_move
|
output.allow_following_move = data.pleroma.allow_following_move
|
||||||
@ -138,16 +132,10 @@ export const parseUser = (data) => {
|
|||||||
|
|
||||||
output.statusnet_profile_url = data.statusnet_profile_url
|
output.statusnet_profile_url = data.statusnet_profile_url
|
||||||
|
|
||||||
output.statusnet_blocking = data.statusnet_blocking
|
|
||||||
|
|
||||||
output.is_local = data.is_local
|
output.is_local = data.is_local
|
||||||
output.role = data.role
|
output.role = data.role
|
||||||
output.show_role = data.show_role
|
output.show_role = data.show_role
|
||||||
|
|
||||||
output.follows_you = data.follows_you
|
|
||||||
|
|
||||||
output.muted = data.muted
|
|
||||||
|
|
||||||
if (data.rights) {
|
if (data.rights) {
|
||||||
output.rights = {
|
output.rights = {
|
||||||
moderator: data.rights.delete_others_notice,
|
moderator: data.rights.delete_others_notice,
|
||||||
@ -161,10 +149,16 @@ export const parseUser = (data) => {
|
|||||||
output.hide_follows_count = data.hide_follows_count
|
output.hide_follows_count = data.hide_follows_count
|
||||||
output.hide_followers_count = data.hide_followers_count
|
output.hide_followers_count = data.hide_followers_count
|
||||||
output.background_image = data.background_image
|
output.background_image = data.background_image
|
||||||
// on mastoapi this info is contained in a "relationship"
|
|
||||||
output.following = data.following
|
|
||||||
// Websocket token
|
// Websocket token
|
||||||
output.token = data.token
|
output.token = data.token
|
||||||
|
|
||||||
|
// Convert relationsip data to expected format
|
||||||
|
output.relationship = {
|
||||||
|
muting: data.muted,
|
||||||
|
blocking: data.statusnet_blocking,
|
||||||
|
followed_by: data.follows_you,
|
||||||
|
following: data.following
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
output.created_at = new Date(data.created_at)
|
output.created_at = new Date(data.created_at)
|
||||||
|
@ -1,24 +1,27 @@
|
|||||||
const fetchUser = (attempt, user, store) => new Promise((resolve, reject) => {
|
const fetchRelationship = (attempt, userId, store) => new Promise((resolve, reject) => {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
store.state.api.backendInteractor.fetchUser({ id: user.id })
|
store.state.api.backendInteractor.fetchUserRelationship({ id: userId })
|
||||||
.then((user) => store.commit('addNewUsers', [user]))
|
.then((relationship) => {
|
||||||
.then(() => resolve([user.following, user.requested, user.locked, attempt]))
|
store.commit('updateUserRelationship', [relationship])
|
||||||
|
return relationship
|
||||||
|
})
|
||||||
|
.then((relationship) => resolve([relationship.following, relationship.requested, relationship.locked, attempt]))
|
||||||
.catch((e) => reject(e))
|
.catch((e) => reject(e))
|
||||||
}, 500)
|
}, 500)
|
||||||
}).then(([following, sent, locked, attempt]) => {
|
}).then(([following, sent, locked, attempt]) => {
|
||||||
if (!following && !(locked && sent) && attempt <= 3) {
|
if (!following && !(locked && sent) && attempt <= 3) {
|
||||||
// If we BE reports that we still not following that user - retry,
|
// If we BE reports that we still not following that user - retry,
|
||||||
// increment attempts by one
|
// increment attempts by one
|
||||||
fetchUser(++attempt, user, store)
|
fetchRelationship(++attempt, userId, store)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
export const requestFollow = (user, store) => new Promise((resolve, reject) => {
|
export const requestFollow = (userId, store) => new Promise((resolve, reject) => {
|
||||||
store.state.api.backendInteractor.followUser({ id: user.id })
|
store.state.api.backendInteractor.followUser({ id: userId })
|
||||||
.then((updated) => {
|
.then((updated) => {
|
||||||
store.commit('updateUserRelationship', [updated])
|
store.commit('updateUserRelationship', [updated])
|
||||||
|
|
||||||
if (updated.following || (user.locked && user.requested)) {
|
if (updated.following || (updated.locked && updated.requested)) {
|
||||||
// If we get result immediately or the account is locked, just stop.
|
// If we get result immediately or the account is locked, just stop.
|
||||||
resolve()
|
resolve()
|
||||||
return
|
return
|
||||||
@ -31,15 +34,15 @@ export const requestFollow = (user, store) => new Promise((resolve, reject) => {
|
|||||||
// don't know that yet.
|
// don't know that yet.
|
||||||
// Recursive Promise, it will call itself up to 3 times.
|
// Recursive Promise, it will call itself up to 3 times.
|
||||||
|
|
||||||
return fetchUser(1, user, store)
|
return fetchRelationship(1, updated, store)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
resolve()
|
resolve()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
export const requestUnfollow = (user, store) => new Promise((resolve, reject) => {
|
export const requestUnfollow = (userId, store) => new Promise((resolve, reject) => {
|
||||||
store.state.api.backendInteractor.unfollowUser({ id: user.id })
|
store.state.api.backendInteractor.unfollowUser({ id: userId })
|
||||||
.then((updated) => {
|
.then((updated) => {
|
||||||
store.commit('updateUserRelationship', [updated])
|
store.commit('updateUserRelationship', [updated])
|
||||||
resolve({
|
resolve({
|
||||||
|
@ -346,6 +346,12 @@
|
|||||||
"code": 59427,
|
"code": 59427,
|
||||||
"src": "fontawesome"
|
"src": "fontawesome"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"uid": "4aad6bb50b02c18508aae9cbe14e784e",
|
||||||
|
"css": "share",
|
||||||
|
"code": 61920,
|
||||||
|
"src": "fontawesome"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"uid": "8b80d36d4ef43889db10bc1f0dc9a862",
|
"uid": "8b80d36d4ef43889db10bc1f0dc9a862",
|
||||||
"css": "user",
|
"css": "user",
|
||||||
|
@ -19,6 +19,7 @@ const actions = {
|
|||||||
|
|
||||||
const testGetters = {
|
const testGetters = {
|
||||||
findUser: state => getters.findUser(state.users),
|
findUser: state => getters.findUser(state.users),
|
||||||
|
relationship: state => getters.relationship(state.users),
|
||||||
mergedConfig: state => ({
|
mergedConfig: state => ({
|
||||||
colors: '',
|
colors: '',
|
||||||
highlight: {},
|
highlight: {},
|
||||||
@ -96,7 +97,8 @@ const externalProfileStore = new Vuex.Store({
|
|||||||
credentials: ''
|
credentials: ''
|
||||||
},
|
},
|
||||||
usersObject: { 100: extUser },
|
usersObject: { 100: extUser },
|
||||||
users: [extUser]
|
users: [extUser],
|
||||||
|
relationships: {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -164,7 +166,8 @@ const localProfileStore = new Vuex.Store({
|
|||||||
credentials: ''
|
credentials: ''
|
||||||
},
|
},
|
||||||
usersObject: { 100: localUser, 'testuser': localUser },
|
usersObject: { 100: localUser, 'testuser': localUser },
|
||||||
users: [localUser]
|
users: [localUser],
|
||||||
|
relationships: {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -18,20 +18,6 @@ describe('The users module', () => {
|
|||||||
expect(state.users).to.eql([user])
|
expect(state.users).to.eql([user])
|
||||||
expect(state.users[0].name).to.eql('Dude')
|
expect(state.users[0].name).to.eql('Dude')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('sets a mute bit on users', () => {
|
|
||||||
const state = cloneDeep(defaultState)
|
|
||||||
const user = { id: '1', name: 'Guy' }
|
|
||||||
|
|
||||||
mutations.addNewUsers(state, [user])
|
|
||||||
mutations.setMuted(state, { user, muted: true })
|
|
||||||
|
|
||||||
expect(user.muted).to.eql(true)
|
|
||||||
|
|
||||||
mutations.setMuted(state, { user, muted: false })
|
|
||||||
|
|
||||||
expect(user.muted).to.eql(false)
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('findUser', () => {
|
describe('findUser', () => {
|
||||||
|
Loading…
Reference in New Issue
Block a user