Merge branch 'develop' of git.pleroma.social:pleroma/pleroma-fe into develop
This commit is contained in:
commit
66c44b4260
62 changed files with 1206 additions and 595 deletions
|
@ -37,12 +37,14 @@ const ChatListItem = {
|
|||
}
|
||||
},
|
||||
messageForStatusContent () {
|
||||
const content = this.chat.lastMessage ? (this.attachmentInfo || this.chat.lastMessage.content) : ''
|
||||
|
||||
const message = this.chat.lastMessage
|
||||
const isYou = message && message.account_id === this.currentUser.id
|
||||
const content = message ? (this.attachmentInfo || message.content) : ''
|
||||
const messagePreview = isYou ? `<i>${this.$t('chats.you')}</i> ${content}` : content
|
||||
return {
|
||||
summary: '',
|
||||
statusnet_html: content,
|
||||
text: content,
|
||||
statusnet_html: messagePreview,
|
||||
text: messagePreview,
|
||||
attachments: []
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
@click.stop.prevent="togglePanel"
|
||||
>
|
||||
<div class="title">
|
||||
<span>{{ $t('chat.title') }}</span>
|
||||
<span>{{ $t('shoutbox.title') }}</span>
|
||||
<i
|
||||
v-if="floating"
|
||||
class="icon-cancel"
|
||||
|
@ -64,7 +64,7 @@
|
|||
>
|
||||
<div class="title">
|
||||
<i class="icon-comment-empty" />
|
||||
{{ $t('chat.title') }}
|
||||
{{ $t('shoutbox.title') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -52,7 +52,7 @@ export default {
|
|||
right: 0;
|
||||
top: 0;
|
||||
display: block;
|
||||
content: '✔';
|
||||
content: '✓';
|
||||
transition: color 200ms;
|
||||
width: 1.1em;
|
||||
height: 1.1em;
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import UserAvatar from '../user_avatar/user_avatar.vue'
|
||||
import Popover from '../popover/popover.vue'
|
||||
import UserListPopover from '../user_list_popover/user_list_popover.vue'
|
||||
|
||||
const EMOJI_REACTION_COUNT_CUTOFF = 12
|
||||
|
||||
|
@ -7,7 +7,7 @@ const EmojiReactions = {
|
|||
name: 'EmojiReactions',
|
||||
components: {
|
||||
UserAvatar,
|
||||
Popover
|
||||
UserListPopover
|
||||
},
|
||||
props: ['status'],
|
||||
data: () => ({
|
||||
|
|
|
@ -1,44 +1,11 @@
|
|||
<template>
|
||||
<div class="emoji-reactions">
|
||||
<Popover
|
||||
<UserListPopover
|
||||
v-for="(reaction) in emojiReactions"
|
||||
:key="reaction.name"
|
||||
trigger="hover"
|
||||
placement="top"
|
||||
:offset="{ y: 5 }"
|
||||
:users="accountsForEmoji[reaction.name]"
|
||||
>
|
||||
<div
|
||||
slot="content"
|
||||
class="reacted-users"
|
||||
>
|
||||
<div v-if="accountsForEmoji[reaction.name].length">
|
||||
<div
|
||||
v-for="(account) in accountsForEmoji[reaction.name]"
|
||||
:key="account.id"
|
||||
class="reacted-user"
|
||||
>
|
||||
<UserAvatar
|
||||
:user="account"
|
||||
class="avatar-small"
|
||||
:compact="true"
|
||||
/>
|
||||
<div class="reacted-user-names">
|
||||
<!-- eslint-disable vue/no-v-html -->
|
||||
<span
|
||||
class="reacted-user-name"
|
||||
v-html="account.name_html"
|
||||
/>
|
||||
<!-- eslint-enable vue/no-v-html -->
|
||||
<span class="reacted-user-screen-name">{{ account.screen_name }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else>
|
||||
<i class="icon-spin4 animate-spin" />
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
slot="trigger"
|
||||
class="emoji-reaction btn btn-default"
|
||||
:class="{ 'picked-reaction': reactedWith(reaction.name), 'not-clickable': !loggedIn }"
|
||||
@click="emojiOnClick(reaction.name, $event)"
|
||||
|
@ -47,7 +14,7 @@
|
|||
<span class="reaction-emoji">{{ reaction.name }}</span>
|
||||
<span>{{ reaction.count }}</span>
|
||||
</button>
|
||||
</Popover>
|
||||
</UserListPopover>
|
||||
<a
|
||||
v-if="tooManyReactions"
|
||||
class="emoji-reaction-expand faint"
|
||||
|
@ -69,32 +36,6 @@
|
|||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.reacted-users {
|
||||
padding: 0.5em;
|
||||
}
|
||||
|
||||
.reacted-user {
|
||||
padding: 0.25em;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
.reacted-user-names {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-left: 0.5em;
|
||||
min-width: 5em;
|
||||
|
||||
img {
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
}
|
||||
}
|
||||
|
||||
.reacted-user-screen-name {
|
||||
font-size: 9px;
|
||||
}
|
||||
}
|
||||
|
||||
.emoji-reaction {
|
||||
padding: 0 0.5em;
|
||||
margin-right: 0.5em;
|
||||
|
|
|
@ -2,6 +2,7 @@ import SideDrawer from '../side_drawer/side_drawer.vue'
|
|||
import Notifications from '../notifications/notifications.vue'
|
||||
import { unseenNotificationsFromStore } from '../../services/notification_utils/notification_utils'
|
||||
import GestureService from '../../services/gesture_service/gesture_service'
|
||||
import { mapGetters } from 'vuex'
|
||||
|
||||
const MobileNav = {
|
||||
components: {
|
||||
|
@ -33,7 +34,8 @@ const MobileNav = {
|
|||
sitename () { return this.$store.state.instance.name },
|
||||
isChat () {
|
||||
return this.$route.name === 'chat'
|
||||
}
|
||||
},
|
||||
...mapGetters(['unreadChatCount'])
|
||||
},
|
||||
methods: {
|
||||
toggleMobileSidebar () {
|
||||
|
@ -67,7 +69,7 @@ const MobileNav = {
|
|||
this.$refs.notifications.markAsSeen()
|
||||
},
|
||||
onScroll ({ target: { scrollTop, clientHeight, scrollHeight } }) {
|
||||
if (this.$store.getters.mergedConfig.autoLoad && scrollTop + clientHeight >= scrollHeight) {
|
||||
if (scrollTop + clientHeight >= scrollHeight) {
|
||||
this.$refs.notifications.fetchOlderNotifications()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,6 +16,10 @@
|
|||
@click.stop.prevent="toggleMobileSidebar()"
|
||||
>
|
||||
<i class="button-icon icon-menu" />
|
||||
<div
|
||||
v-if="unreadChatCount"
|
||||
class="alert-dot"
|
||||
/>
|
||||
</a>
|
||||
<router-link
|
||||
v-if="!hideSitename"
|
||||
|
|
|
@ -75,6 +75,7 @@ export default {
|
|||
deleteOption (index, event) {
|
||||
if (this.options.length > 2) {
|
||||
this.options.splice(index, 1)
|
||||
this.updatePollToParent()
|
||||
}
|
||||
},
|
||||
convertExpiryToUnit (unit, amount) {
|
||||
|
|
|
@ -27,6 +27,11 @@ const buildMentionsString = ({ user, attentions = [] }, currentUser) => {
|
|||
return mentions.length > 0 ? mentions.join(' ') + ' ' : ''
|
||||
}
|
||||
|
||||
// Converts a string with px to a number like '2px' -> 2
|
||||
const pxStringToNumber = (str) => {
|
||||
return Number(str.substring(0, str.length - 2))
|
||||
}
|
||||
|
||||
const PostStatusForm = {
|
||||
props: [
|
||||
'replyTo',
|
||||
|
@ -61,6 +66,7 @@ const PostStatusForm = {
|
|||
StatusContent
|
||||
},
|
||||
mounted () {
|
||||
this.updateIdempotencyKey()
|
||||
this.resize(this.$refs.textarea)
|
||||
|
||||
if (this.replyTo) {
|
||||
|
@ -111,7 +117,8 @@ const PostStatusForm = {
|
|||
dropStopTimeout: null,
|
||||
preview: null,
|
||||
previewLoading: false,
|
||||
emojiInputShown: false
|
||||
emojiInputShown: false,
|
||||
idempotencyKey: ''
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
|
@ -206,17 +213,46 @@ const PostStatusForm = {
|
|||
})
|
||||
},
|
||||
watch: {
|
||||
'newStatus.contentType': function () {
|
||||
this.autoPreview()
|
||||
},
|
||||
'newStatus.spoilerText': function () {
|
||||
this.autoPreview()
|
||||
'newStatus': {
|
||||
deep: true,
|
||||
handler () {
|
||||
this.statusChanged()
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
statusChanged () {
|
||||
this.autoPreview()
|
||||
this.updateIdempotencyKey()
|
||||
},
|
||||
clearStatus () {
|
||||
const newStatus = this.newStatus
|
||||
this.newStatus = {
|
||||
status: '',
|
||||
spoilerText: '',
|
||||
files: [],
|
||||
visibility: newStatus.visibility,
|
||||
contentType: newStatus.contentType,
|
||||
poll: {},
|
||||
mediaDescriptions: {}
|
||||
}
|
||||
this.pollFormVisible = false
|
||||
this.$refs.mediaUpload && this.$refs.mediaUpload.clearFile()
|
||||
this.clearPollForm()
|
||||
if (this.preserveFocus) {
|
||||
this.$nextTick(() => {
|
||||
this.$refs.textarea.focus()
|
||||
})
|
||||
}
|
||||
let el = this.$el.querySelector('textarea')
|
||||
el.style.height = 'auto'
|
||||
el.style.height = undefined
|
||||
this.error = null
|
||||
if (this.preview) this.previewStatus()
|
||||
},
|
||||
async postStatus (event, newStatus, opts = {}) {
|
||||
if (this.posting) { return }
|
||||
if (this.submitDisabled) { return }
|
||||
if (this.disableSubmit) { return }
|
||||
if (this.emojiInputShown) { return }
|
||||
if (this.submitOnEnter) {
|
||||
event.stopPropagation()
|
||||
|
@ -253,36 +289,16 @@ const PostStatusForm = {
|
|||
store: this.$store,
|
||||
inReplyToStatusId: this.replyTo,
|
||||
contentType: newStatus.contentType,
|
||||
poll
|
||||
poll,
|
||||
idempotencyKey: this.idempotencyKey
|
||||
}
|
||||
|
||||
const postHandler = this.postHandler ? this.postHandler : statusPoster.postStatus
|
||||
|
||||
postHandler(postingOptions).then((data) => {
|
||||
if (!data.error) {
|
||||
this.newStatus = {
|
||||
status: '',
|
||||
spoilerText: '',
|
||||
files: [],
|
||||
visibility: newStatus.visibility,
|
||||
contentType: newStatus.contentType,
|
||||
poll: {},
|
||||
mediaDescriptions: {}
|
||||
}
|
||||
this.pollFormVisible = false
|
||||
this.$refs.mediaUpload && this.$refs.mediaUpload.clearFile()
|
||||
this.clearPollForm()
|
||||
this.clearStatus()
|
||||
this.$emit('posted', data)
|
||||
if (this.preserveFocus) {
|
||||
this.$nextTick(() => {
|
||||
this.$refs.textarea.focus()
|
||||
})
|
||||
}
|
||||
let el = this.$el.querySelector('textarea')
|
||||
el.style.height = 'auto'
|
||||
el.style.height = undefined
|
||||
this.error = null
|
||||
if (this.preview) this.previewStatus()
|
||||
} else {
|
||||
this.error = data.error
|
||||
}
|
||||
|
@ -399,7 +415,6 @@ const PostStatusForm = {
|
|||
}
|
||||
},
|
||||
onEmojiInputInput (e) {
|
||||
this.autoPreview()
|
||||
this.$nextTick(() => {
|
||||
this.resize(this.$refs['textarea'])
|
||||
})
|
||||
|
@ -423,7 +438,7 @@ const PostStatusForm = {
|
|||
* scroll is different for `Window` and `Element`s
|
||||
*/
|
||||
const bottomBottomPaddingStr = window.getComputedStyle(bottomRef)['padding-bottom']
|
||||
const bottomBottomPadding = Number(bottomBottomPaddingStr.substring(0, bottomBottomPaddingStr.length - 2))
|
||||
const bottomBottomPadding = pxStringToNumber(bottomBottomPaddingStr)
|
||||
|
||||
const scrollerRef = this.$el.closest('.sidebar-scroller') ||
|
||||
this.$el.closest('.post-form-modal-view') ||
|
||||
|
@ -432,10 +447,12 @@ const PostStatusForm = {
|
|||
// Getting info about padding we have to account for, removing 'px' part
|
||||
const topPaddingStr = window.getComputedStyle(target)['padding-top']
|
||||
const bottomPaddingStr = window.getComputedStyle(target)['padding-bottom']
|
||||
const topPadding = Number(topPaddingStr.substring(0, topPaddingStr.length - 2))
|
||||
const bottomPadding = Number(bottomPaddingStr.substring(0, bottomPaddingStr.length - 2))
|
||||
const topPadding = pxStringToNumber(topPaddingStr)
|
||||
const bottomPadding = pxStringToNumber(bottomPaddingStr)
|
||||
const vertPadding = topPadding + bottomPadding
|
||||
|
||||
const oldHeight = pxStringToNumber(target.style.height)
|
||||
|
||||
/* Explanation:
|
||||
*
|
||||
* https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollHeight
|
||||
|
@ -464,8 +481,13 @@ const PostStatusForm = {
|
|||
|
||||
// BEGIN content size update
|
||||
target.style.height = 'auto'
|
||||
const heightWithoutPadding = target.scrollHeight - vertPadding
|
||||
const newHeight = this.maxHeight ? Math.min(heightWithoutPadding, this.maxHeight) : heightWithoutPadding
|
||||
const heightWithoutPadding = Math.floor(target.scrollHeight - vertPadding)
|
||||
let newHeight = this.maxHeight ? Math.min(heightWithoutPadding, this.maxHeight) : heightWithoutPadding
|
||||
// This is a bit of a hack to combat target.scrollHeight being different on every other input
|
||||
// on some browsers for whatever reason. Don't change the height if difference is 1px or less.
|
||||
if (Math.abs(newHeight - oldHeight) <= 1) {
|
||||
newHeight = oldHeight
|
||||
}
|
||||
target.style.height = `${newHeight}px`
|
||||
this.$emit('resize', newHeight)
|
||||
// END content size update
|
||||
|
@ -530,6 +552,9 @@ const PostStatusForm = {
|
|||
},
|
||||
handleEmojiInputShow (value) {
|
||||
this.emojiInputShown = value
|
||||
},
|
||||
updateIdempotencyKey () {
|
||||
this.idempotencyKey = Date.now().toString()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -79,10 +79,7 @@
|
|||
@click.stop.prevent="togglePreview"
|
||||
>
|
||||
{{ $t('post_status.preview') }}
|
||||
<i
|
||||
class="icon-down-open"
|
||||
:style="{ transform: showPreview ? 'rotate(0deg)' : 'rotate(-90deg)' }"
|
||||
/>
|
||||
<i :class="showPreview ? 'icon-left-open' : 'icon-right-open'" />
|
||||
</a>
|
||||
<i
|
||||
v-show="previewLoading"
|
||||
|
@ -374,6 +371,7 @@
|
|||
}
|
||||
|
||||
.preview-heading {
|
||||
padding-left: 0.5em;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
|
||||
|
@ -385,14 +383,16 @@
|
|||
.preview-toggle {
|
||||
display: flex;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
.icon-down-open {
|
||||
transition: transform 0.1s;
|
||||
i {
|
||||
margin-left: 0.2em;
|
||||
font-size: 0.8em;
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
}
|
||||
|
||||
.preview-container {
|
||||
|
|
|
@ -28,7 +28,10 @@ const ReactButton = {
|
|||
},
|
||||
emojis () {
|
||||
if (this.filterWord !== '') {
|
||||
return this.$store.state.instance.emoji.filter(emoji => emoji.displayText.includes(this.filterWord))
|
||||
const filterWordLowercase = this.filterWord.toLowerCase()
|
||||
return this.$store.state.instance.emoji.filter(emoji =>
|
||||
emoji.displayText.toLowerCase().includes(filterWordLowercase)
|
||||
)
|
||||
}
|
||||
return this.$store.state.instance.emoji || []
|
||||
},
|
||||
|
|
|
@ -53,16 +53,6 @@
|
|||
</small>
|
||||
</Checkbox>
|
||||
</li>
|
||||
<li>
|
||||
<Checkbox v-model="autoLoad">
|
||||
{{ $t('settings.autoload') }}
|
||||
</Checkbox>
|
||||
</li>
|
||||
<li>
|
||||
<Checkbox v-model="hoverPreview">
|
||||
{{ $t('settings.reply_link_preview') }}
|
||||
</Checkbox>
|
||||
</li>
|
||||
<li>
|
||||
<Checkbox v-model="emojiReactionsOnTimeline">
|
||||
{{ $t('settings.emoji_reactions_on_timeline') }}
|
||||
|
|
|
@ -2,38 +2,18 @@
|
|||
<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">
|
||||
<li>
|
||||
<Checkbox v-model="notificationSettings.follows">
|
||||
{{ $t('settings.notification_setting_follows') }}
|
||||
</Checkbox>
|
||||
</li>
|
||||
<li>
|
||||
<Checkbox v-model="notificationSettings.followers">
|
||||
{{ $t('settings.notification_setting_followers') }}
|
||||
</Checkbox>
|
||||
</li>
|
||||
<li>
|
||||
<Checkbox v-model="notificationSettings.non_follows">
|
||||
{{ $t('settings.notification_setting_non_follows') }}
|
||||
</Checkbox>
|
||||
</li>
|
||||
<li>
|
||||
<Checkbox v-model="notificationSettings.non_followers">
|
||||
{{ $t('settings.notification_setting_non_followers') }}
|
||||
</Checkbox>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<p>
|
||||
<Checkbox v-model="notificationSettings.block_from_strangers">
|
||||
{{ $t('settings.notification_setting_block_from_strangers') }}
|
||||
</Checkbox>
|
||||
</p>
|
||||
</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 v-model="notificationSettings.hide_notification_contents">
|
||||
{{ $t('settings.notification_setting_hide_notification_contents') }}
|
||||
</Checkbox>
|
||||
</p>
|
||||
</div>
|
||||
|
|
|
@ -9,6 +9,7 @@ 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 UserListPopover from '../user_list_popover/user_list_popover.vue'
|
||||
import EmojiReactions from '../emoji_reactions/emoji_reactions.vue'
|
||||
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
|
||||
import { highlightClass, highlightStyle } from '../../services/user_highlighter/user_highlighter.js'
|
||||
|
@ -18,6 +19,21 @@ import { mapGetters, mapState } from 'vuex'
|
|||
|
||||
const Status = {
|
||||
name: 'Status',
|
||||
components: {
|
||||
FavoriteButton,
|
||||
ReactButton,
|
||||
RetweetButton,
|
||||
ExtraButtons,
|
||||
PostStatusForm,
|
||||
UserCard,
|
||||
UserAvatar,
|
||||
AvatarList,
|
||||
Timeago,
|
||||
StatusPopover,
|
||||
UserListPopover,
|
||||
EmojiReactions,
|
||||
StatusContent
|
||||
},
|
||||
props: [
|
||||
'statusoid',
|
||||
'expandable',
|
||||
|
@ -197,20 +213,6 @@ const Status = {
|
|||
currentUser: state => state.users.currentUser
|
||||
})
|
||||
},
|
||||
components: {
|
||||
FavoriteButton,
|
||||
ReactButton,
|
||||
RetweetButton,
|
||||
ExtraButtons,
|
||||
PostStatusForm,
|
||||
UserCard,
|
||||
UserAvatar,
|
||||
AvatarList,
|
||||
Timeago,
|
||||
StatusPopover,
|
||||
EmojiReactions,
|
||||
StatusContent
|
||||
},
|
||||
methods: {
|
||||
visibilityIcon (visibility) {
|
||||
switch (visibility) {
|
||||
|
|
|
@ -72,7 +72,10 @@
|
|||
:user="statusoid.user"
|
||||
/>
|
||||
<div class="media-body faint">
|
||||
<span class="user-name">
|
||||
<span
|
||||
class="user-name"
|
||||
:title="retweeter"
|
||||
>
|
||||
<router-link
|
||||
v-if="retweeterHtml"
|
||||
:to="retweeterProfileLink"
|
||||
|
@ -129,20 +132,28 @@
|
|||
<h4
|
||||
v-if="status.user.name_html"
|
||||
class="user-name"
|
||||
:title="status.user.name"
|
||||
v-html="status.user.name_html"
|
||||
/>
|
||||
<h4
|
||||
v-else
|
||||
class="user-name"
|
||||
:title="status.user.name"
|
||||
>
|
||||
{{ status.user.name }}
|
||||
</h4>
|
||||
<router-link
|
||||
class="account-name"
|
||||
:title="status.user.screen_name"
|
||||
:to="userProfileLink"
|
||||
>
|
||||
{{ status.user.screen_name }}
|
||||
</router-link>
|
||||
<img
|
||||
class="status-favicon"
|
||||
v-if="!!(status.user && status.user.favicon)"
|
||||
:src="status.user.favicon"
|
||||
>
|
||||
</div>
|
||||
|
||||
<span class="heading-right">
|
||||
|
@ -222,7 +233,10 @@
|
|||
>
|
||||
<span class="reply-to-text">{{ $t('status.reply_to') }}</span>
|
||||
</span>
|
||||
<router-link :to="replyProfileLink">
|
||||
<router-link
|
||||
:title="replyToName"
|
||||
:to="replyProfileLink"
|
||||
>
|
||||
{{ replyToName }}
|
||||
</router-link>
|
||||
<span
|
||||
|
@ -265,24 +279,30 @@
|
|||
class="favs-repeated-users"
|
||||
>
|
||||
<div class="stats">
|
||||
<div
|
||||
<UserListPopover
|
||||
v-if="statusFromGlobalRepository.rebloggedBy && statusFromGlobalRepository.rebloggedBy.length > 0"
|
||||
class="stat-count"
|
||||
:users="statusFromGlobalRepository.rebloggedBy"
|
||||
>
|
||||
<a class="stat-title">{{ $t('status.repeats') }}</a>
|
||||
<div class="stat-number">
|
||||
{{ statusFromGlobalRepository.rebloggedBy.length }}
|
||||
<div class="stat-count">
|
||||
<a class="stat-title">{{ $t('status.repeats') }}</a>
|
||||
<div class="stat-number">
|
||||
{{ statusFromGlobalRepository.rebloggedBy.length }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
</UserListPopover>
|
||||
<UserListPopover
|
||||
v-if="statusFromGlobalRepository.favoritedBy && statusFromGlobalRepository.favoritedBy.length > 0"
|
||||
class="stat-count"
|
||||
:users="statusFromGlobalRepository.favoritedBy"
|
||||
>
|
||||
<a class="stat-title">{{ $t('status.favorites') }}</a>
|
||||
<div class="stat-number">
|
||||
{{ statusFromGlobalRepository.favoritedBy.length }}
|
||||
<div
|
||||
class="stat-count"
|
||||
>
|
||||
<a class="stat-title">{{ $t('status.favorites') }}</a>
|
||||
<div class="stat-number">
|
||||
{{ statusFromGlobalRepository.favoritedBy.length }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</UserListPopover>
|
||||
<div class="avatar-row">
|
||||
<AvatarList :users="combinedFavsAndRepeatsUsers" />
|
||||
</div>
|
||||
|
@ -428,6 +448,12 @@ $status-margin: 0.75em;
|
|||
}
|
||||
}
|
||||
|
||||
.status-favicon {
|
||||
height: 18px;
|
||||
width: 18px;
|
||||
margin-right: 0.4em;
|
||||
}
|
||||
|
||||
.media-heading {
|
||||
padding: 0;
|
||||
vertical-align: bottom;
|
||||
|
@ -722,6 +748,11 @@ $status-margin: 0.75em;
|
|||
|
||||
.stat-count {
|
||||
margin-right: $status-margin;
|
||||
user-select: none;
|
||||
|
||||
&:hover .stat-title {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.stat-title {
|
||||
color: var(--faint, $fallback--faint);
|
||||
|
|
|
@ -148,7 +148,6 @@ const Timeline = {
|
|||
const bodyBRect = document.body.getBoundingClientRect()
|
||||
const height = Math.max(bodyBRect.height, -(bodyBRect.y))
|
||||
if (this.timeline.loading === false &&
|
||||
this.$store.getters.mergedConfig.autoLoad &&
|
||||
this.$el.offsetHeight > 0 &&
|
||||
(window.innerHeight + window.pageYOffset) >= (height - 750)) {
|
||||
this.fetchOlderStatuses()
|
||||
|
|
|
@ -66,6 +66,7 @@
|
|||
<div class="bottom-line">
|
||||
<router-link
|
||||
class="user-screen-name"
|
||||
:title="user.screen_name"
|
||||
:to="userProfileLink(user)"
|
||||
>
|
||||
@{{ user.screen_name }}
|
||||
|
|
18
src/components/user_list_popover/user_list_popover.js
Normal file
18
src/components/user_list_popover/user_list_popover.js
Normal file
|
@ -0,0 +1,18 @@
|
|||
|
||||
const UserListPopover = {
|
||||
name: 'UserListPopover',
|
||||
props: [
|
||||
'users'
|
||||
],
|
||||
components: {
|
||||
Popover: () => import('../popover/popover.vue'),
|
||||
UserAvatar: () => import('../user_avatar/user_avatar.vue')
|
||||
},
|
||||
computed: {
|
||||
usersCapped () {
|
||||
return this.users.slice(0, 16)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default UserListPopover
|
71
src/components/user_list_popover/user_list_popover.vue
Normal file
71
src/components/user_list_popover/user_list_popover.vue
Normal file
|
@ -0,0 +1,71 @@
|
|||
<template>
|
||||
<Popover
|
||||
trigger="hover"
|
||||
placement="top"
|
||||
:offset="{ y: 5 }"
|
||||
>
|
||||
<template slot="trigger">
|
||||
<slot />
|
||||
</template>
|
||||
<div
|
||||
slot="content"
|
||||
class="user-list-popover"
|
||||
>
|
||||
<div v-if="users.length">
|
||||
<div
|
||||
v-for="(user) in usersCapped"
|
||||
:key="user.id"
|
||||
class="user-list-row"
|
||||
>
|
||||
<UserAvatar
|
||||
:user="user"
|
||||
class="avatar-small"
|
||||
:compact="true"
|
||||
/>
|
||||
<div class="user-list-names">
|
||||
<!-- eslint-disable vue/no-v-html -->
|
||||
<span v-html="user.name_html" />
|
||||
<!-- eslint-enable vue/no-v-html -->
|
||||
<span class="user-list-screen-name">{{ user.screen_name }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else>
|
||||
<i class="icon-spin4 animate-spin" />
|
||||
</div>
|
||||
</div>
|
||||
</Popover>
|
||||
</template>
|
||||
|
||||
<script src="./user_list_popover.js" ></script>
|
||||
|
||||
<style lang="scss">
|
||||
@import '../../_variables.scss';
|
||||
|
||||
.user-list-popover {
|
||||
padding: 0.5em;
|
||||
|
||||
.user-list-row {
|
||||
padding: 0.25em;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
.user-list-names {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-left: 0.5em;
|
||||
min-width: 5em;
|
||||
|
||||
img {
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
}
|
||||
}
|
||||
|
||||
.user-list-screen-name {
|
||||
font-size: 9px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
Loading…
Add table
Add a link
Reference in a new issue