Merge remote-tracking branch 'origin/develop' into settings-modal
* origin/develop: (95 commits) Translated using Weblate (Italian) Translated using Weblate (Chinese (Simplified)) Translated using Weblate (Russian) Translated using Weblate (Polish) Translated using Weblate (Dutch) Translated using Weblate (German) Translated using Weblate (German) Translated using Weblate (German) Translated using Weblate (German) Translated using Weblate (German) Translated using Weblate (German) Translated using Weblate (German) Translated using Weblate (German) Translated using Weblate (German) Translated using Weblate (German) Translated using Weblate (German) Translated using Weblate (German) Translated using Weblate (German) Translated using Weblate (German) Translated using Weblate (German) ...
This commit is contained in:
commit
138ec85003
51 changed files with 5250 additions and 3716 deletions
|
@ -3,7 +3,7 @@ import Popover from '../popover/popover.vue'
|
|||
|
||||
const AccountActions = {
|
||||
props: [
|
||||
'user'
|
||||
'user', 'relationship'
|
||||
],
|
||||
data () {
|
||||
return { }
|
||||
|
|
|
@ -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"
|
||||
>
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
class="basic-user-card-expanded-content"
|
||||
>
|
||||
<UserCard
|
||||
:user="user"
|
||||
:user-id="user.id"
|
||||
:rounded="true"
|
||||
:bordered="true"
|
||||
/>
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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}`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 })
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,6 +18,9 @@ const FollowCard = {
|
|||
},
|
||||
loggedIn () {
|
||||
return this.$store.state.users.currentUser
|
||||
},
|
||||
relationship () {
|
||||
return this.$store.getters.relationship(this.user.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -40,7 +40,7 @@
|
|||
<div class="notification-right">
|
||||
<UserCard
|
||||
v-if="userExpanded"
|
||||
:user="getUser(notification)"
|
||||
:user-id="getUser(notification).id"
|
||||
:rounded="true"
|
||||
:bordered="true"
|
||||
/>
|
||||
|
|
|
@ -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 () {
|
||||
|
|
|
@ -2,7 +2,7 @@ import Popover from '../popover/popover.vue'
|
|||
import { mapGetters } from 'vuex'
|
||||
|
||||
const ReactButton = {
|
||||
props: ['status', 'loggedIn'],
|
||||
props: ['status'],
|
||||
data () {
|
||||
return {
|
||||
filterWord: ''
|
||||
|
|
|
@ -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')"
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 () {
|
||||
|
|
|
@ -19,7 +19,7 @@
|
|||
>
|
||||
<UserCard
|
||||
v-if="currentUser"
|
||||
:user="currentUser"
|
||||
:user-id="currentUser.id"
|
||||
:hide-bio="true"
|
||||
/>
|
||||
<div
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
210
src/components/status_content/status_content.js
Normal file
210
src/components/status_content/status_content.js
Normal file
|
@ -0,0 +1,210 @@
|
|||
import Attachment from '../attachment/attachment.vue'
|
||||
import Poll from '../poll/poll.vue'
|
||||
import Gallery from '../gallery/gallery.vue'
|
||||
import LinkPreview from '../link-preview/link-preview.vue'
|
||||
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
|
||||
import fileType from 'src/services/file_type/file_type.service'
|
||||
import { processHtml } from 'src/services/tiny_post_html_processor/tiny_post_html_processor.service.js'
|
||||
import { mentionMatchesUrl, extractTagFromUrl } from 'src/services/matcher/matcher.service.js'
|
||||
import { mapGetters, mapState } from 'vuex'
|
||||
|
||||
const StatusContent = {
|
||||
name: 'StatusContent',
|
||||
props: [
|
||||
'status',
|
||||
'focused',
|
||||
'noHeading',
|
||||
'fullContent'
|
||||
],
|
||||
data () {
|
||||
return {
|
||||
showingTall: this.inConversation && this.focused,
|
||||
showingLongSubject: false,
|
||||
// not as computed because it sets the initial state which will be changed later
|
||||
expandingSubject: !this.$store.getters.mergedConfig.collapseMessageWithSubject
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
localCollapseSubjectDefault () {
|
||||
return this.mergedConfig.collapseMessageWithSubject
|
||||
},
|
||||
hideAttachments () {
|
||||
return (this.mergedConfig.hideAttachments && !this.inConversation) ||
|
||||
(this.mergedConfig.hideAttachmentsInConv && this.inConversation)
|
||||
},
|
||||
// This is a bit hacky, but we want to approximate post height before rendering
|
||||
// so we count newlines (masto uses <p> for paragraphs, GS uses <br> between them)
|
||||
// as well as approximate line count by counting characters and approximating ~80
|
||||
// per line.
|
||||
//
|
||||
// Using max-height + overflow: auto for status components resulted in false positives
|
||||
// very often with japanese characters, and it was very annoying.
|
||||
tallStatus () {
|
||||
const lengthScore = this.status.statusnet_html.split(/<p|<br/).length + this.status.text.length / 80
|
||||
return lengthScore > 20
|
||||
},
|
||||
longSubject () {
|
||||
return this.status.summary.length > 900
|
||||
},
|
||||
// When a status has a subject and is also tall, we should only have one show more/less button. If the default is to collapse statuses with subjects, we just treat it like a status with a subject; otherwise, we just treat it like a tall status.
|
||||
mightHideBecauseSubject () {
|
||||
return this.status.summary && (!this.tallStatus || this.localCollapseSubjectDefault)
|
||||
},
|
||||
mightHideBecauseTall () {
|
||||
return this.tallStatus && (!this.status.summary || !this.localCollapseSubjectDefault)
|
||||
},
|
||||
hideSubjectStatus () {
|
||||
return this.mightHideBecauseSubject && !this.expandingSubject
|
||||
},
|
||||
hideTallStatus () {
|
||||
return this.mightHideBecauseTall && !this.showingTall
|
||||
},
|
||||
showingMore () {
|
||||
return (this.mightHideBecauseTall && this.showingTall) || (this.mightHideBecauseSubject && this.expandingSubject)
|
||||
},
|
||||
nsfwClickthrough () {
|
||||
if (!this.status.nsfw) {
|
||||
return false
|
||||
}
|
||||
if (this.status.summary && this.localCollapseSubjectDefault) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
},
|
||||
attachmentSize () {
|
||||
if ((this.mergedConfig.hideAttachments && !this.inConversation) ||
|
||||
(this.mergedConfig.hideAttachmentsInConv && this.inConversation) ||
|
||||
(this.status.attachments.length > this.maxThumbnails)) {
|
||||
return 'hide'
|
||||
} else if (this.compact) {
|
||||
return 'small'
|
||||
}
|
||||
return 'normal'
|
||||
},
|
||||
galleryTypes () {
|
||||
if (this.attachmentSize === 'hide') {
|
||||
return []
|
||||
}
|
||||
return this.mergedConfig.playVideosInModal
|
||||
? ['image', 'video']
|
||||
: ['image']
|
||||
},
|
||||
galleryAttachments () {
|
||||
return this.status.attachments.filter(
|
||||
file => fileType.fileMatchesSomeType(this.galleryTypes, file)
|
||||
)
|
||||
},
|
||||
nonGalleryAttachments () {
|
||||
return this.status.attachments.filter(
|
||||
file => !fileType.fileMatchesSomeType(this.galleryTypes, file)
|
||||
)
|
||||
},
|
||||
hasImageAttachments () {
|
||||
return this.status.attachments.some(
|
||||
file => fileType.fileType(file.mimetype) === 'image'
|
||||
)
|
||||
},
|
||||
hasVideoAttachments () {
|
||||
return this.status.attachments.some(
|
||||
file => fileType.fileType(file.mimetype) === 'video'
|
||||
)
|
||||
},
|
||||
maxThumbnails () {
|
||||
return this.mergedConfig.maxThumbnails
|
||||
},
|
||||
postBodyHtml () {
|
||||
const html = this.status.statusnet_html
|
||||
|
||||
if (this.mergedConfig.greentext) {
|
||||
try {
|
||||
if (html.includes('>')) {
|
||||
// This checks if post has '>' at the beginning, excluding mentions so that @mention >impying works
|
||||
return processHtml(html, (string) => {
|
||||
if (string.includes('>') &&
|
||||
string
|
||||
.replace(/<[^>]+?>/gi, '') // remove all tags
|
||||
.replace(/@\w+/gi, '') // remove mentions (even failed ones)
|
||||
.trim()
|
||||
.startsWith('>')) {
|
||||
return `<span class='greentext'>${string}</span>`
|
||||
} else {
|
||||
return string
|
||||
}
|
||||
})
|
||||
} else {
|
||||
return html
|
||||
}
|
||||
} catch (e) {
|
||||
console.err('Failed to process status html', e)
|
||||
return html
|
||||
}
|
||||
} else {
|
||||
return html
|
||||
}
|
||||
},
|
||||
contentHtml () {
|
||||
if (!this.status.summary_html) {
|
||||
return this.postBodyHtml
|
||||
}
|
||||
return this.status.summary_html + '<br />' + this.postBodyHtml
|
||||
},
|
||||
...mapGetters(['mergedConfig']),
|
||||
...mapState({
|
||||
betterShadow: state => state.interface.browserSupport.cssFilter,
|
||||
currentUser: state => state.users.currentUser
|
||||
})
|
||||
},
|
||||
components: {
|
||||
Attachment,
|
||||
Poll,
|
||||
Gallery,
|
||||
LinkPreview
|
||||
},
|
||||
methods: {
|
||||
linkClicked (event) {
|
||||
const target = event.target.closest('.status-content a')
|
||||
if (target) {
|
||||
if (target.className.match(/mention/)) {
|
||||
const href = target.href
|
||||
const attn = this.status.attentions.find(attn => mentionMatchesUrl(attn, href))
|
||||
if (attn) {
|
||||
event.stopPropagation()
|
||||
event.preventDefault()
|
||||
const link = this.generateUserProfileLink(attn.id, attn.screen_name)
|
||||
this.$router.push(link)
|
||||
return
|
||||
}
|
||||
}
|
||||
if (target.rel.match(/(?:^|\s)tag(?:$|\s)/) || target.className.match(/hashtag/)) {
|
||||
// Extract tag name from link url
|
||||
const tag = extractTagFromUrl(target.href)
|
||||
if (tag) {
|
||||
const link = this.generateTagLink(tag)
|
||||
this.$router.push(link)
|
||||
return
|
||||
}
|
||||
}
|
||||
window.open(target.href, '_blank')
|
||||
}
|
||||
},
|
||||
toggleShowMore () {
|
||||
if (this.mightHideBecauseTall) {
|
||||
this.showingTall = !this.showingTall
|
||||
} else if (this.mightHideBecauseSubject) {
|
||||
this.expandingSubject = !this.expandingSubject
|
||||
}
|
||||
},
|
||||
generateUserProfileLink (id, name) {
|
||||
return generateProfileLink(id, name, this.$store.state.instance.restrictedNicknames)
|
||||
},
|
||||
generateTagLink (tag) {
|
||||
return `/tag/${tag}`
|
||||
},
|
||||
setMedia () {
|
||||
const attachments = this.attachmentSize === 'hide' ? this.status.attachments : this.galleryAttachments
|
||||
return () => this.$store.dispatch('setMedia', attachments)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default StatusContent
|
240
src/components/status_content/status_content.vue
Normal file
240
src/components/status_content/status_content.vue
Normal file
|
@ -0,0 +1,240 @@
|
|||
<template>
|
||||
<!-- eslint-disable vue/no-v-html -->
|
||||
<div class="status-body">
|
||||
<slot name="header" />
|
||||
<div
|
||||
v-if="longSubject"
|
||||
class="status-content-wrapper"
|
||||
:class="{ 'tall-status': !showingLongSubject }"
|
||||
>
|
||||
<a
|
||||
v-if="!showingLongSubject"
|
||||
class="tall-status-hider"
|
||||
:class="{ 'tall-status-hider_focused': focused }"
|
||||
href="#"
|
||||
@click.prevent="showingLongSubject=true"
|
||||
>
|
||||
{{ $t("general.show_more") }}
|
||||
<span
|
||||
v-if="hasImageAttachments"
|
||||
class="icon-picture"
|
||||
/>
|
||||
<span
|
||||
v-if="hasVideoAttachments"
|
||||
class="icon-video"
|
||||
/>
|
||||
<span
|
||||
v-if="status.card"
|
||||
class="icon-link"
|
||||
/>
|
||||
</a>
|
||||
<div
|
||||
class="status-content media-body"
|
||||
@click.prevent="linkClicked"
|
||||
v-html="contentHtml"
|
||||
/>
|
||||
<a
|
||||
v-if="showingLongSubject"
|
||||
href="#"
|
||||
class="status-unhider"
|
||||
@click.prevent="showingLongSubject=false"
|
||||
>{{ $t("general.show_less") }}</a>
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
:class="{'tall-status': hideTallStatus}"
|
||||
class="status-content-wrapper"
|
||||
>
|
||||
<a
|
||||
v-if="hideTallStatus"
|
||||
class="tall-status-hider"
|
||||
:class="{ 'tall-status-hider_focused': focused }"
|
||||
href="#"
|
||||
@click.prevent="toggleShowMore"
|
||||
>{{ $t("general.show_more") }}</a>
|
||||
<div
|
||||
v-if="!hideSubjectStatus"
|
||||
class="status-content media-body"
|
||||
@click.prevent="linkClicked"
|
||||
v-html="contentHtml"
|
||||
/>
|
||||
<div
|
||||
v-else
|
||||
class="status-content media-body"
|
||||
@click.prevent="linkClicked"
|
||||
v-html="status.summary_html"
|
||||
/>
|
||||
<a
|
||||
v-if="hideSubjectStatus"
|
||||
href="#"
|
||||
class="cw-status-hider"
|
||||
@click.prevent="toggleShowMore"
|
||||
>{{ $t("general.show_more") }}</a>
|
||||
<a
|
||||
v-if="showingMore"
|
||||
href="#"
|
||||
class="status-unhider"
|
||||
@click.prevent="toggleShowMore"
|
||||
>{{ $t("general.show_less") }}</a>
|
||||
</div>
|
||||
|
||||
<div v-if="status.poll && status.poll.options">
|
||||
<poll :base-poll="status.poll" />
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="status.attachments.length !== 0 && (!hideSubjectStatus || showingLongSubject)"
|
||||
class="attachments media-body"
|
||||
>
|
||||
<attachment
|
||||
v-for="attachment in nonGalleryAttachments"
|
||||
:key="attachment.id"
|
||||
class="non-gallery"
|
||||
:size="attachmentSize"
|
||||
:nsfw="nsfwClickthrough"
|
||||
:attachment="attachment"
|
||||
:allow-play="true"
|
||||
:set-media="setMedia()"
|
||||
/>
|
||||
<gallery
|
||||
v-if="galleryAttachments.length > 0"
|
||||
:nsfw="nsfwClickthrough"
|
||||
:attachments="galleryAttachments"
|
||||
:set-media="setMedia()"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="status.card && !hideSubjectStatus && !noHeading"
|
||||
class="link-preview media-body"
|
||||
>
|
||||
<link-preview
|
||||
:card="status.card"
|
||||
:size="attachmentSize"
|
||||
:nsfw="nsfwClickthrough"
|
||||
/>
|
||||
</div>
|
||||
<slot name="footer" />
|
||||
</div>
|
||||
<!-- eslint-enable vue/no-v-html -->
|
||||
</template>
|
||||
|
||||
<script src="./status_content.js" ></script>
|
||||
<style lang="scss">
|
||||
@import '../../_variables.scss';
|
||||
|
||||
$status-margin: 0.75em;
|
||||
|
||||
.status-body {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
|
||||
.tall-status {
|
||||
position: relative;
|
||||
height: 220px;
|
||||
overflow-x: hidden;
|
||||
overflow-y: hidden;
|
||||
z-index: 1;
|
||||
.status-content {
|
||||
height: 100%;
|
||||
mask: linear-gradient(to top, white, transparent) bottom/100% 70px no-repeat,
|
||||
linear-gradient(to top, white, white);
|
||||
/* Autoprefixed seem to ignore this one, and also syntax is different */
|
||||
-webkit-mask-composite: xor;
|
||||
mask-composite: exclude;
|
||||
}
|
||||
}
|
||||
|
||||
.tall-status-hider {
|
||||
display: inline-block;
|
||||
word-break: break-all;
|
||||
position: absolute;
|
||||
height: 70px;
|
||||
margin-top: 150px;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
line-height: 110px;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.status-unhider, .cw-status-hider {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
display: inline-block;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.status-content {
|
||||
font-family: var(--postFont, sans-serif);
|
||||
line-height: 1.4em;
|
||||
white-space: pre-wrap;
|
||||
|
||||
img, video {
|
||||
max-width: 100%;
|
||||
max-height: 400px;
|
||||
vertical-align: middle;
|
||||
object-fit: contain;
|
||||
|
||||
&.emoji {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
}
|
||||
}
|
||||
|
||||
blockquote {
|
||||
margin: 0.2em 0 0.2em 2em;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
pre {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
code, samp, kbd, var, pre {
|
||||
font-family: var(--postCodeFont, monospace);
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0 0 1em 0;
|
||||
}
|
||||
|
||||
p:last-child {
|
||||
margin: 0 0 0 0;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 1.1em;
|
||||
line-height: 1.2em;
|
||||
margin: 1.4em 0;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 1.1em;
|
||||
margin: 1.0em 0;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 1em;
|
||||
margin: 1.2em 0;
|
||||
}
|
||||
|
||||
h4 {
|
||||
margin: 1.1em 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.greentext {
|
||||
color: $fallback--cGreen;
|
||||
color: var(--cGreen, $fallback--cGreen);
|
||||
}
|
||||
|
||||
.timeline :not(.panel-disabled) > {
|
||||
.status-el:last-child {
|
||||
border-radius: 0 0 $fallback--panelRadius $fallback--panelRadius;
|
||||
border-radius: 0 0 var(--panelRadius, $fallback--panelRadius) var(--panelRadius, $fallback--panelRadius);
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
|
@ -9,7 +9,7 @@ import { mapGetters } from 'vuex'
|
|||
|
||||
export default {
|
||||
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
|
||||
|
|
|
@ -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"
|
||||
>
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
class="panel panel-default signed-in"
|
||||
>
|
||||
<UserCard
|
||||
:user="user"
|
||||
:user-id="user.id"
|
||||
:hide-bio="true"
|
||||
rounded="top"
|
||||
/>
|
||||
|
|
|
@ -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"
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue