logo

pleroma-fe

My custom branche(s) on git.pleroma.social/pleroma/pleroma-fe git clone https://hacktivis.me/git/pleroma-fe.git
commit: 5d49edc823ba2ea3e34d4fd6c5efcc84ef9712f7
parent a0f780c4550b77d4574e0de8932a2dff288784a3
Author: Shpuld Shpludson <shp@cock.li>
Date:   Tue, 12 May 2020 17:36:05 +0000

Merge branch 'rc/2.0.5' into 'master'

Update MASTER for 2.0.5 patch

See merge request pleroma/pleroma-fe!1105

Diffstat:

MCHANGELOG.md11+++++++++++
Msrc/boot/after_store.js6++++++
Msrc/components/account_actions/account_actions.js2+-
Msrc/components/account_actions/account_actions.vue8++++----
Msrc/components/basic_user_card/basic_user_card.vue2+-
Msrc/components/block_card/block_card.js5++++-
Msrc/components/extra_buttons/extra_buttons.js8++++++++
Msrc/components/extra_buttons/extra_buttons.vue19++++++++++++++-----
Msrc/components/follow_button/follow_button.js20++++++++++----------
Msrc/components/follow_card/follow_card.js3+++
Msrc/components/follow_card/follow_card.vue8++++----
Msrc/components/mute_card/mute_card.js9++++++---
Msrc/components/notification/notification.js2+-
Msrc/components/notification/notification.vue2+-
Msrc/components/react_button/react_button.js2+-
Msrc/components/react_button/react_button.vue1-
Msrc/components/registration/registration.js25++++++++++++++-----------
Msrc/components/side_drawer/side_drawer.vue2+-
Msrc/components/status/status.js203+++++++------------------------------------------------------------------------
Msrc/components/status/status.vue234+++----------------------------------------------------------------------------
Asrc/components/status_content/status_content.js210+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/components/status_content/status_content.vue240+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/components/user_card/user_card.js8+++++++-
Msrc/components/user_card/user_card.vue11++++++-----
Msrc/components/user_panel/user_panel.vue2+-
Msrc/components/user_profile/user_profile.vue2+-
Msrc/components/user_settings/user_settings.js8++++----
Msrc/components/user_settings/user_settings.vue12++++++++++++
Msrc/i18n/en.json6+++++-
Msrc/modules/users.js53+++++++++++++++++++++++++----------------------------
Msrc/services/entity_normalizer/entity_normalizer.service.js24+++++++++---------------
Msrc/services/follow_manipulate/follow_manipulate.js25++++++++++++++-----------
Mstatic/fontello.json6++++++
Mtest/unit/specs/components/user_profile.spec.js7+++++--
Mtest/unit/specs/modules/users.spec.js14--------------
35 files changed, 659 insertions(+), 541 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md @@ -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 diff --git a/src/boot/after_store.js b/src/boot/after_store.js @@ -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), diff --git a/src/components/account_actions/account_actions.js b/src/components/account_actions/account_actions.js @@ -3,7 +3,7 @@ import Popover from '../popover/popover.vue' const AccountActions = { props: [ - 'user' + 'user', 'relationship' ], data () { return { } diff --git a/src/components/account_actions/account_actions.vue b/src/components/account_actions/account_actions.vue @@ -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" > diff --git a/src/components/basic_user_card/basic_user_card.vue b/src/components/basic_user_card/basic_user_card.vue @@ -12,7 +12,7 @@ class="basic-user-card-expanded-content" > <UserCard - :user="user" + :user-id="user.id" :rounded="true" :bordered="true" /> diff --git a/src/components/block_card/block_card.js b/src/components/block_card/block_card.js @@ -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: { diff --git a/src/components/extra_buttons/extra_buttons.js b/src/components/extra_buttons/extra_buttons.js @@ -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}` } } } diff --git a/src/components/extra_buttons/extra_buttons.vue b/src/components/extra_buttons/extra_buttons.vue @@ -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 diff --git a/src/components/follow_button/follow_button.js b/src/components/follow_button/follow_button.js @@ -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 }) }) } } diff --git a/src/components/follow_card/follow_card.js b/src/components/follow_card/follow_card.js @@ -18,6 +18,9 @@ const FollowCard = { }, loggedIn () { return this.$store.state.users.currentUser + }, + relationship () { + return this.$store.getters.relationship(this.user.id) } } } diff --git a/src/components/follow_card/follow_card.vue b/src/components/follow_card/follow_card.vue @@ -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> diff --git a/src/components/mute_card/mute_card.js b/src/components/mute_card/mute_card.js @@ -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 }) } diff --git a/src/components/notification/notification.js b/src/components/notification/notification.js @@ -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) diff --git a/src/components/notification/notification.vue b/src/components/notification/notification.vue @@ -40,7 +40,7 @@ <div class="notification-right"> <UserCard v-if="userExpanded" - :user="getUser(notification)" + :user-id="getUser(notification).id" :rounded="true" :bordered="true" /> diff --git a/src/components/react_button/react_button.js b/src/components/react_button/react_button.js @@ -2,7 +2,7 @@ import Popover from '../popover/popover.vue' import { mapGetters } from 'vuex' const ReactButton = { - props: ['status', 'loggedIn'], + props: ['status'], data () { return { filterWord: '' diff --git a/src/components/react_button/react_button.vue b/src/components/react_button/react_button.vue @@ -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')" diff --git a/src/components/registration/registration.js b/src/components/registration/registration.js @@ -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: { diff --git a/src/components/side_drawer/side_drawer.vue b/src/components/side_drawer/side_drawer.vue @@ -19,7 +19,7 @@ > <UserCard v-if="currentUser" - :user="currentUser" + :user-id="currentUser.id" :hide-bio="true" /> <div diff --git a/src/components/status/status.js b/src/components/status/status.js @@ -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: { diff --git a/src/components/status/status.vue b/src/components/status/status.vue @@ -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; diff --git a/src/components/status_content/status_content.js b/src/components/status_content/status_content.js @@ -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 diff --git a/src/components/status_content/status_content.vue b/src/components/status_content/status_content.vue @@ -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> diff --git a/src/components/user_card/user_card.js b/src/components/user_card/user_card.js @@ -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 diff --git a/src/components/user_card/user_card.vue b/src/components/user_card/user_card.vue @@ -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" > diff --git a/src/components/user_panel/user_panel.vue b/src/components/user_panel/user_panel.vue @@ -6,7 +6,7 @@ class="panel panel-default signed-in" > <UserCard - :user="user" + :user-id="user.id" :hide-bio="true" rounded="top" /> diff --git a/src/components/user_profile/user_profile.vue b/src/components/user_profile/user_profile.vue @@ -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" diff --git a/src/components/user_settings/user_settings.js b/src/components/user_settings/user_settings.js @@ -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) { diff --git a/src/components/user_settings/user_settings.vue b/src/components/user_settings/user_settings.vue @@ -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 diff --git a/src/i18n/en.json b/src/i18n/en.json @@ -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", diff --git a/src/modules/users.js b/src/modules/users.js @@ -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) diff --git a/src/services/entity_normalizer/entity_normalizer.service.js b/src/services/entity_normalizer/entity_normalizer.service.js @@ -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) diff --git a/src/services/follow_manipulate/follow_manipulate.js b/src/services/follow_manipulate/follow_manipulate.js @@ -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({ diff --git a/static/fontello.json b/static/fontello.json @@ -347,6 +347,12 @@ "src": "fontawesome" }, { + "uid": "4aad6bb50b02c18508aae9cbe14e784e", + "css": "share", + "code": 61920, + "src": "fontawesome" + }, + { "uid": "8b80d36d4ef43889db10bc1f0dc9a862", "css": "user", "code": 59428, diff --git a/test/unit/specs/components/user_profile.spec.js b/test/unit/specs/components/user_profile.spec.js @@ -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: {} } } }) diff --git a/test/unit/specs/modules/users.spec.js b/test/unit/specs/modules/users.spec.js @@ -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', () => {