Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F85629455
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Award Token
Flag For Later
Size
38 KB
Referenced Files
None
Subscribers
None
View Options
diff --git a/src/components/emoji_reactions/emoji_reactions.js b/src/components/emoji_reactions/emoji_reactions.js
index ae7f53be5a..bb11b840f3 100644
--- a/src/components/emoji_reactions/emoji_reactions.js
+++ b/src/components/emoji_reactions/emoji_reactions.js
@@ -1,69 +1,69 @@
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
const EmojiReactions = {
name: 'EmojiReactions',
components: {
UserAvatar,
- Popover
+ UserListPopover
},
props: ['status'],
data: () => ({
showAll: false
}),
computed: {
tooManyReactions () {
return this.status.emoji_reactions.length > EMOJI_REACTION_COUNT_CUTOFF
},
emojiReactions () {
return this.showAll
? this.status.emoji_reactions
: this.status.emoji_reactions.slice(0, EMOJI_REACTION_COUNT_CUTOFF)
},
showMoreString () {
return `+${this.status.emoji_reactions.length - EMOJI_REACTION_COUNT_CUTOFF}`
},
accountsForEmoji () {
return this.status.emoji_reactions.reduce((acc, reaction) => {
acc[reaction.name] = reaction.accounts || []
return acc
}, {})
},
loggedIn () {
return !!this.$store.state.users.currentUser
}
},
methods: {
toggleShowAll () {
this.showAll = !this.showAll
},
reactedWith (emoji) {
return this.status.emoji_reactions.find(r => r.name === emoji).me
},
fetchEmojiReactionsByIfMissing () {
const hasNoAccounts = this.status.emoji_reactions.find(r => !r.accounts)
if (hasNoAccounts) {
this.$store.dispatch('fetchEmojiReactionsBy', this.status.id)
}
},
reactWith (emoji) {
this.$store.dispatch('reactWithEmoji', { id: this.status.id, emoji })
},
unreact (emoji) {
this.$store.dispatch('unreactWithEmoji', { id: this.status.id, emoji })
},
emojiOnClick (emoji, event) {
if (!this.loggedIn) return
if (this.reactedWith(emoji)) {
this.unreact(emoji)
} else {
this.reactWith(emoji)
}
}
}
}
export default EmojiReactions
diff --git a/src/components/emoji_reactions/emoji_reactions.vue b/src/components/emoji_reactions/emoji_reactions.vue
index bac4c6051b..2f14b5b2e2 100644
--- a/src/components/emoji_reactions/emoji_reactions.vue
+++ b/src/components/emoji_reactions/emoji_reactions.vue
@@ -1,141 +1,82 @@
<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)"
@mouseenter="fetchEmojiReactionsByIfMissing()"
>
<span class="reaction-emoji">{{ reaction.name }}</span>
<span>{{ reaction.count }}</span>
</button>
- </Popover>
+ </UserListPopover>
<a
v-if="tooManyReactions"
class="emoji-reaction-expand faint"
href="javascript:void(0)"
@click="toggleShowAll"
>
{{ showAll ? $t('general.show_less') : showMoreString }}
</a>
</div>
</template>
<script src="./emoji_reactions.js" ></script>
<style lang="scss">
@import '../../_variables.scss';
.emoji-reactions {
display: flex;
margin-top: 0.25em;
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;
margin-top: 0.5em;
display: flex;
align-items: center;
justify-content: center;
box-sizing: border-box;
.reaction-emoji {
width: 1.25em;
margin-right: 0.25em;
}
&:focus {
outline: none;
}
&.not-clickable {
cursor: default;
&:hover {
box-shadow: $fallback--buttonShadow;
box-shadow: var(--buttonShadow);
}
}
}
.emoji-reaction-expand {
padding: 0 0.5em;
margin-right: 0.5em;
margin-top: 0.5em;
display: flex;
align-items: center;
justify-content: center;
&:hover {
text-decoration: underline;
}
}
.picked-reaction {
border: 1px solid var(--accent, $fallback--link);
margin-left: -1px; // offset the border, can't use inset shadows either
margin-right: calc(0.5em - 1px);
}
</style>
diff --git a/src/components/status/status.js b/src/components/status/status.js
index ad0b72a9ab..d263da682e 100644
--- a/src/components/status/status.js
+++ b/src/components/status/status.js
@@ -1,290 +1,292 @@
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 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 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'
import { muteWordHits } from '../../services/status_parser/status_parser.js'
import { unescape, uniqBy } from 'lodash'
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',
'inConversation',
'focused',
'highlight',
'compact',
'replies',
'isPreview',
'noHeading',
'inlineExpanded',
'showPinned',
'inProfile',
'profileUserId'
],
data () {
return {
replying: false,
unmuted: false,
userExpanded: false,
error: null
}
},
computed: {
muteWords () {
return this.mergedConfig.muteWords
},
showReasonMutedThread () {
return (
this.status.thread_muted ||
(this.status.reblog && this.status.reblog.thread_muted)
) && !this.inConversation
},
repeaterClass () {
const user = this.statusoid.user
return highlightClass(user)
},
userClass () {
const user = this.retweet ? (this.statusoid.retweeted_status.user) : this.statusoid.user
return highlightClass(user)
},
deleted () {
return this.statusoid.deleted
},
repeaterStyle () {
const user = this.statusoid.user
const highlight = this.mergedConfig.highlight
return highlightStyle(highlight[user.screen_name])
},
userStyle () {
if (this.noHeading) return
const user = this.retweet ? (this.statusoid.retweeted_status.user) : this.statusoid.user
const highlight = this.mergedConfig.highlight
return highlightStyle(highlight[user.screen_name])
},
userProfileLink () {
return this.generateUserProfileLink(this.status.user.id, this.status.user.screen_name)
},
replyProfileLink () {
if (this.isReply) {
return this.generateUserProfileLink(this.status.in_reply_to_user_id, this.replyToName)
}
},
retweet () { return !!this.statusoid.retweeted_status },
retweeter () { return this.statusoid.user.name || this.statusoid.user.screen_name },
retweeterHtml () { return this.statusoid.user.name_html },
retweeterProfileLink () { return this.generateUserProfileLink(this.statusoid.user.id, this.statusoid.user.screen_name) },
status () {
if (this.retweet) {
return this.statusoid.retweeted_status
} else {
return this.statusoid
}
},
statusFromGlobalRepository () {
// NOTE: Consider to replace status with statusFromGlobalRepository
return this.$store.state.statuses.allStatusesObject[this.status.id]
},
loggedIn () {
return !!this.currentUser
},
muteWordHits () {
return muteWordHits(this.status, this.muteWords)
},
muted () {
const { status } = this
const { reblog } = status
const relationship = this.$store.getters.relationship(status.user.id)
const relationshipReblog = reblog && this.$store.getters.relationship(reblog.user.id)
const reasonsToMute = (
// Post is muted according to BE
status.muted ||
// Reprööt of a muted post according to BE
(reblog && reblog.muted) ||
// Muted user
relationship.muting ||
// Muted user of a reprööt
(relationshipReblog && relationshipReblog.muting) ||
// Thread is muted
status.thread_muted ||
// Wordfiltered
this.muteWordHits.length > 0
)
const excusesNotToMute = (
(
this.inProfile && (
// Don't mute user's posts on user timeline (except reblogs)
(!reblog && status.user.id === this.profileUserId) ||
// Same as above but also allow self-reblogs
(reblog && reblog.user.id === this.profileUserId)
)
) ||
// Don't mute statuses in muted conversation when said conversation is opened
(this.inConversation && status.thread_muted)
// No excuses if post has muted words
) && !this.muteWordHits.length > 0
return !this.unmuted && !excusesNotToMute && reasonsToMute
},
hideFilteredStatuses () {
return this.mergedConfig.hideFilteredStatuses
},
hideStatus () {
return this.deleted || (this.muted && this.hideFilteredStatuses)
},
isFocused () {
// retweet or root of an expanded conversation
if (this.focused) {
return true
} else if (!this.inConversation) {
return false
}
// use conversation highlight only when in conversation
return this.status.id === this.highlight
},
isReply () {
return !!(this.status.in_reply_to_status_id && this.status.in_reply_to_user_id)
},
replyToName () {
if (this.status.in_reply_to_screen_name) {
return this.status.in_reply_to_screen_name
} else {
const user = this.$store.getters.findUser(this.status.in_reply_to_user_id)
return user && user.screen_name
}
},
replySubject () {
if (!this.status.summary) return ''
const decodedSummary = unescape(this.status.summary)
const behavior = this.mergedConfig.subjectLineBehavior
const startsWithRe = decodedSummary.match(/^re[: ]/i)
if ((behavior !== 'noop' && startsWithRe) || behavior === 'masto') {
return decodedSummary
} else if (behavior === 'email') {
return 're: '.concat(decodedSummary)
} else if (behavior === 'noop') {
return ''
}
},
combinedFavsAndRepeatsUsers () {
// Use the status from the global status repository since favs and repeats are saved in it
const combinedUsers = [].concat(
this.statusFromGlobalRepository.favoritedBy,
this.statusFromGlobalRepository.rebloggedBy
)
return uniqBy(combinedUsers, 'id')
},
tags () {
return this.status.tags.filter(tagObj => tagObj.hasOwnProperty('name')).map(tagObj => tagObj.name).join(' ')
},
hidePostStats () {
return this.mergedConfig.hidePostStats
},
...mapGetters(['mergedConfig']),
...mapState({
betterShadow: state => state.interface.browserSupport.cssFilter,
currentUser: state => state.users.currentUser
})
},
- components: {
- FavoriteButton,
- ReactButton,
- RetweetButton,
- ExtraButtons,
- PostStatusForm,
- UserCard,
- UserAvatar,
- AvatarList,
- Timeago,
- StatusPopover,
- EmojiReactions,
- StatusContent
- },
methods: {
visibilityIcon (visibility) {
switch (visibility) {
case 'private':
return 'icon-lock'
case 'unlisted':
return 'icon-lock-open-alt'
case 'direct':
return 'icon-mail-alt'
default:
return 'icon-globe'
}
},
showError (error) {
this.error = error
},
clearError () {
this.error = undefined
},
toggleReplying () {
this.replying = !this.replying
},
gotoOriginal (id) {
if (this.inConversation) {
this.$emit('goto', id)
}
},
toggleExpanded () {
this.$emit('toggleExpanded')
},
toggleMute () {
this.unmuted = !this.unmuted
},
toggleUserExpanded () {
this.userExpanded = !this.userExpanded
},
generateUserProfileLink (id, name) {
return generateProfileLink(id, name, this.$store.state.instance.restrictedNicknames)
}
},
watch: {
'highlight': function (id) {
if (this.status.id === id) {
let rect = this.$el.getBoundingClientRect()
if (rect.top < 100) {
// Post is above screen, match its top to screen top
window.scrollBy(0, rect.top - 100)
} else if (rect.height >= (window.innerHeight - 50)) {
// Post we want to see is taller than screen so match its top to screen top
window.scrollBy(0, rect.top - 100)
} else if (rect.bottom > window.innerHeight - 50) {
// Post is below screen, match its bottom to screen bottom
window.scrollBy(0, rect.bottom - window.innerHeight + 50)
}
}
},
'status.repeat_num': function (num) {
// refetch repeats when repeat_num is changed in any way
if (this.isFocused && this.statusFromGlobalRepository.rebloggedBy && this.statusFromGlobalRepository.rebloggedBy.length !== num) {
this.$store.dispatch('fetchRepeats', this.status.id)
}
},
'status.fave_num': function (num) {
// refetch favs when fave_num is changed in any way
if (this.isFocused && this.statusFromGlobalRepository.favoritedBy && this.statusFromGlobalRepository.favoritedBy.length !== num) {
this.$store.dispatch('fetchFavs', this.status.id)
}
}
},
filters: {
capitalize: function (str) {
return str.charAt(0).toUpperCase() + str.slice(1)
}
}
}
export default Status
diff --git a/src/components/status/status.vue b/src/components/status/status.vue
index f6b5dd6f48..4764f46714 100644
--- a/src/components/status/status.vue
+++ b/src/components/status/status.vue
@@ -1,782 +1,793 @@
<template>
<!-- eslint-disable vue/no-v-html -->
<div
v-if="!hideStatus"
class="status-el"
:class="[{ 'status-el_focused': isFocused }, { 'status-conversation': inlineExpanded }]"
>
<div
v-if="error"
class="alert error"
>
{{ error }}
<i
class="button-icon icon-cancel"
@click="clearError"
/>
</div>
<template v-if="muted && !isPreview">
<div class="media status container muted">
<small class="username">
<i
v-if="muted && retweet"
class="button-icon icon-retweet"
/>
<router-link :to="userProfileLink">
{{ status.user.screen_name }}
</router-link>
</small>
<small
v-if="showReasonMutedThread"
class="mute-thread"
>
{{ $t('status.thread_muted') }}
</small>
<small
v-if="showReasonMutedThread && muteWordHits.length > 0"
class="mute-thread"
>
{{ $t('status.thread_muted_and_words') }}
</small>
<small
class="mute-words"
:title="muteWordHits.join(', ')"
>
{{ muteWordHits.join(', ') }}
</small>
<a
href="#"
class="unmute"
@click.prevent="toggleMute"
><i class="button-icon icon-eye-off" /></a>
</div>
</template>
<template v-else>
<div
v-if="showPinned"
class="status-pin"
>
<i class="fa icon-pin faint" />
<span class="faint">{{ $t('status.pinned') }}</span>
</div>
<div
v-if="retweet && !noHeading && !inConversation"
:class="[repeaterClass, { highlighted: repeaterStyle }]"
:style="[repeaterStyle]"
class="media container retweet-info"
>
<UserAvatar
v-if="retweet"
class="media-left"
:better-shadow="betterShadow"
:user="statusoid.user"
/>
<div class="media-body faint">
<span class="user-name">
<router-link
v-if="retweeterHtml"
:to="retweeterProfileLink"
v-html="retweeterHtml"
/>
<router-link
v-else
:to="retweeterProfileLink"
>{{ retweeter }}</router-link>
</span>
<i
class="fa icon-retweet retweeted"
:title="$t('tool_tip.repeat')"
/>
{{ $t('timeline.repeated') }}
</div>
</div>
<div
:class="[userClass, { highlighted: userStyle, 'is-retweet': retweet && !inConversation }]"
:style="[ userStyle ]"
class="media status"
:data-tags="tags"
>
<div
v-if="!noHeading"
class="media-left"
>
<router-link
:to="userProfileLink"
@click.stop.prevent.capture.native="toggleUserExpanded"
>
<UserAvatar
:compact="compact"
:better-shadow="betterShadow"
:user="status.user"
/>
</router-link>
</div>
<div class="status-body">
<UserCard
v-if="userExpanded"
:user-id="status.user.id"
:rounded="true"
:bordered="true"
class="status-usercard"
/>
<div
v-if="!noHeading"
class="media-heading"
>
<div class="heading-name-row">
<div class="name-and-account-name">
<h4
v-if="status.user.name_html"
class="user-name"
v-html="status.user.name_html"
/>
<h4
v-else
class="user-name"
>
{{ status.user.name }}
</h4>
<router-link
class="account-name"
:to="userProfileLink"
>
{{ status.user.screen_name }}
</router-link>
</div>
<span class="heading-right">
<router-link
class="timeago faint-link"
:to="{ name: 'conversation', params: { id: status.id } }"
>
<Timeago
:time="status.created_at"
:auto-update="60"
/>
</router-link>
<div
v-if="status.visibility"
class="button-icon visibility-icon"
>
<i
:class="visibilityIcon(status.visibility)"
:title="status.visibility | capitalize"
/>
</div>
<a
v-if="!status.is_local && !isPreview"
:href="status.external_url"
target="_blank"
class="source_url"
title="Source"
>
<i class="button-icon icon-link-ext-alt" />
</a>
<template v-if="expandable && !isPreview">
<a
href="#"
title="Expand"
@click.prevent="toggleExpanded"
>
<i class="button-icon icon-plus-squared" />
</a>
</template>
<a
v-if="unmuted"
href="#"
@click.prevent="toggleMute"
><i class="button-icon icon-eye-off" /></a>
</span>
</div>
<div class="heading-reply-row">
<div
v-if="isReply"
class="reply-to-and-accountname"
>
<StatusPopover
v-if="!isPreview"
:status-id="status.parent_visible && status.in_reply_to_status_id"
class="reply-to-popover"
style="min-width: 0"
>
<a
class="reply-to"
href="#"
:aria-label="$t('tool_tip.reply')"
@click.prevent="gotoOriginal(status.in_reply_to_status_id)"
>
<i class="button-icon icon-reply" />
<span
class="faint-link reply-to-text"
:class="{ 'strikethrough': !status.parent_visible }"
>
{{ $t('status.reply_to') }}
</span>
</a>
</StatusPopover>
<span
v-else
class="reply-to"
>
<span class="reply-to-text">{{ $t('status.reply_to') }}</span>
</span>
<router-link :to="replyProfileLink">
{{ replyToName }}
</router-link>
<span
v-if="replies && replies.length"
class="faint replies-separator"
>
-
</span>
</div>
<div
v-if="inConversation && !isPreview && replies && replies.length"
class="replies"
>
<span class="faint">{{ $t('status.replies_list') }}</span>
<StatusPopover
v-for="reply in replies"
:key="reply.id"
:status-id="reply.id"
>
<a
href="#"
class="reply-link"
@click.prevent="gotoOriginal(reply.id)"
>{{ reply.name }}</a>
</StatusPopover>
</div>
</div>
</div>
<StatusContent
:status="status"
:no-heading="noHeading"
:highlight="highlight"
:focused="isFocused"
/>
<transition name="fade">
<div
v-if="!hidePostStats && isFocused && combinedFavsAndRepeatsUsers.length > 0"
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>
</div>
</div>
</transition>
<EmojiReactions
v-if="(mergedConfig.emojiReactionsOnTimeline || isFocused) && (!noHeading && !isPreview)"
:status="status"
/>
<div
v-if="!noHeading && !isPreview"
class="status-actions media-body"
>
<div>
<i
v-if="loggedIn"
class="button-icon icon-reply"
:title="$t('tool_tip.reply')"
:class="{'button-icon-active': replying}"
@click.prevent="toggleReplying"
/>
<i
v-else
class="button-icon button-icon-disabled icon-reply"
:title="$t('tool_tip.reply')"
/>
<span v-if="status.replies_count > 0">{{ status.replies_count }}</span>
</div>
<retweet-button
:visibility="status.visibility"
:logged-in="loggedIn"
:status="status"
/>
<favorite-button
:logged-in="loggedIn"
:status="status"
/>
<ReactButton
v-if="loggedIn"
:status="status"
/>
<extra-buttons
:status="status"
@onError="showError"
@onSuccess="clearError"
/>
</div>
</div>
</div>
<div
v-if="replying"
class="container"
>
<PostStatusForm
class="reply-body"
:reply-to="status.id"
:attentions="status.attentions"
:replied-user="status.user"
:copy-message-scope="status.visibility"
:subject="replySubject"
@posted="toggleReplying"
/>
</div>
</template>
</div>
<!-- eslint-enable vue/no-v-html -->
</template>
<script src="./status.js" ></script>
<style lang="scss">
@import '../../_variables.scss';
$status-margin: 0.75em;
.status-body {
flex: 1;
min-width: 0;
}
.status-pin {
padding: $status-margin $status-margin 0;
display: flex;
align-items: center;
justify-content: flex-end;
}
.media-left {
margin-right: $status-margin;
}
.status-el {
border-left-width: 0px;
min-width: 0;
border-color: $fallback--border;
border-color: var(--border, $fallback--border);
border-left: 4px $fallback--cRed;
border-left: 4px var(--cRed, $fallback--cRed);
&_focused {
background-color: $fallback--lightBg;
background-color: var(--selectedPost, $fallback--lightBg);
color: $fallback--text;
color: var(--selectedPostText, $fallback--text);
--lightText: var(--selectedPostLightText, $fallback--light);
--faint: var(--selectedPostFaintText, $fallback--faint);
--faintLink: var(--selectedPostFaintLink, $fallback--faint);
--postLink: var(--selectedPostPostLink, $fallback--faint);
--postFaintLink: var(--selectedPostFaintPostLink, $fallback--faint);
--icon: var(--selectedPostIcon, $fallback--icon);
}
.timeline & {
border-bottom-width: 1px;
border-bottom-style: solid;
}
.media-body {
flex: 1;
padding: 0;
}
.status-usercard {
margin-bottom: $status-margin;
}
.user-name {
white-space: nowrap;
font-size: 14px;
overflow: hidden;
flex-shrink: 0;
max-width: 85%;
font-weight: bold;
img.emoji {
width: 14px;
height: 14px;
vertical-align: middle;
object-fit: contain
}
}
.media-heading {
padding: 0;
vertical-align: bottom;
flex-basis: 100%;
margin-bottom: 0.5em;
small {
font-weight: lighter;
}
.heading-name-row {
padding: 0;
display: flex;
justify-content: space-between;
line-height: 18px;
a {
display: inline-block;
word-break: break-all;
}
.name-and-account-name {
display: flex;
min-width: 0;
}
.user-name {
flex-shrink: 1;
margin-right: 0.4em;
overflow: hidden;
text-overflow: ellipsis;
}
.account-name {
min-width: 1.6em;
margin-right: 0.4em;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
flex: 1 1 0;
}
}
.heading-right {
display: flex;
flex-shrink: 0;
}
.timeago {
margin-right: 0.2em;
}
.heading-reply-row {
position: relative;
align-content: baseline;
font-size: 12px;
line-height: 18px;
max-width: 100%;
display: flex;
flex-wrap: wrap;
align-items: stretch;
> .reply-to-and-accountname > a {
overflow: hidden;
max-width: 100%;
text-overflow: ellipsis;
white-space: nowrap;
word-break: break-all;
}
}
.reply-to-and-accountname {
display: flex;
height: 18px;
margin-right: 0.5em;
max-width: 100%;
.icon-reply {
transform: scaleX(-1);
}
}
.reply-info {
display: flex;
}
.reply-to-popover {
min-width: 0;
}
.reply-to {
display: flex;
}
.reply-to-text {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
margin: 0 0.4em 0 0.2em;
}
.strikethrough {
text-decoration: line-through;
}
.replies-separator {
margin-left: 0.4em;
}
.replies {
line-height: 18px;
font-size: 12px;
display: flex;
flex-wrap: wrap;
& > * {
margin-right: 0.4em;
}
}
.reply-link {
height: 17px;
}
}
.retweet-info {
padding: 0.4em $status-margin;
margin: 0;
.avatar.still-image {
border-radius: $fallback--avatarAltRadius;
border-radius: var(--avatarAltRadius, $fallback--avatarAltRadius);
margin-left: 28px;
width: 20px;
height: 20px;
}
.media-body {
font-size: 1em;
line-height: 22px;
display: flex;
align-content: center;
flex-wrap: wrap;
.user-name {
font-weight: bold;
overflow: hidden;
text-overflow: ellipsis;
img {
width: 14px;
height: 14px;
vertical-align: middle;
object-fit: contain
}
}
i {
padding: 0 0.2em;
}
a {
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
}
}
.status-fadein {
animation-duration: 0.4s;
animation-name: fadein;
}
@keyframes fadein {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
.status-conversation {
border-left-style: solid;
}
.status-actions {
position: relative;
width: 100%;
display: flex;
margin-top: $status-margin;
> * {
max-width: 4em;
flex: 1;
}
}
.button-icon.icon-reply {
&:not(.button-icon-disabled):hover,
&.button-icon-active {
color: $fallback--cBlue;
color: var(--cBlue, $fallback--cBlue);
}
}
.button-icon.icon-reply {
&:not(.button-icon-disabled) {
cursor: pointer;
}
}
.status:hover .animated.avatar {
canvas {
display: none;
}
img {
visibility: visible;
}
}
.status {
display: flex;
padding: $status-margin;
&.is-retweet {
padding-top: 0;
}
}
.status-conversation:last-child {
border-bottom: none;
}
.muted {
padding: .25em .6em;
height: 1.2em;
line-height: 1.2em;
text-overflow: ellipsis;
overflow: hidden;
display: flex;
flex-wrap: nowrap;
.username, .mute-thread, .mute-words {
word-wrap: normal;
word-break: normal;
white-space: nowrap;
}
.username, .mute-words {
text-overflow: ellipsis;
overflow: hidden;
}
.username {
flex: 0 1 auto;
margin-right: .2em;
}
.mute-thread {
flex: 0 0 auto;
}
.mute-words {
flex: 1 0 5em;
margin-left: .2em;
&::before {
content: ' '
}
}
.unmute {
flex: 0 0 auto;
margin-left: auto;
display: block;
margin-left: auto;
}
}
.reply-body {
flex: 1;
}
.favs-repeated-users {
margin-top: $status-margin;
.stats {
width: 100%;
display: flex;
line-height: 1em;
.stat-count {
margin-right: $status-margin;
+ user-select: none;
+
+ &:hover .stat-title {
+ text-decoration: underline;
+ }
.stat-title {
color: var(--faint, $fallback--faint);
font-size: 12px;
text-transform: uppercase;
position: relative;
}
.stat-number {
font-weight: bolder;
font-size: 16px;
line-height: 1em;
}
}
.avatar-row {
flex: 1;
overflow: hidden;
position: relative;
display: flex;
align-items: center;
&::before {
content: '';
position: absolute;
height: 100%;
width: 1px;
left: 0;
background-color: var(--faint, $fallback--faint);
}
}
}
}
@media all and (max-width: 800px) {
.status-el {
.retweet-info {
.avatar.still-image {
margin-left: 20px;
}
}
}
.status {
max-width: 100%;
}
.status .avatar.still-image {
width: 40px;
height: 40px;
&.avatar-compact {
width: 32px;
height: 32px;
}
}
}
</style>
diff --git a/src/components/user_list_popover/user_list_popover.js b/src/components/user_list_popover/user_list_popover.js
new file mode 100644
index 0000000000..26d2ed1aee
--- /dev/null
+++ b/src/components/user_list_popover/user_list_popover.js
@@ -0,0 +1,13 @@
+
+const UserListPopover = {
+ name: 'UserListPopover',
+ props: [
+ 'users'
+ ],
+ components: {
+ Popover: () => import('../popover/popover.vue'),
+ UserAvatar: () => import('../user_avatar/user_avatar.vue')
+ }
+}
+
+export default UserListPopover
diff --git a/src/components/user_list_popover/user_list_popover.vue b/src/components/user_list_popover/user_list_popover.vue
new file mode 100644
index 0000000000..b6b7ab95ab
--- /dev/null
+++ b/src/components/user_list_popover/user_list_popover.vue
@@ -0,0 +1,74 @@
+<template>
+ <Popover
+ trigger="hover"
+ placement="top"
+ :offset="{ y: 5 }"
+ >
+ <template slot="trigger">
+ <slot />
+ </template>
+ <div
+ slot="content"
+ class="reacted-users"
+ >
+ <div v-if="users.length">
+ <div
+ v-for="(user) in users"
+ :key="user.id"
+ class="reacted-user"
+ >
+ <UserAvatar
+ :user="user"
+ class="avatar-small"
+ :compact="true"
+ />
+ <div class="reacted-user-names">
+ <!-- eslint-disable vue/no-v-html -->
+ <span
+ class="reacted-user-name"
+ v-html="user.name_html"
+ />
+ <!-- eslint-enable vue/no-v-html -->
+ <span class="reacted-user-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';
+
+.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;
+ }
+}
+
+</style>
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Sat, Aug 8, 9:36 PM (1 d, 19 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1723990
Default Alt Text
(38 KB)
Attached To
Mode
rPUFE pleroma-fe-upstream
Attached
Detach File
Event Timeline
Log In to Comment