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:
Shpuld Shpludson 2020-05-12 17:36:05 +00:00
commit 5d49edc823
35 changed files with 659 additions and 541 deletions

View File

@ -6,6 +6,17 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
### Changed
- 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
### Fixed
- Show more/less works correctly with auto-collapsed subjects and long posts

View File

@ -241,6 +241,9 @@ const getNodeInfo = async ({ store }) => {
: federation.enabled
})
const accountActivationRequired = metadata.accountActivationRequired
store.dispatch('setInstanceOption', { name: 'accountActivationRequired', value: accountActivationRequired })
const accounts = metadata.staffAccounts
resolveStaffAccounts({ store, accounts })
} else {
@ -304,6 +307,9 @@ const afterStoreSetup = async ({ store, i18n }) => {
getNodeInfo({ store })
])
// Start fetching things that don't need to block the UI
store.dispatch('fetchMutes')
const router = new VueRouter({
mode: 'history',
routes: routes(store),

View File

@ -3,7 +3,7 @@ import Popover from '../popover/popover.vue'
const AccountActions = {
props: [
'user'
'user', 'relationship'
],
data () {
return { }

View File

@ -9,16 +9,16 @@
class="account-tools-popover"
>
<div class="dropdown-menu">
<template v-if="user.following">
<template v-if="relationship.following">
<button
v-if="user.showing_reblogs"
v-if="relationship.showing_reblogs"
class="btn btn-default dropdown-item"
@click="hideRepeats"
>
{{ $t('user_card.hide_repeats') }}
</button>
<button
v-if="!user.showing_reblogs"
v-if="!relationship.showing_reblogs"
class="btn btn-default dropdown-item"
@click="showRepeats"
>
@ -30,7 +30,7 @@
/>
</template>
<button
v-if="user.statusnet_blocking"
v-if="relationship.blocking"
class="btn btn-default btn-block dropdown-item"
@click="unblockUser"
>

View File

@ -12,7 +12,7 @@
class="basic-user-card-expanded-content"
>
<UserCard
:user="user"
:user-id="user.id"
:rounded="true"
:bordered="true"
/>

View File

@ -11,8 +11,11 @@ const BlockCard = {
user () {
return this.$store.getters.findUser(this.userId)
},
relationship () {
return this.$store.getters.relationship(this.userId)
},
blocked () {
return this.user.statusnet_blocking
return this.relationship.blocking
}
},
components: {

View File

@ -29,6 +29,11 @@ const ExtraButtons = {
this.$store.dispatch('unmuteConversation', this.status.id)
.then(() => this.$emit('onSuccess'))
.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: {
@ -46,6 +51,9 @@ const ExtraButtons = {
},
canMute () {
return !!this.currentUser
},
statusLink () {
return `${this.$store.state.instance.server}${this.$router.resolve({ name: 'conversation', params: { id: this.status.id } }).href}`
}
}
}

View File

@ -1,11 +1,13 @@
<template>
<Popover
v-if="canDelete || canMute || canPin"
trigger="click"
placement="top"
class="extra-button-popover"
>
<div slot="content">
<div
slot="content"
slot-scope="{close}"
>
<div class="dropdown-menu">
<button
v-if="canMute && !status.thread_muted"
@ -23,28 +25,35 @@
</button>
<button
v-if="!status.pinned && canPin"
v-close-popover
class="dropdown-item dropdown-item-icon"
@click.prevent="pinStatus"
@click="close"
>
<i class="icon-pin" /><span>{{ $t("status.pin") }}</span>
</button>
<button
v-if="status.pinned && canPin"
v-close-popover
class="dropdown-item dropdown-item-icon"
@click.prevent="unpinStatus"
@click="close"
>
<i class="icon-pin" /><span>{{ $t("status.unpin") }}</span>
</button>
<button
v-if="canDelete"
v-close-popover
class="dropdown-item dropdown-item-icon"
@click.prevent="deleteStatus"
@click="close"
>
<i class="icon-cancel" /><span>{{ $t("status.delete") }}</span>
</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>
<i

View File

@ -1,6 +1,6 @@
import { requestFollow, requestUnfollow } from '../../services/follow_manipulate/follow_manipulate'
export default {
props: ['user', 'labelFollowing', 'buttonClass'],
props: ['relationship', 'labelFollowing', 'buttonClass'],
data () {
return {
inProgress: false
@ -8,12 +8,12 @@ export default {
},
computed: {
isPressed () {
return this.inProgress || this.user.following
return this.inProgress || this.relationship.following
},
title () {
if (this.inProgress || this.user.following) {
if (this.inProgress || this.relationship.following) {
return this.$t('user_card.follow_unfollow')
} else if (this.user.requested) {
} else if (this.relationship.requested) {
return this.$t('user_card.follow_again')
} else {
return this.$t('user_card.follow')
@ -22,9 +22,9 @@ export default {
label () {
if (this.inProgress) {
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')
} else if (this.user.requested) {
} else if (this.relationship.requested) {
return this.$t('user_card.follow_sent')
} else {
return this.$t('user_card.follow')
@ -33,20 +33,20 @@ export default {
},
methods: {
onClick () {
this.user.following ? this.unfollow() : this.follow()
this.relationship.following ? this.unfollow() : this.follow()
},
follow () {
this.inProgress = true
requestFollow(this.user, this.$store).then(() => {
requestFollow(this.relationship.id, this.$store).then(() => {
this.inProgress = false
})
},
unfollow () {
const store = this.$store
this.inProgress = true
requestUnfollow(this.user, store).then(() => {
requestUnfollow(this.relationship.id, store).then(() => {
this.inProgress = false
store.commit('removeStatus', { timeline: 'friends', userId: this.user.id })
store.commit('removeStatus', { timeline: 'friends', userId: this.relationship.id })
})
}
}

View File

@ -18,6 +18,9 @@ const FollowCard = {
},
loggedIn () {
return this.$store.state.users.currentUser
},
relationship () {
return this.$store.getters.relationship(this.user.id)
}
}
}

View File

@ -2,14 +2,14 @@
<basic-user-card :user="user">
<div class="follow-card-content-container">
<span
v-if="!noFollowsYou && user.follows_you"
v-if="!noFollowsYou && relationship.followed_by"
class="faint"
>
{{ isMe ? $t('user_card.its_you') : $t('user_card.follows_you') }}
</span>
<template v-if="!loggedIn">
<div
v-if="!user.following"
v-if="!relationship.following"
class="follow-card-follow-button"
>
<RemoteFollow :user="user" />
@ -17,9 +17,9 @@
</template>
<template v-else>
<FollowButton
:user="user"
class="follow-card-follow-button"
:relationship="relationship"
:label-following="$t('user_card.follow_unfollow')"
class="follow-card-follow-button"
/>
</template>
</div>

View File

@ -11,8 +11,11 @@ const MuteCard = {
user () {
return this.$store.getters.findUser(this.userId)
},
relationship () {
return this.$store.getters.relationship(this.userId)
},
muted () {
return this.user.muted
return this.relationship.muting
}
},
components: {
@ -21,13 +24,13 @@ const MuteCard = {
methods: {
unmuteUser () {
this.progress = true
this.$store.dispatch('unmuteUser', this.user.id).then(() => {
this.$store.dispatch('unmuteUser', this.userId).then(() => {
this.progress = false
})
},
muteUser () {
this.progress = true
this.$store.dispatch('muteUser', this.user.id).then(() => {
this.$store.dispatch('muteUser', this.userId).then(() => {
this.progress = false
})
}

View File

@ -75,7 +75,7 @@ const Notification = {
return this.generateUserProfileLink(this.targetUser)
},
needMute () {
return this.user.muted
return this.$store.getters.relationship(this.user.id).muting
},
isStatusNotification () {
return isStatusNotification(this.notification.type)

View File

@ -40,7 +40,7 @@
<div class="notification-right">
<UserCard
v-if="userExpanded"
:user="getUser(notification)"
:user-id="getUser(notification).id"
:rounded="true"
:bordered="true"
/>

View File

@ -2,7 +2,7 @@ import Popover from '../popover/popover.vue'
import { mapGetters } from 'vuex'
const ReactButton = {
props: ['status', 'loggedIn'],
props: ['status'],
data () {
return {
filterWord: ''

View File

@ -37,7 +37,6 @@
</div>
</div>
<i
v-if="loggedIn"
slot="trigger"
class="icon-smile button-icon add-reaction-button"
:title="$t('tool_tip.add_reaction')"

View File

@ -1,5 +1,5 @@
import { validationMixin } from 'vuelidate'
import { required, sameAs } from 'vuelidate/lib/validators'
import { required, requiredIf, sameAs } from 'vuelidate/lib/validators'
import { mapActions, mapState } from 'vuex'
const registration = {
@ -14,15 +14,17 @@ const registration = {
},
captcha: {}
}),
validations: {
user: {
email: { required },
username: { required },
fullname: { required },
password: { required },
confirm: {
required,
sameAsPassword: sameAs('password')
validations () {
return {
user: {
email: { required: requiredIf(() => this.accountActivationRequired) },
username: { required },
fullname: { required },
password: { required },
confirm: {
required,
sameAsPassword: sameAs('password')
}
}
}
},
@ -43,7 +45,8 @@ const registration = {
signedIn: (state) => !!state.users.currentUser,
isPending: (state) => state.users.signUpPending,
serverValidationErrors: (state) => state.users.signUpErrors,
termsOfService: (state) => state.instance.tos
termsOfService: (state) => state.instance.tos,
accountActivationRequired: (state) => state.instance.accountActivationRequired
})
},
methods: {

View File

@ -19,7 +19,7 @@
>
<UserCard
v-if="currentUser"
:user="currentUser"
:user-id="currentUser.id"
:hide-bio="true"
/>
<div

View File

@ -1,23 +1,17 @@
import Attachment from '../attachment/attachment.vue'
import FavoriteButton from '../favorite_button/favorite_button.vue'
import ReactButton from '../react_button/react_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 PostStatusForm from '../post_status_form/post_status_form.vue'
import UserCard from '../user_card/user_card.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 Timeago from '../timeago/timeago.vue'
import StatusContent from '../status_content/status_content.vue'
import StatusPopover from '../status_popover/status_popover.vue'
import EmojiReactions from '../emoji_reactions/emoji_reactions.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 { 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 { mapGetters, mapState } from 'vuex'
@ -43,17 +37,10 @@ const Status = {
replying: false,
unmuted: false,
userExpanded: false,
showingTall: this.inConversation && this.focused,
showingLongSubject: false,
error: null,
// not as computed because it sets the initial state which will be changed later
expandingSubject: !this.$store.getters.mergedConfig.collapseMessageWithSubject
error: null
}
},
computed: {
localCollapseSubjectDefault () {
return this.mergedConfig.collapseMessageWithSubject
},
muteWords () {
return this.mergedConfig.muteWords
},
@ -79,10 +66,6 @@ const Status = {
const highlight = this.mergedConfig.highlight
return highlightStyle(highlight[user.screen_name])
},
hideAttachments () {
return (this.mergedConfig.hideAttachments && !this.inConversation) ||
(this.mergedConfig.hideAttachmentsInConv && this.inConversation)
},
userProfileLink () {
return this.generateUserProfileLink(this.status.user.id, this.status.user.screen_name)
},
@ -118,7 +101,13 @@ const Status = {
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 () {
return this.mergedConfig.hideFilteredStatuses
},
@ -135,20 +124,6 @@ const Status = {
// use conversation highlight only when in conversation
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 () {
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) {
continue
}
const taggedUser = this.$store.getters.findUser(this.status.attentions[i].id)
if (checkFollowing && taggedUser && taggedUser.following) {
// There's zero guarantee of this working. If we happen to have that user and their
// 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
}
if (this.status.attentions[i].id === this.currentUser.id) {
@ -188,32 +166,6 @@ const Status = {
}
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 () {
if (!this.status.summary) return ''
const decodedSummary = unescape(this.status.summary)
@ -227,83 +179,6 @@ const Status = {
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('&gt;')) {
// This checks if post has '>' at the beginning, excluding mentions so that @mention >impying works
return processHtml(html, (string) => {
if (string.includes('&gt;') &&
string
.replace(/<[^>]+?>/gi, '') // remove all tags
.replace(/@\w+/gi, '') // remove mentions (even failed ones)
.trim()
.startsWith('&gt;')) {
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 () {
// Use the status from the global status repository since favs and repeats are saved in it
const combinedUsers = [].concat(
@ -312,9 +187,6 @@ const Status = {
)
return uniqBy(combinedUsers, 'id')
},
ownStatus () {
return this.status.user.id === this.currentUser.id
},
tags () {
return this.status.tags.filter(tagObj => tagObj.hasOwnProperty('name')).map(tagObj => tagObj.name).join(' ')
},
@ -328,21 +200,18 @@ const Status = {
})
},
components: {
Attachment,
FavoriteButton,
ReactButton,
RetweetButton,
ExtraButtons,
PostStatusForm,
Poll,
UserCard,
UserAvatar,
Gallery,
LinkPreview,
AvatarList,
Timeago,
StatusPopover,
EmojiReactions
EmojiReactions,
StatusContent
},
methods: {
visibilityIcon (visibility) {
@ -363,32 +232,6 @@ const Status = {
clearError () {
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 () {
this.replying = !this.replying
},
@ -406,22 +249,8 @@ const Status = {
toggleUserExpanded () {
this.userExpanded = !this.userExpanded
},
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)
}
},
watch: {

View File

@ -94,7 +94,7 @@
<div class="status-body">
<UserCard
v-if="userExpanded"
:user="status.user"
:user-id="status.user.id"
:rounded="true"
:bordered="true"
class="status-usercard"
@ -226,118 +226,12 @@
</div>
</div>
<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': 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>
<StatusContent
:status="status"
:no-heading="noHeading"
:highlight="highlight"
:focused="isFocused"
/>
<transition name="fade">
<div
@ -404,7 +298,7 @@
:status="status"
/>
<ReactButton
:logged-in="loggedIn"
v-if="loggedIn"
:status="status"
/>
<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 {
padding: 0.4em $status-margin;
margin: 0;
@ -790,11 +585,6 @@ $status-margin: 0.75em;
}
}
.greentext {
color: $fallback--cGreen;
color: var(--cGreen, $fallback--cGreen);
}
.status-conversation {
border-left-style: solid;
}
@ -866,14 +656,6 @@ a.unmute {
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 {
margin-top: $status-margin;

View 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('&gt;')) {
// This checks if post has '>' at the beginning, excluding mentions so that @mention >impying works
return processHtml(html, (string) => {
if (string.includes('&gt;') &&
string
.replace(/<[^>]+?>/gi, '') // remove all tags
.replace(/@\w+/gi, '') // remove mentions (even failed ones)
.trim()
.startsWith('&gt;')) {
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

View 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>

View File

@ -9,7 +9,7 @@ import { mapGetters } from 'vuex'
export default {
props: [
'user', 'switcher', 'selected', 'hideBio', 'rounded', 'bordered', 'allowZoomingAvatar'
'userId', 'switcher', 'selected', 'hideBio', 'rounded', 'bordered', 'allowZoomingAvatar'
],
data () {
return {
@ -21,6 +21,12 @@ export default {
this.$store.dispatch('fetchUserRelationship', this.user.id)
},
computed: {
user () {
return this.$store.getters.findUser(this.userId)
},
relationship () {
return this.$store.getters.relationship(this.userId)
},
classes () {
return [{
'user-card-rounded-t': this.rounded === 'top', // set border-top-left-radius and border-top-right-radius

View File

@ -69,6 +69,7 @@
<AccountActions
v-if="isOtherUser && loggedIn"
:user="user"
:relationship="relationship"
/>
</div>
<div class="bottom-line">
@ -92,7 +93,7 @@
</div>
<div class="user-meta">
<div
v-if="user.follows_you && loggedIn && isOtherUser"
v-if="relationship.followed_by && loggedIn && isOtherUser"
class="following"
>
{{ $t('user_card.follows_you') }}
@ -139,10 +140,10 @@
class="user-interactions"
>
<div class="btn-group">
<FollowButton :user="user" />
<template v-if="user.following">
<FollowButton :relationship="relationship" />
<template v-if="relationship.following">
<ProgressButton
v-if="!user.subscribed"
v-if="!relationship.subscribing"
class="btn btn-default"
:click="subscribeUser"
:title="$t('user_card.subscribe')"
@ -161,7 +162,7 @@
</div>
<div>
<button
v-if="user.muted"
v-if="relationship.muting"
class="btn btn-default btn-block toggled"
@click="unmuteUser"
>

View File

@ -6,7 +6,7 @@
class="panel panel-default signed-in"
>
<UserCard
:user="user"
:user-id="user.id"
:hide-bio="true"
rounded="top"
/>

View File

@ -5,7 +5,7 @@
class="user-profile panel panel-default"
>
<UserCard
:user="user"
:user-id="userId"
:switcher="true"
:selected="timeline.viewing"
:allow-zooming-avatar="true"

View File

@ -351,14 +351,14 @@ const UserSettings = {
},
filterUnblockedUsers (userIds) {
return reject(userIds, (userId) => {
const user = this.$store.getters.findUser(userId)
return !user || user.statusnet_blocking || user.id === this.$store.state.users.currentUser.id
const relationship = this.$store.getters.relationship(this.userId)
return relationship.blocking || userId === this.$store.state.users.currentUser.id
})
},
filterUnMutedUsers (userIds) {
return reject(userIds, (userId) => {
const user = this.$store.getters.findUser(userId)
return !user || user.muted || user.id === this.$store.state.users.currentUser.id
const relationship = this.$store.getters.relationship(this.userId)
return relationship.muting || userId === this.$store.state.users.currentUser.id
})
},
queryUserIds (query) {

View File

@ -379,6 +379,7 @@
:label="$t('settings.notifications')"
>
<div class="setting-item">
<h2>{{ $t('settings.notification_setting_filters') }}</h2>
<div class="select-multiple">
<span class="label">{{ $t('settings.notification_setting') }}</span>
<ul class="option-list">
@ -404,6 +405,17 @@
</li>
</ul>
</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_blocks') }}</p>
<button

View File

@ -405,11 +405,14 @@
"fun": "Fun",
"greentext": "Meme arrows",
"notifications": "Notifications",
"notification_setting_filters": "Filters",
"notification_setting": "Receive notifications from:",
"notification_setting_follows": "Users you follow",
"notification_setting_non_follows": "Users you do not follow",
"notification_setting_followers": "Users who 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_blocks": "Blocking a user stops all notifications as well as unsubscribes them.",
"enable_web_push_notifications": "Enable web push notifications",
@ -617,7 +620,8 @@
"replies_list": "Replies:",
"mute_conversation": "Mute conversation",
"unmute_conversation": "Unmute conversation",
"status_unavailable": "Status unavailable"
"status_unavailable": "Status unavailable",
"copy_link": "Copy link to status"
},
"user_card": {
"approve": "Approve",

View File

@ -48,6 +48,11 @@ const unblockUser = (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 })
.then((relationship) => {
store.commit('updateUserRelationship', [relationship])
@ -56,6 +61,10 @@ const muteUser = (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 })
.then((relationship) => store.commit('updateUserRelationship', [relationship]))
}
@ -83,10 +92,6 @@ const unmuteDomain = (store, domain) => {
}
export const mutations = {
setMuted (state, { user: { id }, muted }) {
const user = state.usersObject[id]
set(user, 'muted', muted)
},
tagUser (state, { user: { id }, tag }) {
const user = state.usersObject[id]
const tags = user.tags || []
@ -146,26 +151,18 @@ export const mutations = {
}
},
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) {
relationships.forEach((relationship) => {
const user = state.usersObject[relationship.id]
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
}
set(state.relationships, relationship.id, relationship)
})
},
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) {
state.currentUser.blockIds = blockIds
},
@ -174,11 +171,6 @@ export const mutations = {
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) {
state.currentUser.muteIds = muteIds
},
@ -244,6 +236,10 @@ export const getters = {
return state.usersObject[query.toLowerCase()]
}
return result
},
relationship: state => id => {
const rel = id && state.relationships[id]
return rel || { id, loading: true }
}
}
@ -254,7 +250,8 @@ export const defaultState = {
users: [],
usersObject: {},
signUpPending: false,
signUpErrors: []
signUpErrors: [],
relationships: {}
}
const users = {
@ -279,7 +276,7 @@ const users = {
return store.rootState.api.backendInteractor.fetchBlocks()
.then((blocks) => {
store.commit('saveBlockIds', map(blocks, 'id'))
store.commit('updateBlocks', blocks)
store.commit('addNewUsers', blocks)
return blocks
})
},
@ -298,8 +295,8 @@ const users = {
fetchMutes (store) {
return store.rootState.api.backendInteractor.fetchMutes()
.then((mutes) => {
store.commit('updateMutes', mutes)
store.commit('saveMuteIds', map(mutes, 'id'))
store.commit('addNewUsers', mutes)
return mutes
})
},
@ -416,7 +413,7 @@ const users = {
},
addNewNotifications (store, { notifications }) {
const users = map(notifications, 'from_profile')
const targetUsers = map(notifications, 'target')
const targetUsers = map(notifications, 'target').filter(_ => _)
const notificationIds = notifications.map(_ => _.id)
store.commit('addNewUsers', users)
store.commit('addNewUsers', targetUsers)

View File

@ -75,13 +75,7 @@ export const parseUser = (data) => {
output.token = data.pleroma.chat_token
if (relationship) {
output.follows_you = relationship.followed_by
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.relationship = relationship
}
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_blocking = data.statusnet_blocking
output.is_local = data.is_local
output.role = data.role
output.show_role = data.show_role
output.follows_you = data.follows_you
output.muted = data.muted
if (data.rights) {
output.rights = {
moderator: data.rights.delete_others_notice,
@ -161,10 +149,16 @@ export const parseUser = (data) => {
output.hide_follows_count = data.hide_follows_count
output.hide_followers_count = data.hide_followers_count
output.background_image = data.background_image
// on mastoapi this info is contained in a "relationship"
output.following = data.following
// Websocket 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)

View File

@ -1,24 +1,27 @@
const fetchUser = (attempt, user, store) => new Promise((resolve, reject) => {
const fetchRelationship = (attempt, userId, store) => new Promise((resolve, reject) => {
setTimeout(() => {
store.state.api.backendInteractor.fetchUser({ id: user.id })
.then((user) => store.commit('addNewUsers', [user]))
.then(() => resolve([user.following, user.requested, user.locked, attempt]))
store.state.api.backendInteractor.fetchUserRelationship({ id: userId })
.then((relationship) => {
store.commit('updateUserRelationship', [relationship])
return relationship
})
.then((relationship) => resolve([relationship.following, relationship.requested, relationship.locked, attempt]))
.catch((e) => reject(e))
}, 500)
}).then(([following, sent, locked, attempt]) => {
if (!following && !(locked && sent) && attempt <= 3) {
// If we BE reports that we still not following that user - retry,
// increment attempts by one
fetchUser(++attempt, user, store)
fetchRelationship(++attempt, userId, store)
}
})
export const requestFollow = (user, store) => new Promise((resolve, reject) => {
store.state.api.backendInteractor.followUser({ id: user.id })
export const requestFollow = (userId, store) => new Promise((resolve, reject) => {
store.state.api.backendInteractor.followUser({ id: userId })
.then((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.
resolve()
return
@ -31,15 +34,15 @@ export const requestFollow = (user, store) => new Promise((resolve, reject) => {
// don't know that yet.
// Recursive Promise, it will call itself up to 3 times.
return fetchUser(1, user, store)
return fetchRelationship(1, updated, store)
.then(() => {
resolve()
})
})
})
export const requestUnfollow = (user, store) => new Promise((resolve, reject) => {
store.state.api.backendInteractor.unfollowUser({ id: user.id })
export const requestUnfollow = (userId, store) => new Promise((resolve, reject) => {
store.state.api.backendInteractor.unfollowUser({ id: userId })
.then((updated) => {
store.commit('updateUserRelationship', [updated])
resolve({

View File

@ -346,6 +346,12 @@
"code": 59427,
"src": "fontawesome"
},
{
"uid": "4aad6bb50b02c18508aae9cbe14e784e",
"css": "share",
"code": 61920,
"src": "fontawesome"
},
{
"uid": "8b80d36d4ef43889db10bc1f0dc9a862",
"css": "user",

View File

@ -19,6 +19,7 @@ const actions = {
const testGetters = {
findUser: state => getters.findUser(state.users),
relationship: state => getters.relationship(state.users),
mergedConfig: state => ({
colors: '',
highlight: {},
@ -96,7 +97,8 @@ const externalProfileStore = new Vuex.Store({
credentials: ''
},
usersObject: { 100: extUser },
users: [extUser]
users: [extUser],
relationships: {}
}
}
})
@ -164,7 +166,8 @@ const localProfileStore = new Vuex.Store({
credentials: ''
},
usersObject: { 100: localUser, 'testuser': localUser },
users: [localUser]
users: [localUser],
relationships: {}
}
}
})

View File

@ -18,20 +18,6 @@ describe('The users module', () => {
expect(state.users).to.eql([user])
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', () => {