Compare commits
17 commits
develop
...
feat/user-
Author | SHA1 | Date | |
---|---|---|---|
|
72934c1f91 | ||
|
b3254c99d1 | ||
|
6b9ce950c8 | ||
|
fd0d4fbfcf | ||
|
35c459f412 | ||
|
2031f3822d | ||
|
14708ce433 | ||
|
5edb7e76ff | ||
|
e785fbb3c3 | ||
|
7cf6fc32c0 | ||
|
0f862e3512 | ||
|
b64af18eda | ||
|
4545f20f86 | ||
|
cebf4989e7 | ||
|
df66af74dc | ||
|
c104c76889 | ||
|
c4b1acd775 |
24 changed files with 514 additions and 273 deletions
|
@ -1,6 +1,7 @@
|
||||||
import { mapState } from 'vuex'
|
import { mapState } from 'vuex'
|
||||||
import ProgressButton from '../progress_button/progress_button.vue'
|
import ProgressButton from '../progress_button/progress_button.vue'
|
||||||
import Popover from '../popover/popover.vue'
|
import Popover from '../popover/popover.vue'
|
||||||
|
import ModerationTools from '../moderation_tools/moderation_tools.vue'
|
||||||
|
|
||||||
const AccountActions = {
|
const AccountActions = {
|
||||||
props: [
|
props: [
|
||||||
|
@ -11,7 +12,8 @@ const AccountActions = {
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
ProgressButton,
|
ProgressButton,
|
||||||
Popover
|
Popover,
|
||||||
|
ModerationTools
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
showRepeats () {
|
showRepeats () {
|
||||||
|
@ -20,6 +22,12 @@ const AccountActions = {
|
||||||
hideRepeats () {
|
hideRepeats () {
|
||||||
this.$store.dispatch('hideReblogs', this.user.id)
|
this.$store.dispatch('hideReblogs', this.user.id)
|
||||||
},
|
},
|
||||||
|
muteUser () {
|
||||||
|
this.$store.dispatch('muteUser', this.user.id)
|
||||||
|
},
|
||||||
|
unmuteUser () {
|
||||||
|
this.$store.dispatch('unmuteUser', this.user.id)
|
||||||
|
},
|
||||||
blockUser () {
|
blockUser () {
|
||||||
this.$store.dispatch('blockUser', this.user.id)
|
this.$store.dispatch('blockUser', this.user.id)
|
||||||
},
|
},
|
||||||
|
@ -34,9 +42,15 @@ const AccountActions = {
|
||||||
name: 'chat',
|
name: 'chat',
|
||||||
params: { recipient_id: this.user.id }
|
params: { recipient_id: this.user.id }
|
||||||
})
|
})
|
||||||
|
},
|
||||||
|
mentionUser () {
|
||||||
|
this.$store.dispatch('openPostStatusModal', { replyTo: true, repliedUser: this.user })
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
loggedIn () {
|
||||||
|
return this.$store.state.users.currentUser
|
||||||
|
},
|
||||||
...mapState({
|
...mapState({
|
||||||
pleromaChatMessagesAvailable: state => state.instance.pleromaChatMessagesAvailable
|
pleromaChatMessagesAvailable: state => state.instance.pleromaChatMessagesAvailable
|
||||||
})
|
})
|
||||||
|
|
|
@ -30,6 +30,20 @@
|
||||||
class="dropdown-divider"
|
class="dropdown-divider"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
<button
|
||||||
|
v-if="relationship.muting"
|
||||||
|
class="btn btn-default btn-block dropdown-item"
|
||||||
|
@click="unmuteUser"
|
||||||
|
>
|
||||||
|
{{ $t('user_card.muted') }}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
v-else
|
||||||
|
class="btn btn-default btn-block dropdown-item"
|
||||||
|
@click="muteUser"
|
||||||
|
>
|
||||||
|
{{ $t('user_card.mute') }}
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
v-if="relationship.blocking"
|
v-if="relationship.blocking"
|
||||||
class="btn btn-default btn-block dropdown-item"
|
class="btn btn-default btn-block dropdown-item"
|
||||||
|
@ -50,6 +64,10 @@
|
||||||
>
|
>
|
||||||
{{ $t('user_card.report') }}
|
{{ $t('user_card.report') }}
|
||||||
</button>
|
</button>
|
||||||
|
<div
|
||||||
|
role="separator"
|
||||||
|
class="dropdown-divider"
|
||||||
|
/>
|
||||||
<button
|
<button
|
||||||
v-if="pleromaChatMessagesAvailable"
|
v-if="pleromaChatMessagesAvailable"
|
||||||
class="btn btn-default btn-block dropdown-item"
|
class="btn btn-default btn-block dropdown-item"
|
||||||
|
@ -57,14 +75,25 @@
|
||||||
>
|
>
|
||||||
{{ $t('user_card.message') }}
|
{{ $t('user_card.message') }}
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
class="btn btn-default btn-block dropdown-item"
|
||||||
|
@click="mentionUser"
|
||||||
|
>
|
||||||
|
{{ $t('user_card.mention') }}
|
||||||
|
</button>
|
||||||
|
<ModerationTools
|
||||||
|
v-if="loggedIn.role === "admin" || loggedIn"
|
||||||
|
button-class="btn btn-default btn-block dropdown-item"
|
||||||
|
:user="user"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<button
|
||||||
slot="trigger"
|
slot="trigger"
|
||||||
class="btn btn-default ellipsis-button"
|
class="btn btn-default ellipsis-button"
|
||||||
>
|
>
|
||||||
<i class="icon-ellipsis trigger-button" />
|
<i class="icon-ellipsis trigger-button" />
|
||||||
</div>
|
</button>
|
||||||
</Popover>
|
</Popover>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
@ -74,7 +103,7 @@
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
@import '../../_variables.scss';
|
@import '../../_variables.scss';
|
||||||
.account-actions {
|
.account-actions {
|
||||||
margin: 0 .8em;
|
margin: 0 .5em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.account-actions button.dropdown-item {
|
.account-actions button.dropdown-item {
|
||||||
|
|
|
@ -8,6 +8,7 @@
|
||||||
>
|
>
|
||||||
<UserAvatar
|
<UserAvatar
|
||||||
:user="user"
|
:user="user"
|
||||||
|
no-popover="true"
|
||||||
class="avatar-small"
|
class="avatar-small"
|
||||||
/>
|
/>
|
||||||
</router-link>
|
</router-link>
|
||||||
|
|
|
@ -6,19 +6,11 @@ const BasicUserCard = {
|
||||||
props: [
|
props: [
|
||||||
'user'
|
'user'
|
||||||
],
|
],
|
||||||
data () {
|
|
||||||
return {
|
|
||||||
userExpanded: false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
components: {
|
components: {
|
||||||
UserCard,
|
UserCard,
|
||||||
UserAvatar
|
UserAvatar
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
toggleUserExpanded () {
|
|
||||||
this.userExpanded = !this.userExpanded
|
|
||||||
},
|
|
||||||
userProfileLink (user) {
|
userProfileLink (user) {
|
||||||
return generateProfileLink(user.id, user.screen_name, this.$store.state.instance.restrictedNicknames)
|
return generateProfileLink(user.id, user.screen_name, this.$store.state.instance.restrictedNicknames)
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,22 +4,10 @@
|
||||||
<UserAvatar
|
<UserAvatar
|
||||||
class="avatar"
|
class="avatar"
|
||||||
:user="user"
|
:user="user"
|
||||||
@click.prevent.native="toggleUserExpanded"
|
|
||||||
/>
|
/>
|
||||||
</router-link>
|
</router-link>
|
||||||
<div
|
<div
|
||||||
v-if="userExpanded"
|
class="basic-user-card-content"
|
||||||
class="basic-user-card-expanded-content"
|
|
||||||
>
|
|
||||||
<UserCard
|
|
||||||
:user-id="user.id"
|
|
||||||
:rounded="true"
|
|
||||||
:bordered="true"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
v-else
|
|
||||||
class="basic-user-card-collapsed-content"
|
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
:title="user.name"
|
:title="user.name"
|
||||||
|
@ -59,7 +47,7 @@
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0.6em 1em;
|
padding: 0.6em 1em;
|
||||||
|
|
||||||
&-collapsed-content {
|
&-content {
|
||||||
margin-left: 0.7em;
|
margin-left: 0.7em;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
@ -83,11 +71,5 @@
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
|
|
||||||
&-expanded-content {
|
|
||||||
flex: 1;
|
|
||||||
margin-left: 0.7em;
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -11,7 +11,8 @@ const QUARANTINE = 'mrf_tag:quarantine'
|
||||||
|
|
||||||
const ModerationTools = {
|
const ModerationTools = {
|
||||||
props: [
|
props: [
|
||||||
'user'
|
'user',
|
||||||
|
'buttonClass'
|
||||||
],
|
],
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -124,8 +124,7 @@
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
slot="trigger"
|
slot="trigger"
|
||||||
class="btn btn-default btn-block"
|
:class="`${buttonClass} ${toggled && 'toggled'}`"
|
||||||
:class="{ toggled }"
|
|
||||||
>
|
>
|
||||||
{{ $t('user_card.admin_menu.moderation') }}
|
{{ $t('user_card.admin_menu.moderation') }}
|
||||||
</button>
|
</button>
|
||||||
|
|
|
@ -2,7 +2,6 @@ import StatusContent from '../status_content/status_content.vue'
|
||||||
import { mapState } from 'vuex'
|
import { mapState } from 'vuex'
|
||||||
import Status from '../status/status.vue'
|
import Status from '../status/status.vue'
|
||||||
import UserAvatar from '../user_avatar/user_avatar.vue'
|
import UserAvatar from '../user_avatar/user_avatar.vue'
|
||||||
import UserCard from '../user_card/user_card.vue'
|
|
||||||
import Timeago from '../timeago/timeago.vue'
|
import Timeago from '../timeago/timeago.vue'
|
||||||
import { isStatusNotification } from '../../services/notification_utils/notification_utils.js'
|
import { isStatusNotification } from '../../services/notification_utils/notification_utils.js'
|
||||||
import { highlightClass, highlightStyle } from '../../services/user_highlighter/user_highlighter.js'
|
import { highlightClass, highlightStyle } from '../../services/user_highlighter/user_highlighter.js'
|
||||||
|
@ -20,14 +19,10 @@ const Notification = {
|
||||||
components: {
|
components: {
|
||||||
StatusContent,
|
StatusContent,
|
||||||
UserAvatar,
|
UserAvatar,
|
||||||
UserCard,
|
|
||||||
Timeago,
|
Timeago,
|
||||||
Status
|
Status
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
toggleUserExpanded () {
|
|
||||||
this.userExpanded = !this.userExpanded
|
|
||||||
},
|
|
||||||
generateUserProfileLink (user) {
|
generateUserProfileLink (user) {
|
||||||
return generateProfileLink(user.id, user.screen_name, this.$store.state.instance.restrictedNicknames)
|
return generateProfileLink(user.id, user.screen_name, this.$store.state.instance.restrictedNicknames)
|
||||||
},
|
},
|
||||||
|
|
|
@ -26,24 +26,17 @@
|
||||||
:class="[userClass, { highlighted: userStyle }]"
|
:class="[userClass, { highlighted: userStyle }]"
|
||||||
:style="[ userStyle ]"
|
:style="[ userStyle ]"
|
||||||
>
|
>
|
||||||
<a
|
<router-link
|
||||||
class="avatar-container"
|
class="avatar-container"
|
||||||
:href="notification.from_profile.statusnet_profile_url"
|
:to="userProfileLink"
|
||||||
@click.stop.prevent.capture="toggleUserExpanded"
|
|
||||||
>
|
>
|
||||||
<UserAvatar
|
<UserAvatar
|
||||||
:compact="true"
|
:compact="true"
|
||||||
:better-shadow="betterShadow"
|
:better-shadow="betterShadow"
|
||||||
:user="notification.from_profile"
|
:user="notification.from_profile"
|
||||||
/>
|
/>
|
||||||
</a>
|
</router-link>
|
||||||
<div class="notification-right">
|
<div class="notification-right">
|
||||||
<UserCard
|
|
||||||
v-if="userExpanded"
|
|
||||||
:user-id="getUser(notification).id"
|
|
||||||
:rounded="true"
|
|
||||||
:bordered="true"
|
|
||||||
/>
|
|
||||||
<span class="notification-details">
|
<span class="notification-details">
|
||||||
<div class="name-and-action">
|
<div class="name-and-action">
|
||||||
<!-- eslint-disable vue/no-v-html -->
|
<!-- eslint-disable vue/no-v-html -->
|
||||||
|
|
|
@ -24,7 +24,7 @@ const Notifications = {
|
||||||
bottomedOut: false,
|
bottomedOut: false,
|
||||||
// How many seen notifications to display in the list. The more there are,
|
// How many seen notifications to display in the list. The more there are,
|
||||||
// the heavier the page becomes. This count is increased when loading
|
// the heavier the page becomes. This count is increased when loading
|
||||||
// older notifications, and cut back to default whenever hitting "Read!".
|
// older notifications, and cut back to default whenever hitting "Read!"1.
|
||||||
seenToDisplayCount: DEFAULT_SEEN_TO_DISPLAY_COUNT
|
seenToDisplayCount: DEFAULT_SEEN_TO_DISPLAY_COUNT
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -18,16 +18,37 @@ const Popover = {
|
||||||
// Takes a x/y object and tells how many pixels to offset from
|
// Takes a x/y object and tells how many pixels to offset from
|
||||||
// anchor point on either axis
|
// anchor point on either axis
|
||||||
offset: Object,
|
offset: Object,
|
||||||
|
// Takes a x/y/h object and tells how much to offset the anchor point
|
||||||
|
anchorOffset: Object,
|
||||||
// Replaces the classes you may want for the popover container.
|
// Replaces the classes you may want for the popover container.
|
||||||
// Use 'popover-default' in addition to get the default popover
|
// Use 'popover-default' in addition to get the default popover
|
||||||
// styles with your custom class.
|
// styles with your custom class.
|
||||||
popoverClass: String
|
popoverClass: String,
|
||||||
|
// Time in milliseconds until the popup appears, default is 100ms
|
||||||
|
delay: Number,
|
||||||
|
// If disabled, don't show popover even when trigger conditions met
|
||||||
|
disabled: Boolean
|
||||||
},
|
},
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
hidden: true,
|
hidden: true,
|
||||||
styles: { opacity: 0 },
|
styles: { opacity: 0 },
|
||||||
oldSize: { width: 0, height: 0 }
|
oldSize: { width: 0, height: 0 },
|
||||||
|
timeout: null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
isMobileLayout () {
|
||||||
|
return this.$store.state.interface.mobileLayout
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
disabled (newValue, oldValue) {
|
||||||
|
if (newValue) {
|
||||||
|
this.styles = { opacity: 0 }
|
||||||
|
} else {
|
||||||
|
if (this.trigger === 'hover') this.onMouseenter()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
@ -36,10 +57,8 @@ const Popover = {
|
||||||
return container.getBoundingClientRect()
|
return container.getBoundingClientRect()
|
||||||
},
|
},
|
||||||
updateStyles () {
|
updateStyles () {
|
||||||
if (this.hidden) {
|
if (this.hidden || !(this.$el && this.$el.offsetParent)) {
|
||||||
this.styles = {
|
this.hidePopover()
|
||||||
opacity: 0
|
|
||||||
}
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -48,7 +67,15 @@ const Popover = {
|
||||||
const anchorEl = (this.$refs.trigger && this.$refs.trigger.children[0]) || this.$el
|
const anchorEl = (this.$refs.trigger && this.$refs.trigger.children[0]) || this.$el
|
||||||
const screenBox = anchorEl.getBoundingClientRect()
|
const screenBox = anchorEl.getBoundingClientRect()
|
||||||
// Screen position of the origin point for popover
|
// Screen position of the origin point for popover
|
||||||
const origin = { x: screenBox.left + screenBox.width * 0.5, y: screenBox.top }
|
const anchorOffset = {
|
||||||
|
x: (this.anchorOffset && this.anchorOffset.x) || 0,
|
||||||
|
y: (this.anchorOffset && this.anchorOffset.y) || 0,
|
||||||
|
h: (this.anchorOffset && this.anchorOffset.h) || 0
|
||||||
|
}
|
||||||
|
const origin = {
|
||||||
|
x: screenBox.left + screenBox.width * 0.5 + anchorOffset.x,
|
||||||
|
y: screenBox.top + anchorOffset.y
|
||||||
|
}
|
||||||
const content = this.$refs.content
|
const content = this.$refs.content
|
||||||
// Minor optimization, don't call a slow reflow call if we don't have to
|
// Minor optimization, don't call a slow reflow call if we don't have to
|
||||||
const parentBounds = this.boundTo &&
|
const parentBounds = this.boundTo &&
|
||||||
|
@ -97,12 +124,13 @@ const Popover = {
|
||||||
if (origin.y - content.offsetHeight < yBounds.min) usingTop = false
|
if (origin.y - content.offsetHeight < yBounds.min) usingTop = false
|
||||||
|
|
||||||
const yOffset = (this.offset && this.offset.y) || 0
|
const yOffset = (this.offset && this.offset.y) || 0
|
||||||
|
const anchorHeight = anchorOffset.h || anchorEl.offsetHeight
|
||||||
const translateY = usingTop
|
const translateY = usingTop
|
||||||
? -anchorEl.offsetHeight - yOffset - content.offsetHeight
|
? -anchorEl.offsetHeight - yOffset - content.offsetHeight + anchorOffset.y
|
||||||
: yOffset
|
: -anchorEl.offsetHeight + anchorHeight + yOffset + anchorOffset.y
|
||||||
|
|
||||||
const xOffset = (this.offset && this.offset.x) || 0
|
const xOffset = (this.offset && this.offset.x) || 0
|
||||||
const translateX = (anchorEl.offsetWidth * 0.5) - content.offsetWidth * 0.5 + horizOffset + xOffset
|
const translateX = (anchorEl.offsetWidth * 0.5) - content.offsetWidth * 0.5 + horizOffset + xOffset + anchorOffset.x
|
||||||
|
|
||||||
// Note, separate translateX and translateY avoids blurry text on chromium,
|
// Note, separate translateX and translateY avoids blurry text on chromium,
|
||||||
// single translate or translate3d resulted in blurry text.
|
// single translate or translate3d resulted in blurry text.
|
||||||
|
@ -112,20 +140,36 @@ const Popover = {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
showPopover () {
|
showPopover () {
|
||||||
if (this.hidden) this.$emit('show')
|
if (this.disabled) return
|
||||||
|
if (this.hidden) {
|
||||||
|
this.$emit('show')
|
||||||
|
document.addEventListener('click', this.onClickOutside, true)
|
||||||
|
}
|
||||||
this.hidden = false
|
this.hidden = false
|
||||||
this.$nextTick(this.updateStyles)
|
this.$nextTick(this.updateStyles)
|
||||||
},
|
},
|
||||||
hidePopover () {
|
hidePopover () {
|
||||||
if (!this.hidden) this.$emit('close')
|
if (!this.hidden) {
|
||||||
|
this.$emit('close')
|
||||||
|
document.removeEventListener('click', this.onClickOutside, true)
|
||||||
|
}
|
||||||
|
if (this.timeout) {
|
||||||
|
clearTimeout(this.timeout)
|
||||||
|
this.timeout = null
|
||||||
|
}
|
||||||
this.hidden = true
|
this.hidden = true
|
||||||
this.styles = { opacity: 0 }
|
this.styles = { opacity: 0 }
|
||||||
},
|
},
|
||||||
onMouseenter (e) {
|
onMouseenter (e) {
|
||||||
if (this.trigger === 'hover') this.showPopover()
|
if (this.trigger === 'hover') {
|
||||||
|
this.$emit('enter')
|
||||||
|
this.timeout = setTimeout(this.showPopover, this.delay || 100)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
onMouseleave (e) {
|
onMouseleave (e) {
|
||||||
if (this.trigger === 'hover') this.hidePopover()
|
if (this.trigger === 'hover') {
|
||||||
|
this.hidePopover()
|
||||||
|
}
|
||||||
},
|
},
|
||||||
onClick (e) {
|
onClick (e) {
|
||||||
if (this.trigger === 'click') {
|
if (this.trigger === 'click') {
|
||||||
|
@ -134,12 +178,25 @@ const Popover = {
|
||||||
} else {
|
} else {
|
||||||
this.hidePopover()
|
this.hidePopover()
|
||||||
}
|
}
|
||||||
|
} else if ((this.trigger === 'hover') && this.isMobileLayout) {
|
||||||
|
// This is to enable using hover stuff with mobile:
|
||||||
|
// on first touch it opens the popover, when touching the trigger
|
||||||
|
// again it will do the click action. Can't use touch events as
|
||||||
|
// we can't stop/prevent the actual click which will be handled
|
||||||
|
// first.
|
||||||
|
if (this.hidden) {
|
||||||
|
this.$emit('enter')
|
||||||
|
this.showPopover()
|
||||||
|
e.preventDefault()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onClickOutside (e) {
|
onClickOutside (e) {
|
||||||
if (this.hidden) return
|
if (this.hidden) return
|
||||||
if (this.$el.contains(e.target)) return
|
if (this.$el.contains(e.target)) return
|
||||||
this.hidePopover()
|
this.hidePopover()
|
||||||
|
e.preventDefault()
|
||||||
|
e.stopPropagation()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
updated () {
|
updated () {
|
||||||
|
@ -153,11 +210,7 @@ const Popover = {
|
||||||
this.oldSize = { width: content.offsetWidth, height: content.offsetHeight }
|
this.oldSize = { width: content.offsetWidth, height: content.offsetHeight }
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
created () {
|
|
||||||
document.addEventListener('click', this.onClickOutside)
|
|
||||||
},
|
|
||||||
destroyed () {
|
destroyed () {
|
||||||
document.removeEventListener('click', this.onClickOutside)
|
|
||||||
this.hidePopover()
|
this.hidePopover()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,12 +5,12 @@
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
ref="trigger"
|
ref="trigger"
|
||||||
@click="onClick"
|
@click.capture="onClick"
|
||||||
>
|
>
|
||||||
<slot name="trigger" />
|
<slot name="trigger" />
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="!hidden"
|
v-if="!hidden && !disabled"
|
||||||
ref="content"
|
ref="content"
|
||||||
:style="styles"
|
:style="styles"
|
||||||
class="popover"
|
class="popover"
|
||||||
|
@ -31,7 +31,7 @@
|
||||||
@import '../../_variables.scss';
|
@import '../../_variables.scss';
|
||||||
|
|
||||||
.popover {
|
.popover {
|
||||||
z-index: 8;
|
z-index: 1000;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
@ -63,7 +63,6 @@
|
||||||
text-align: left;
|
text-align: left;
|
||||||
list-style: none;
|
list-style: none;
|
||||||
max-width: 100vw;
|
max-width: 100vw;
|
||||||
z-index: 10;
|
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
|
||||||
.dropdown-divider {
|
.dropdown-divider {
|
||||||
|
@ -116,6 +115,17 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.toggled {
|
||||||
|
background-color: $fallback--lightBg;
|
||||||
|
background-color: var(--selectedMenuPopover, $fallback--lightBg);
|
||||||
|
color: $fallback--link;
|
||||||
|
color: var(--selectedMenuPopoverText, $fallback--link);
|
||||||
|
--faint: var(--selectedMenuPopoverFaintText, $fallback--faint);
|
||||||
|
--faintLink: var(--selectedMenuPopoverFaintLink, $fallback--faint);
|
||||||
|
--lightText: var(--selectedMenuPopoverLightText, $fallback--lightText);
|
||||||
|
--icon: var(--selectedMenuPopoverIcon, $fallback--icon);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -3,12 +3,12 @@ import ReactButton from '../react_button/react_button.vue'
|
||||||
import RetweetButton from '../retweet_button/retweet_button.vue'
|
import RetweetButton from '../retweet_button/retweet_button.vue'
|
||||||
import ExtraButtons from '../extra_buttons/extra_buttons.vue'
|
import ExtraButtons from '../extra_buttons/extra_buttons.vue'
|
||||||
import PostStatusForm from '../post_status_form/post_status_form.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 UserAvatar from '../user_avatar/user_avatar.vue'
|
||||||
import AvatarList from '../avatar_list/avatar_list.vue'
|
import AvatarList from '../avatar_list/avatar_list.vue'
|
||||||
import Timeago from '../timeago/timeago.vue'
|
import Timeago from '../timeago/timeago.vue'
|
||||||
import StatusContent from '../status_content/status_content.vue'
|
import StatusContent from '../status_content/status_content.vue'
|
||||||
import StatusPopover from '../status_popover/status_popover.vue'
|
import StatusPopover from '../status_popover/status_popover.vue'
|
||||||
|
import UserPopover from '../user_popover/user_popover.vue'
|
||||||
import UserListPopover from '../user_list_popover/user_list_popover.vue'
|
import UserListPopover from '../user_list_popover/user_list_popover.vue'
|
||||||
import EmojiReactions from '../emoji_reactions/emoji_reactions.vue'
|
import EmojiReactions from '../emoji_reactions/emoji_reactions.vue'
|
||||||
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
|
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
|
||||||
|
@ -25,12 +25,12 @@ const Status = {
|
||||||
RetweetButton,
|
RetweetButton,
|
||||||
ExtraButtons,
|
ExtraButtons,
|
||||||
PostStatusForm,
|
PostStatusForm,
|
||||||
UserCard,
|
|
||||||
UserAvatar,
|
UserAvatar,
|
||||||
AvatarList,
|
AvatarList,
|
||||||
Timeago,
|
Timeago,
|
||||||
StatusPopover,
|
StatusPopover,
|
||||||
UserListPopover,
|
UserListPopover,
|
||||||
|
UserPopover,
|
||||||
EmojiReactions,
|
EmojiReactions,
|
||||||
StatusContent
|
StatusContent
|
||||||
},
|
},
|
||||||
|
@ -53,7 +53,6 @@ const Status = {
|
||||||
return {
|
return {
|
||||||
replying: false,
|
replying: false,
|
||||||
unmuted: false,
|
unmuted: false,
|
||||||
userExpanded: false,
|
|
||||||
error: null
|
error: null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -246,9 +245,6 @@ const Status = {
|
||||||
toggleMute () {
|
toggleMute () {
|
||||||
this.unmuted = !this.unmuted
|
this.unmuted = !this.unmuted
|
||||||
},
|
},
|
||||||
toggleUserExpanded () {
|
|
||||||
this.userExpanded = !this.userExpanded
|
|
||||||
},
|
|
||||||
generateUserProfileLink (id, name) {
|
generateUserProfileLink (id, name) {
|
||||||
return generateProfileLink(id, name, this.$store.state.instance.restrictedNicknames)
|
return generateProfileLink(id, name, this.$store.state.instance.restrictedNicknames)
|
||||||
}
|
}
|
||||||
|
|
|
@ -100,6 +100,7 @@ $status-margin: 0.75em;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
flex: 1 1 0;
|
flex: 1 1 0;
|
||||||
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
.heading-left {
|
.heading-left {
|
||||||
|
@ -127,19 +128,21 @@ $status-margin: 0.75em;
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.reply-to-accountname {
|
||||||
|
overflow-x: hidden;
|
||||||
|
max-width: 100%;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
word-break: break-all;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
.reply-to-and-accountname {
|
.reply-to-and-accountname {
|
||||||
display: flex;
|
display: flex;
|
||||||
height: 18px;
|
height: 18px;
|
||||||
margin-right: 0.5em;
|
margin-right: 0.5em;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
|
|
||||||
.reply-to-link {
|
|
||||||
white-space: nowrap;
|
|
||||||
word-break: break-word;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
overflow-x: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-reply {
|
.icon-reply {
|
||||||
// mirror the icon
|
// mirror the icon
|
||||||
transform: scaleX(-1);
|
transform: scaleX(-1);
|
||||||
|
|
|
@ -22,9 +22,11 @@
|
||||||
v-if="muted && retweet"
|
v-if="muted && retweet"
|
||||||
class="button-icon icon-retweet"
|
class="button-icon icon-retweet"
|
||||||
/>
|
/>
|
||||||
<router-link :to="userProfileLink">
|
<UserPopover :user-id="status.user.id">
|
||||||
{{ status.user.screen_name }}
|
<router-link :to="userProfileLink">
|
||||||
</router-link>
|
{{ status.user.screen_name }}
|
||||||
|
</router-link>
|
||||||
|
</UserPopover>
|
||||||
</small>
|
</small>
|
||||||
<small
|
<small
|
||||||
v-if="showReasonMutedThread"
|
v-if="showReasonMutedThread"
|
||||||
|
@ -76,15 +78,18 @@
|
||||||
class="status-username repeater-name"
|
class="status-username repeater-name"
|
||||||
:title="retweeter"
|
:title="retweeter"
|
||||||
>
|
>
|
||||||
<router-link
|
<UserPopover :user-id="statusoid.user.id">
|
||||||
v-if="retweeterHtml"
|
<router-link
|
||||||
:to="retweeterProfileLink"
|
v-if="retweeterHtml"
|
||||||
v-html="retweeterHtml"
|
:to="retweeterProfileLink"
|
||||||
/>
|
v-html="retweeterHtml"
|
||||||
<router-link
|
/>
|
||||||
v-else
|
<router-link
|
||||||
:to="retweeterProfileLink"
|
v-else
|
||||||
>{{ retweeter }}</router-link>
|
:to="retweeterProfileLink"
|
||||||
|
>{{ retweeter }}</router-link>
|
||||||
|
</UserPopover>
|
||||||
|
|
||||||
</span>
|
</span>
|
||||||
<i
|
<i
|
||||||
class="fa icon-retweet retweeted"
|
class="fa icon-retweet retweeted"
|
||||||
|
@ -104,10 +109,7 @@
|
||||||
v-if="!noHeading"
|
v-if="!noHeading"
|
||||||
class="left-side"
|
class="left-side"
|
||||||
>
|
>
|
||||||
<router-link
|
<router-link :to="userProfileLink">
|
||||||
:to="userProfileLink"
|
|
||||||
@click.stop.prevent.capture.native="toggleUserExpanded"
|
|
||||||
>
|
|
||||||
<UserAvatar
|
<UserAvatar
|
||||||
:compact="compact"
|
:compact="compact"
|
||||||
:better-shadow="betterShadow"
|
:better-shadow="betterShadow"
|
||||||
|
@ -116,13 +118,6 @@
|
||||||
</router-link>
|
</router-link>
|
||||||
</div>
|
</div>
|
||||||
<div class="right-side">
|
<div class="right-side">
|
||||||
<UserCard
|
|
||||||
v-if="userExpanded"
|
|
||||||
:user-id="status.user.id"
|
|
||||||
:rounded="true"
|
|
||||||
:bordered="true"
|
|
||||||
class="usercard"
|
|
||||||
/>
|
|
||||||
<div
|
<div
|
||||||
v-if="!noHeading"
|
v-if="!noHeading"
|
||||||
class="status-heading"
|
class="status-heading"
|
||||||
|
@ -142,13 +137,18 @@
|
||||||
>
|
>
|
||||||
{{ status.user.name }}
|
{{ status.user.name }}
|
||||||
</h4>
|
</h4>
|
||||||
<router-link
|
<UserPopover
|
||||||
|
:user-id="status.user.id"
|
||||||
class="account-name"
|
class="account-name"
|
||||||
:title="status.user.screen_name"
|
|
||||||
:to="userProfileLink"
|
|
||||||
>
|
>
|
||||||
{{ status.user.screen_name }}
|
<router-link
|
||||||
</router-link>
|
class="account-name"
|
||||||
|
:title="status.user.screen_name"
|
||||||
|
:to="userProfileLink"
|
||||||
|
>
|
||||||
|
{{ status.user.screen_name }}
|
||||||
|
</router-link>
|
||||||
|
</UserPopover>
|
||||||
<img
|
<img
|
||||||
v-if="!!(status.user && status.user.favicon)"
|
v-if="!!(status.user && status.user.favicon)"
|
||||||
class="status-favicon"
|
class="status-favicon"
|
||||||
|
@ -233,13 +233,17 @@
|
||||||
>
|
>
|
||||||
<span class="reply-to-text">{{ $t('status.reply_to') }}</span>
|
<span class="reply-to-text">{{ $t('status.reply_to') }}</span>
|
||||||
</span>
|
</span>
|
||||||
<router-link
|
<UserPopover
|
||||||
class="reply-to-link"
|
:user-id="status.in_reply_to_user_id"
|
||||||
:title="replyToName"
|
|
||||||
:to="replyProfileLink"
|
|
||||||
>
|
>
|
||||||
{{ replyToName }}
|
<router-link
|
||||||
</router-link>
|
class="reply-to-accountname"
|
||||||
|
:title="replyToName"
|
||||||
|
:to="replyProfileLink"
|
||||||
|
>
|
||||||
|
{{ replyToName }}
|
||||||
|
</router-link>
|
||||||
|
</UserPopover>
|
||||||
<span
|
<span
|
||||||
v-if="replies && replies.length"
|
v-if="replies && replies.length"
|
||||||
class="faint replies-separator"
|
class="faint replies-separator"
|
||||||
|
@ -376,4 +380,5 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script src="./status.js" ></script>
|
<script src="./status.js" ></script>
|
||||||
|
|
||||||
<style src="./status.scss" lang="scss"></style>
|
<style src="./status.scss" lang="scss"></style>
|
||||||
|
|
|
@ -2,6 +2,7 @@ import Attachment from '../attachment/attachment.vue'
|
||||||
import Poll from '../poll/poll.vue'
|
import Poll from '../poll/poll.vue'
|
||||||
import Gallery from '../gallery/gallery.vue'
|
import Gallery from '../gallery/gallery.vue'
|
||||||
import LinkPreview from '../link-preview/link-preview.vue'
|
import LinkPreview from '../link-preview/link-preview.vue'
|
||||||
|
import UserPopover from '../user_popover/user_popover.vue'
|
||||||
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
|
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
|
||||||
import fileType from 'src/services/file_type/file_type.service'
|
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 { processHtml } from 'src/services/tiny_post_html_processor/tiny_post_html_processor.service.js'
|
||||||
|
@ -10,6 +11,13 @@ import { mapGetters, mapState } from 'vuex'
|
||||||
|
|
||||||
const StatusContent = {
|
const StatusContent = {
|
||||||
name: 'StatusContent',
|
name: 'StatusContent',
|
||||||
|
components: {
|
||||||
|
Attachment,
|
||||||
|
Poll,
|
||||||
|
Gallery,
|
||||||
|
LinkPreview,
|
||||||
|
UserPopover
|
||||||
|
},
|
||||||
props: [
|
props: [
|
||||||
'status',
|
'status',
|
||||||
'focused',
|
'focused',
|
||||||
|
@ -22,7 +30,9 @@ const StatusContent = {
|
||||||
showingTall: this.fullContent || (this.inConversation && this.focused),
|
showingTall: this.fullContent || (this.inConversation && this.focused),
|
||||||
showingLongSubject: false,
|
showingLongSubject: false,
|
||||||
// not as computed because it sets the initial state which will be changed later
|
// not as computed because it sets the initial state which will be changed later
|
||||||
expandingSubject: !this.$store.getters.mergedConfig.collapseMessageWithSubject
|
expandingSubject: !this.$store.getters.mergedConfig.collapseMessageWithSubject,
|
||||||
|
focusedUserId: null,
|
||||||
|
userPopoverOffset: { x: 0, y: 0 }
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
@ -142,13 +152,18 @@ const StatusContent = {
|
||||||
currentUser: state => state.users.currentUser
|
currentUser: state => state.users.currentUser
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
components: {
|
|
||||||
Attachment,
|
|
||||||
Poll,
|
|
||||||
Gallery,
|
|
||||||
LinkPreview
|
|
||||||
},
|
|
||||||
methods: {
|
methods: {
|
||||||
|
setUserPopoverTarget (event, target, attn) {
|
||||||
|
// event.stopPropagation()
|
||||||
|
// event.preventDefault()
|
||||||
|
this.focusedUserId = attn.id
|
||||||
|
// Give the popover an offset to place it over the hovered element
|
||||||
|
const containerWidth = this.$refs.userPopover.$el.offsetWidth
|
||||||
|
const elementWidth = target.offsetWidth
|
||||||
|
const x = -containerWidth / 2 + target.offsetLeft + elementWidth / 2
|
||||||
|
const y = target.offsetTop
|
||||||
|
this.userPopoverOffset = { x, y, h: target.offsetHeight }
|
||||||
|
},
|
||||||
linkClicked (event) {
|
linkClicked (event) {
|
||||||
const target = event.target.closest('.status-content a')
|
const target = event.target.closest('.status-content a')
|
||||||
if (target) {
|
if (target) {
|
||||||
|
@ -156,8 +171,10 @@ const StatusContent = {
|
||||||
const href = target.href
|
const href = target.href
|
||||||
const attn = this.status.attentions.find(attn => mentionMatchesUrl(attn, href))
|
const attn = this.status.attentions.find(attn => mentionMatchesUrl(attn, href))
|
||||||
if (attn) {
|
if (attn) {
|
||||||
event.stopPropagation()
|
if (this.$store.state.interface.mobileLayout) {
|
||||||
event.preventDefault()
|
this.setUserPopoverTarget(event, target, attn)
|
||||||
|
return
|
||||||
|
}
|
||||||
const link = this.generateUserProfileLink(attn.id, attn.screen_name)
|
const link = this.generateUserProfileLink(attn.id, attn.screen_name)
|
||||||
this.$router.push(link)
|
this.$router.push(link)
|
||||||
return
|
return
|
||||||
|
@ -175,6 +192,19 @@ const StatusContent = {
|
||||||
window.open(target.href, '_blank')
|
window.open(target.href, '_blank')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
linkHover (event) {
|
||||||
|
const target = event.target.closest('.status-content a')
|
||||||
|
this.focusedUserId = null
|
||||||
|
if (target) {
|
||||||
|
if (target.className.match(/mention/)) {
|
||||||
|
const href = target.href
|
||||||
|
const attn = this.status.attentions.find(attn => mentionMatchesUrl(attn, href))
|
||||||
|
if (attn) {
|
||||||
|
this.setUserPopoverTarget(event, target, attn)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
toggleShowMore () {
|
toggleShowMore () {
|
||||||
if (this.mightHideBecauseTall) {
|
if (this.mightHideBecauseTall) {
|
||||||
this.showingTall = !this.showingTall
|
this.showingTall = !this.showingTall
|
||||||
|
|
|
@ -28,67 +28,76 @@
|
||||||
{{ $t("status.show_full_subject") }}
|
{{ $t("status.show_full_subject") }}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div
|
|
||||||
:class="{'tall-status': hideTallStatus}"
|
<UserPopover
|
||||||
class="status-content-wrapper"
|
ref="userPopover"
|
||||||
|
class="status-user-popover"
|
||||||
|
:user-id="focusedUserId"
|
||||||
|
:anchor-offset="userPopoverOffset"
|
||||||
>
|
>
|
||||||
<a
|
|
||||||
v-if="hideTallStatus"
|
|
||||||
class="tall-status-hider"
|
|
||||||
:class="{ 'tall-status-hider_focused': focused }"
|
|
||||||
href="#"
|
|
||||||
@click.prevent="toggleShowMore"
|
|
||||||
>
|
|
||||||
{{ $t("general.show_more") }}
|
|
||||||
</a>
|
|
||||||
<div
|
<div
|
||||||
v-if="!hideSubjectStatus"
|
:class="{'tall-status': hideTallStatus}"
|
||||||
:class="{ 'single-line': singleLine }"
|
class="status-content-wrapper"
|
||||||
class="status-content media-body"
|
|
||||||
@click.prevent="linkClicked"
|
|
||||||
v-html="postBodyHtml"
|
|
||||||
/>
|
|
||||||
<a
|
|
||||||
v-if="hideSubjectStatus"
|
|
||||||
href="#"
|
|
||||||
class="cw-status-hider"
|
|
||||||
@click.prevent="toggleShowMore"
|
|
||||||
>
|
>
|
||||||
{{ $t("status.show_content") }}
|
<a
|
||||||
<span
|
v-if="hideTallStatus"
|
||||||
v-if="attachmentTypes.includes('image')"
|
class="tall-status-hider"
|
||||||
class="icon-picture"
|
:class="{ 'tall-status-hider_focused': focused }"
|
||||||
|
href="#"
|
||||||
|
@click.prevent="toggleShowMore"
|
||||||
|
>
|
||||||
|
{{ $t("general.show_more") }}
|
||||||
|
</a>
|
||||||
|
<div
|
||||||
|
v-if="!hideSubjectStatus"
|
||||||
|
:class="{ 'single-line': singleLine }"
|
||||||
|
class="status-content media-body"
|
||||||
|
@click.prevent="linkClicked"
|
||||||
|
@mouseover="linkHover"
|
||||||
|
v-html="postBodyHtml"
|
||||||
/>
|
/>
|
||||||
<span
|
<a
|
||||||
v-if="attachmentTypes.includes('video')"
|
v-if="hideSubjectStatus"
|
||||||
class="icon-video"
|
href="#"
|
||||||
/>
|
class="cw-status-hider"
|
||||||
<span
|
@click.prevent="toggleShowMore"
|
||||||
v-if="attachmentTypes.includes('audio')"
|
>
|
||||||
class="icon-music"
|
{{ $t("status.show_content") }}
|
||||||
/>
|
<span
|
||||||
<span
|
v-if="attachmentTypes.includes('image')"
|
||||||
v-if="attachmentTypes.includes('unknown')"
|
class="icon-picture"
|
||||||
class="icon-doc"
|
/>
|
||||||
/>
|
<span
|
||||||
<span
|
v-if="attachmentTypes.includes('video')"
|
||||||
v-if="status.poll && status.poll.options"
|
class="icon-video"
|
||||||
class="icon-chart-bar"
|
/>
|
||||||
/>
|
<span
|
||||||
<span
|
v-if="attachmentTypes.includes('audio')"
|
||||||
v-if="status.card"
|
class="icon-music"
|
||||||
class="icon-link"
|
/>
|
||||||
/>
|
<span
|
||||||
</a>
|
v-if="attachmentTypes.includes('unknown')"
|
||||||
<a
|
class="icon-doc"
|
||||||
v-if="showingMore && !fullContent"
|
/>
|
||||||
href="#"
|
<span
|
||||||
class="status-unhider"
|
v-if="status.poll && status.poll.options"
|
||||||
@click.prevent="toggleShowMore"
|
class="icon-chart-bar"
|
||||||
>
|
/>
|
||||||
{{ tallStatus ? $t("general.show_less") : $t("status.hide_content") }}
|
<span
|
||||||
</a>
|
v-if="status.card"
|
||||||
</div>
|
class="icon-link"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
v-if="showingMore && !fullContent"
|
||||||
|
href="#"
|
||||||
|
class="status-unhider"
|
||||||
|
@click.prevent="toggleShowMore"
|
||||||
|
>
|
||||||
|
{{ tallStatus ? $t("general.show_less") : $t("status.hide_content") }}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</UserPopover>
|
||||||
|
|
||||||
<div v-if="status.poll && status.poll.options && !hideSubjectStatus">
|
<div v-if="status.poll && status.poll.options && !hideSubjectStatus">
|
||||||
<poll :base-poll="status.poll" />
|
<poll :base-poll="status.poll" />
|
||||||
|
@ -141,6 +150,10 @@ $status-margin: 0.75em;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
|
|
||||||
|
.status-user-popover {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
.status-content-wrapper {
|
.status-content-wrapper {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
|
@ -175,16 +175,16 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
&:not(.active) {
|
&:not(.active) {
|
||||||
z-index: 4;
|
z-index: 2;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
z-index: 6;
|
z-index: 4;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.active {
|
&.active {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
z-index: 5;
|
z-index: 3;
|
||||||
color: $fallback--text;
|
color: $fallback--text;
|
||||||
color: var(--tabActiveText, $fallback--text);
|
color: var(--tabActiveText, $fallback--text);
|
||||||
}
|
}
|
||||||
|
@ -216,7 +216,7 @@
|
||||||
&::after {
|
&::after {
|
||||||
content: '';
|
content: '';
|
||||||
position: absolute;
|
position: absolute;
|
||||||
z-index: 7;
|
z-index: 5;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,12 @@
|
||||||
import StillImage from '../still-image/still-image.vue'
|
import StillImage from '../still-image/still-image.vue'
|
||||||
|
import UserPopover from '../user_popover/user_popover.vue'
|
||||||
|
|
||||||
const UserAvatar = {
|
const UserAvatar = {
|
||||||
props: [
|
props: [
|
||||||
'user',
|
'user',
|
||||||
'betterShadow',
|
'betterShadow',
|
||||||
'compact'
|
'compact',
|
||||||
|
'noPopover'
|
||||||
],
|
],
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
|
@ -13,7 +15,8 @@ const UserAvatar = {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
StillImage
|
StillImage,
|
||||||
|
UserPopover
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
imgSrc (src) {
|
imgSrc (src) {
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
<template>
|
<template>
|
||||||
<StillImage
|
<StillImage
|
||||||
|
v-if="noPopover"
|
||||||
class="Avatar"
|
class="Avatar"
|
||||||
:alt="user.screen_name"
|
:alt="user.screen_name"
|
||||||
:title="user.screen_name"
|
:title="user.screen_name"
|
||||||
|
@ -7,6 +8,19 @@
|
||||||
:class="{ 'avatar-compact': compact, 'better-shadow': betterShadow }"
|
:class="{ 'avatar-compact': compact, 'better-shadow': betterShadow }"
|
||||||
:image-load-error="imageLoadError"
|
:image-load-error="imageLoadError"
|
||||||
/>
|
/>
|
||||||
|
<UserPopover
|
||||||
|
v-else
|
||||||
|
:user-id="user.id"
|
||||||
|
>
|
||||||
|
<StillImage
|
||||||
|
class="Avatar"
|
||||||
|
:alt="user.screen_name"
|
||||||
|
:title="user.screen_name"
|
||||||
|
:src="imgSrc(user.profile_image_url_original)"
|
||||||
|
:class="{ 'avatar-compact': compact, 'better-shadow': betterShadow }"
|
||||||
|
:image-load-error="imageLoadError"
|
||||||
|
/>
|
||||||
|
</UserPopover>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script src="./user_avatar.js"></script>
|
<script src="./user_avatar.js"></script>
|
||||||
|
|
|
@ -9,7 +9,13 @@ import { mapGetters } from 'vuex'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
props: [
|
props: [
|
||||||
'userId', 'switcher', 'selected', 'hideBio', 'rounded', 'bordered', 'allowZoomingAvatar'
|
'userId',
|
||||||
|
'switcher',
|
||||||
|
'selected',
|
||||||
|
'hideBio',
|
||||||
|
'rounded',
|
||||||
|
'bordered',
|
||||||
|
'allowZoomingAvatar'
|
||||||
],
|
],
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
|
@ -18,7 +24,10 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
created () {
|
created () {
|
||||||
this.$store.dispatch('fetchUserRelationship', this.user.id)
|
const relationship = this.$store.getters.relationship(this.userId)
|
||||||
|
if (!(relationship && !relationship.loading)) {
|
||||||
|
this.$store.dispatch('fetchUserRelationship', this.user.id)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
user () {
|
user () {
|
||||||
|
@ -105,12 +114,6 @@ export default {
|
||||||
FollowButton
|
FollowButton
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
muteUser () {
|
|
||||||
this.$store.dispatch('muteUser', this.user.id)
|
|
||||||
},
|
|
||||||
unmuteUser () {
|
|
||||||
this.$store.dispatch('unmuteUser', this.user.id)
|
|
||||||
},
|
|
||||||
subscribeUser () {
|
subscribeUser () {
|
||||||
return this.$store.dispatch('subscribeUser', this.user.id)
|
return this.$store.dispatch('subscribeUser', this.user.id)
|
||||||
},
|
},
|
||||||
|
@ -144,9 +147,6 @@ export default {
|
||||||
}
|
}
|
||||||
this.$store.dispatch('setMedia', [attachment])
|
this.$store.dispatch('setMedia', [attachment])
|
||||||
this.$store.dispatch('setCurrent', attachment)
|
this.$store.dispatch('setCurrent', attachment)
|
||||||
},
|
|
||||||
mentionUser () {
|
|
||||||
this.$store.dispatch('openPostStatusModal', { replyTo: true, repliedUser: this.user })
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,6 +19,7 @@
|
||||||
<UserAvatar
|
<UserAvatar
|
||||||
:better-shadow="betterShadow"
|
:better-shadow="betterShadow"
|
||||||
:user="user"
|
:user="user"
|
||||||
|
no-popover="true"
|
||||||
/>
|
/>
|
||||||
<div class="user-info-avatar-link-overlay">
|
<div class="user-info-avatar-link-overlay">
|
||||||
<i class="button-icon icon-zoom-in" />
|
<i class="button-icon icon-zoom-in" />
|
||||||
|
@ -31,6 +32,7 @@
|
||||||
<UserAvatar
|
<UserAvatar
|
||||||
:better-shadow="betterShadow"
|
:better-shadow="betterShadow"
|
||||||
:user="user"
|
:user="user"
|
||||||
|
no-popover="true"
|
||||||
/>
|
/>
|
||||||
</router-link>
|
</router-link>
|
||||||
<div class="user-summary">
|
<div class="user-summary">
|
||||||
|
@ -57,11 +59,6 @@
|
||||||
>
|
>
|
||||||
<i class="icon-link-ext usersettings" />
|
<i class="icon-link-ext usersettings" />
|
||||||
</a>
|
</a>
|
||||||
<AccountActions
|
|
||||||
v-if="isOtherUser && loggedIn"
|
|
||||||
:user="user"
|
|
||||||
:relationship="relationship"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="bottom-line">
|
<div class="bottom-line">
|
||||||
<router-link
|
<router-link
|
||||||
|
@ -100,42 +97,6 @@
|
||||||
>
|
>
|
||||||
{{ $t('user_card.follows_you') }}
|
{{ $t('user_card.follows_you') }}
|
||||||
</div>
|
</div>
|
||||||
<div
|
|
||||||
v-if="isOtherUser && (loggedIn || !switcher)"
|
|
||||||
class="highlighter"
|
|
||||||
>
|
|
||||||
<!-- id's need to be unique, otherwise vue confuses which user-card checkbox belongs to -->
|
|
||||||
<input
|
|
||||||
v-if="userHighlightType !== 'disabled'"
|
|
||||||
:id="'userHighlightColorTx'+user.id"
|
|
||||||
v-model="userHighlightColor"
|
|
||||||
class="userHighlightText"
|
|
||||||
type="text"
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
v-if="userHighlightType !== 'disabled'"
|
|
||||||
:id="'userHighlightColor'+user.id"
|
|
||||||
v-model="userHighlightColor"
|
|
||||||
class="userHighlightCl"
|
|
||||||
type="color"
|
|
||||||
>
|
|
||||||
<label
|
|
||||||
for="theme_tab"
|
|
||||||
class="userHighlightSel select"
|
|
||||||
>
|
|
||||||
<select
|
|
||||||
:id="'userHighlightSel'+user.id"
|
|
||||||
v-model="userHighlightType"
|
|
||||||
class="userHighlightSel"
|
|
||||||
>
|
|
||||||
<option value="disabled">No highlight</option>
|
|
||||||
<option value="solid">Solid bg</option>
|
|
||||||
<option value="striped">Striped bg</option>
|
|
||||||
<option value="side">Side stripe</option>
|
|
||||||
</select>
|
|
||||||
<i class="icon-down-open" />
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="loggedIn && isOtherUser"
|
v-if="loggedIn && isOtherUser"
|
||||||
|
@ -160,36 +121,49 @@
|
||||||
>
|
>
|
||||||
<i class="icon-bell-ringing-o" />
|
<i class="icon-bell-ringing-o" />
|
||||||
</ProgressButton>
|
</ProgressButton>
|
||||||
|
<AccountActions
|
||||||
|
v-if="isOtherUser && loggedIn"
|
||||||
|
:user="user"
|
||||||
|
:relationship="relationship"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
v-if="isOtherUser && (loggedIn || !switcher)"
|
||||||
|
class="highlighter"
|
||||||
|
>
|
||||||
|
<!-- id's need to be unique, otherwise vue confuses which user-card checkbox belongs to -->
|
||||||
|
<input
|
||||||
|
v-if="userHighlightType !== 'disabled'"
|
||||||
|
:id="'userHighlightColorTx'+user.id"
|
||||||
|
v-model="userHighlightColor"
|
||||||
|
class="userHighlightText"
|
||||||
|
type="text"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
v-if="userHighlightType !== 'disabled'"
|
||||||
|
:id="'userHighlightColor'+user.id"
|
||||||
|
v-model="userHighlightColor"
|
||||||
|
class="userHighlightCl"
|
||||||
|
type="color"
|
||||||
|
>
|
||||||
|
<label
|
||||||
|
for="theme_tab"
|
||||||
|
class="userHighlightSel select"
|
||||||
|
>
|
||||||
|
<select
|
||||||
|
:id="'userHighlightSel'+user.id"
|
||||||
|
v-model="userHighlightType"
|
||||||
|
class="userHighlightSel"
|
||||||
|
>
|
||||||
|
<option value="disabled">No highlight</option>
|
||||||
|
<option value="solid">Solid bg</option>
|
||||||
|
<option value="striped">Striped bg</option>
|
||||||
|
<option value="side">Side stripe</option>
|
||||||
|
</select>
|
||||||
|
<i class="icon-down-open" />
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
|
||||||
<button
|
|
||||||
v-if="relationship.muting"
|
|
||||||
class="btn btn-default btn-block toggled"
|
|
||||||
@click="unmuteUser"
|
|
||||||
>
|
|
||||||
{{ $t('user_card.muted') }}
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
v-else
|
|
||||||
class="btn btn-default btn-block"
|
|
||||||
@click="muteUser"
|
|
||||||
>
|
|
||||||
{{ $t('user_card.mute') }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<button
|
|
||||||
class="btn btn-default btn-block"
|
|
||||||
@click="mentionUser"
|
|
||||||
>
|
|
||||||
{{ $t('user_card.mention') }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<ModerationTools
|
|
||||||
v-if="loggedIn.role === "admin""
|
|
||||||
:user="user"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="!loggedIn && user.is_local"
|
v-if="!loggedIn && user.is_local"
|
||||||
|
@ -354,7 +328,7 @@
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
max-height: 56px;
|
max-height: 56px;
|
||||||
|
|
||||||
.Avatar {
|
.Avatar.still-image {
|
||||||
flex: 1 0 100%;
|
flex: 1 0 100%;
|
||||||
width: 56px;
|
width: 56px;
|
||||||
height: 56px;
|
height: 56px;
|
||||||
|
|
61
src/components/user_popover/user_popover.js
Normal file
61
src/components/user_popover/user_popover.js
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
|
||||||
|
const UserPopover = {
|
||||||
|
name: 'UserPopover',
|
||||||
|
props: [
|
||||||
|
'userId',
|
||||||
|
'anchorOffset'
|
||||||
|
],
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
error: false,
|
||||||
|
fetching: false,
|
||||||
|
entered: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
user () {
|
||||||
|
return this.$store.getters.findUser(this.userId)
|
||||||
|
},
|
||||||
|
relationshipAvailable () {
|
||||||
|
const relationship = this.$store.getters.relationship(this.userId)
|
||||||
|
return relationship && !relationship.loading
|
||||||
|
}
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
Popover: () => import('../popover/popover.vue'),
|
||||||
|
UserCard: () => import('../user_card/user_card.vue')
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
userId (newValue, oldValue) {
|
||||||
|
if (this.entered) {
|
||||||
|
this.fetchUser()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
fetchUser () {
|
||||||
|
if (!this.userId) return
|
||||||
|
if (this.fetching) return
|
||||||
|
const promises = []
|
||||||
|
if (!this.user) {
|
||||||
|
promises.push(this.$store.dispatch('fetchUser', this.userId))
|
||||||
|
}
|
||||||
|
if (!this.relationshipAvailable) {
|
||||||
|
promises.push(this.$store.dispatch('fetchUserRelationship', this.userId))
|
||||||
|
}
|
||||||
|
if (promises.length > 0) {
|
||||||
|
this.fetching = true
|
||||||
|
Promise.all(promises)
|
||||||
|
.then(data => (this.error = false))
|
||||||
|
.catch(e => (this.error = true))
|
||||||
|
.finally(() => (this.fetching = false))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
enter () {
|
||||||
|
this.entered = true
|
||||||
|
this.fetchUser()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default UserPopover
|
73
src/components/user_popover/user_popover.vue
Normal file
73
src/components/user_popover/user_popover.vue
Normal file
|
@ -0,0 +1,73 @@
|
||||||
|
<template>
|
||||||
|
<Popover
|
||||||
|
class="user-popover-container"
|
||||||
|
trigger="hover"
|
||||||
|
popover-class="popover-default user-popover"
|
||||||
|
:bound-to="{ x: 'container' }"
|
||||||
|
:margin="{ left: 5, right: 5 }"
|
||||||
|
:delay="200"
|
||||||
|
:anchor-offset="anchorOffset"
|
||||||
|
:disabled="!userId"
|
||||||
|
@enter="enter"
|
||||||
|
>
|
||||||
|
<template slot="trigger">
|
||||||
|
<slot />
|
||||||
|
</template>
|
||||||
|
<div
|
||||||
|
slot="content"
|
||||||
|
@click.prevent=""
|
||||||
|
>
|
||||||
|
<span v-if="user && relationshipAvailable">
|
||||||
|
<UserCard
|
||||||
|
:user-id="userId"
|
||||||
|
hide-bio="true"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
<div
|
||||||
|
v-else-if="error"
|
||||||
|
class="user-preview-no-content faint"
|
||||||
|
>
|
||||||
|
{{ $t('status.status_unavailable') }}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-else
|
||||||
|
class="user-preview-no-content"
|
||||||
|
>
|
||||||
|
<i class="icon-spin4 animate-spin" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Popover>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script src="./user_popover.js" ></script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
@import '../../_variables.scss';
|
||||||
|
|
||||||
|
.user-popover-container {
|
||||||
|
max-width: 100%;
|
||||||
|
min-width: 0;
|
||||||
|
&:first-child {
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-popover {
|
||||||
|
font-size: 1rem;
|
||||||
|
width: 30em;
|
||||||
|
max-width: 95%;
|
||||||
|
cursor: default;
|
||||||
|
box-shadow: 2px 2px 3px rgba(0, 0, 0, 0.5);
|
||||||
|
box-shadow: var(--popupShadow);
|
||||||
|
|
||||||
|
.user-preview-no-content {
|
||||||
|
padding: 1em;
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
i {
|
||||||
|
font-size: 2em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
Loading…
Reference in a new issue