diff options
Diffstat (limited to 'src/components')
28 files changed, 582 insertions, 474 deletions
diff --git a/src/components/account_actions/account_actions.js b/src/components/account_actions/account_actions.js index 5d7ecf7e..0826c275 100644 --- 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 index 483783cf..744b77d5 100644 --- 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 index 8a02174e..9e410610 100644 --- 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 index c459ff1b..0bf4e37b 100644 --- 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 index 37485383..e4b19d01 100644 --- 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 index 3a7f1283..bca93ea7 100644 --- 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 index 12da2645..95e7cb6b 100644 --- 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 index aefd609e..6dcb6d47 100644 --- 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 index 81e6e6dc..b503783f 100644 --- a/src/components/follow_card/follow_card.vue +++ b/src/components/follow_card/follow_card.vue @@ -2,24 +2,24 @@ <basic-user-card :user="user"> <div class="follow-card-content-container"> <span - v-if="!noFollowsYou && user.follows_you" + v-if="isMe || (!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" /> </div> </template> - <template v-else> + <template v-else-if="!isMe"> <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 index 65c9cfb5..cbec0e9b 100644 --- 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 index 1ae81ce4..1cf4c9bc 100644 --- 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 index f6da07dd..0e46a2a7 100644 --- 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/post_status_form/post_status_form.js b/src/components/post_status_form/post_status_form.js index 74067fef..a98e1e31 100644 --- a/src/components/post_status_form/post_status_form.js +++ b/src/components/post_status_form/post_status_form.js @@ -102,7 +102,7 @@ const PostStatusForm = { ...this.$store.state.instance.customEmoji ], users: this.$store.state.users.users, - updateUsersList: (input) => this.$store.dispatch('searchUsers', input) + updateUsersList: (query) => this.$store.dispatch('searchUsers', { query }) }) }, emojiSuggestor () { diff --git a/src/components/react_button/react_button.js b/src/components/react_button/react_button.js index 19949563..abc3bf07 100644 --- 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 index ab4b4fcd..0b34add1 100644 --- 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 index ace8cc7c..dab06e1e 100644 --- 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/settings_modal/tabs/mutes_and_blocks_tab.js b/src/components/settings_modal/tabs/mutes_and_blocks_tab.js index 3f6b7205..b0043dbb 100644 --- a/src/components/settings_modal/tabs/mutes_and_blocks_tab.js +++ b/src/components/settings_modal/tabs/mutes_and_blocks_tab.js @@ -85,18 +85,18 @@ const MutesAndBlocks = { }, 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) { - return this.$store.dispatch('searchUsers', query) + return this.$store.dispatch('searchUsers', { query }) .then((users) => map(users, 'id')) }, blockUsers (ids) { diff --git a/src/components/settings_modal/tabs/notifications_tab.vue b/src/components/settings_modal/tabs/notifications_tab.vue index ab33a6a5..19181e24 100644 --- a/src/components/settings_modal/tabs/notifications_tab.vue +++ b/src/components/settings_modal/tabs/notifications_tab.vue @@ -1,6 +1,7 @@ <template> <div :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"> @@ -26,6 +27,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/components/settings_modal/tabs/profile_tab.js b/src/components/settings_modal/tabs/profile_tab.js index 949b480b..0b4951d3 100644 --- a/src/components/settings_modal/tabs/profile_tab.js +++ b/src/components/settings_modal/tabs/profile_tab.js @@ -54,7 +54,7 @@ const ProfileTab = { ...this.$store.state.instance.customEmoji ], users: this.$store.state.users.users, - updateUsersList: (input) => this.$store.dispatch('searchUsers', input) + updateUsersList: (query) => this.$store.dispatch('searchUsers', { query }) }) }, emojiSuggestor () { diff --git a/src/components/side_drawer/side_drawer.vue b/src/components/side_drawer/side_drawer.vue index df1d22a4..2958a386 100644 --- 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 index 61d66301..9cd9d61c 100644 --- 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('>')) { - // This checks if post has '>' at the beginning, excluding mentions so that @mention >impying works - return processHtml(html, (string) => { - if (string.includes('>') && - string - .replace(/<[^>]+?>/gi, '') // remove all tags - .replace(/@\w+/gi, '') // remove mentions (even failed ones) - .trim() - .startsWith('>')) { - return `<span class='greentext'>${string}</span>` - } else { - return string - } - }) - } else { - return html - } - } catch (e) { - console.err('Failed to process status html', e) - return html - } - } else { - return html - } - }, - contentHtml () { - if (!this.status.summary_html) { - return this.postBodyHtml - } - return this.status.summary_html + '<br />' + this.postBodyHtml - }, combinedFavsAndRepeatsUsers () { // 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 index ca295640..e4c7545b 100644 --- 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 new file mode 100644 index 00000000..ccc01b6f --- /dev/null +++ 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('>')) { + // This checks if post has '>' at the beginning, excluding mentions so that @mention >impying works + return processHtml(html, (string) => { + if (string.includes('>') && + string + .replace(/<[^>]+?>/gi, '') // remove all tags + .replace(/@\w+/gi, '') // remove mentions (even failed ones) + .trim() + .startsWith('>')) { + return `<span class='greentext'>${string}</span>` + } else { + return string + } + }) + } else { + return html + } + } catch (e) { + console.err('Failed to process status html', e) + return html + } + } else { + return html + } + }, + contentHtml () { + if (!this.status.summary_html) { + return this.postBodyHtml + } + return this.status.summary_html + '<br />' + this.postBodyHtml + }, + ...mapGetters(['mergedConfig']), + ...mapState({ + betterShadow: state => state.interface.browserSupport.cssFilter, + currentUser: state => state.users.currentUser + }) + }, + components: { + Attachment, + Poll, + Gallery, + LinkPreview + }, + methods: { + linkClicked (event) { + const target = event.target.closest('.status-content a') + if (target) { + if (target.className.match(/mention/)) { + const href = target.href + const attn = this.status.attentions.find(attn => mentionMatchesUrl(attn, href)) + if (attn) { + event.stopPropagation() + event.preventDefault() + const link = this.generateUserProfileLink(attn.id, attn.screen_name) + this.$router.push(link) + return + } + } + if (target.rel.match(/(?:^|\s)tag(?:$|\s)/) || target.className.match(/hashtag/)) { + // Extract tag name from link url + const tag = extractTagFromUrl(target.href) + if (tag) { + const link = this.generateTagLink(tag) + this.$router.push(link) + return + } + } + window.open(target.href, '_blank') + } + }, + toggleShowMore () { + if (this.mightHideBecauseTall) { + this.showingTall = !this.showingTall + } else if (this.mightHideBecauseSubject) { + this.expandingSubject = !this.expandingSubject + } + }, + generateUserProfileLink (id, name) { + return generateProfileLink(id, name, this.$store.state.instance.restrictedNicknames) + }, + generateTagLink (tag) { + return `/tag/${tag}` + }, + setMedia () { + const attachments = this.attachmentSize === 'hide' ? this.status.attachments : this.galleryAttachments + return () => this.$store.dispatch('setMedia', attachments) + } + } +} + +export default StatusContent diff --git a/src/components/status_content/status_content.vue b/src/components/status_content/status_content.vue new file mode 100644 index 00000000..8c2e8749 --- /dev/null +++ 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 index 1cdbd3fa..8e6b9d7f 100644 --- 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 index b4f275d9..c4a5ce9d 100644 --- a/src/components/user_card/user_card.vue +++ b/src/components/user_card/user_card.vue @@ -60,6 +60,7 @@ <AccountActions v-if="isOtherUser && loggedIn" :user="user" + :relationship="relationship" /> </div> <div class="bottom-line"> @@ -83,7 +84,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') }} @@ -130,10 +131,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')" @@ -152,7 +153,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 index e9f08015..1db4f648 100644 --- 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 index 14082e83..1871d46c 100644 --- 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" |
