Merge branch 'disjointed-popovers' into 'develop'

Disjointed popovers

See merge request pleroma/pleroma-fe!1540
This commit is contained in:
HJ 2022-07-31 17:57:32 +00:00
commit 33ad712852
50 changed files with 658 additions and 358 deletions

View File

@ -16,17 +16,26 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Attachments are ALWAYS in same order as user uploaded, no more "videos first" - Attachments are ALWAYS in same order as user uploaded, no more "videos first"
- Attachment description is prefilled with backend-provided default when uploading - Attachment description is prefilled with backend-provided default when uploading
- Proper visual feedback that next image is loading when browsing - Proper visual feedback that next image is loading when browsing
- UI no longer lags when switching between mobile and desktop mode
- Popovers no longer constrained by DOM hierarchy, shouldn't be cut off by anything
- "Always show mobile button" is working now
### Changed ### Changed
- Using Vue 3 now
- (You)s are optional (opt-in) now, bolding your nickname is also optional (opt-out) - (You)s are optional (opt-in) now, bolding your nickname is also optional (opt-out)
- User highlight background now also covers the `@` - User highlight background now also covers the `@`
- Reverted back to textual `@`, svg version is opt-in. - Reverted back to textual `@`, svg version is opt-in.
- Settings window has been throughly rearranged to make make more sense and make navication settings easier. - Settings window has been thoroughly rearranged to make more sense and make navigation settings easier.
- Uploaded attachments are uniform with displayed attachments - Uploaded attachments are uniform with displayed attachments
- Flash is watchable in media-modal (takes up nearly full screen though due to sizing issues) - Flash is watchable in media-modal (takes up nearly full screen though due to sizing issues)
- Notifications about likes/repeats/emoji reacts are now minimized so they always take up same amount of space irrelevant to size of post. - Notifications about likes/repeats/emoji reacts are now minimized so they always take up same amount of space irrelevant to size of post.
- Slight width/spacing adjustments
- More sizing stuff is font-size dependent now
- Scrollbars are styled/colorized now
- Scrollbars are toggleable (for stuff that didn't have visible scrollbars before) (opt-in)
### Added ### Added
- 3 column mode: only enables when there's space for it (opt-out, customizable)
- Options to show domains in mentions - Options to show domains in mentions
- Option to show user avatars in mention links (opt-in) - Option to show user avatars in mention links (opt-in)
- Option to disable the tooltip for mentions - Option to disable the tooltip for mentions
@ -37,6 +46,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Media modal now also displays description and counter position in gallery (i.e. 1/5) - Media modal now also displays description and counter position in gallery (i.e. 1/5)
- Ability to rearrange order of attachments when uploading - Ability to rearrange order of attachments when uploading
- Enabled users to zoom and pan images in media viewer with mouse and touch - Enabled users to zoom and pan images in media viewer with mouse and touch
- Timelines/panels and conversations have sticky headers now
- Added frontend ui for account migration - Added frontend ui for account migration

View File

@ -4,7 +4,6 @@ import InstanceSpecificPanel from './components/instance_specific_panel/instance
import FeaturesPanel from './components/features_panel/features_panel.vue' import FeaturesPanel from './components/features_panel/features_panel.vue'
import WhoToFollowPanel from './components/who_to_follow_panel/who_to_follow_panel.vue' import WhoToFollowPanel from './components/who_to_follow_panel/who_to_follow_panel.vue'
import ShoutPanel from './components/shout_panel/shout_panel.vue' import ShoutPanel from './components/shout_panel/shout_panel.vue'
import SettingsModal from './components/settings_modal/settings_modal.vue'
import MediaModal from './components/media_modal/media_modal.vue' import MediaModal from './components/media_modal/media_modal.vue'
import SideDrawer from './components/side_drawer/side_drawer.vue' import SideDrawer from './components/side_drawer/side_drawer.vue'
import MobilePostStatusButton from './components/mobile_post_status_button/mobile_post_status_button.vue' import MobilePostStatusButton from './components/mobile_post_status_button/mobile_post_status_button.vue'
@ -32,7 +31,7 @@ export default {
MobilePostStatusButton, MobilePostStatusButton,
MobileNav, MobileNav,
DesktopNav, DesktopNav,
SettingsModal, SettingsModal: defineAsyncComponent(() => import('./components/settings_modal/settings_modal.vue')),
UserReportingModal, UserReportingModal,
PostStatusModal, PostStatusModal,
GlobalNoticeList GlobalNoticeList

View File

@ -4,6 +4,13 @@
:root { :root {
--navbar-height: 3.5rem; --navbar-height: 3.5rem;
--post-line-height: 1.4; --post-line-height: 1.4;
// Z-Index stuff
--ZI_media_modal: 90000;
--ZI_modals_popovers: 85000;
--ZI_modals: 80000;
--ZI_navbar_popovers: 75000;
--ZI_navbar: 70000;
--ZI_popovers: 60000;
} }
html { html {
@ -117,7 +124,7 @@ i[class*=icon-],
} }
nav { nav {
z-index: 1000; z-index: var(--ZI_navbar);
color: var(--topBarText); color: var(--topBarText);
background-color: $fallback--fg; background-color: $fallback--fg;
background-color: var(--topBar, $fallback--fg); background-color: var(--topBar, $fallback--fg);
@ -828,7 +835,7 @@ option {
// Vue transitions // Vue transitions
.fade-enter-active, .fade-enter-active,
.fade-leave-active { .fade-leave-active {
transition: opacity 0.2s; transition: opacity 0.3s;
} }
.fade-enter-from, .fade-enter-from,

View File

@ -42,7 +42,7 @@
</div> </div>
<div id="notifs-column" class="column -scrollable" :class="{ '-show-scrollbar': showScrollbars }"/> <div id="notifs-column" class="column -scrollable" :class="{ '-show-scrollbar': showScrollbars }"/>
</div> </div>
<media-modal /> <MediaModal />
<shout-panel <shout-panel
v-if="currentUser && shout && !hideShoutbox" v-if="currentUser && shout && !hideShoutbox"
:floating="true" :floating="true"
@ -55,6 +55,7 @@
<SettingsModal /> <SettingsModal />
<div id="modal" /> <div id="modal" />
<GlobalNoticeList /> <GlobalNoticeList />
<div id="popovers" />
</div> </div>
</template> </template>

View File

@ -396,6 +396,9 @@ const afterStoreSetup = async ({ store, i18n }) => {
app.component('FAIcon', FontAwesomeIcon) app.component('FAIcon', FontAwesomeIcon)
app.component('FALayers', FontAwesomeLayers) app.component('FALayers', FontAwesomeLayers)
// remove after vue 3.3
app.config.unwrapInjectedRef = true
app.mount('#app') app.mount('#app')
return app return app

View File

@ -1,4 +1,4 @@
import UserCard from '../user_card/user_card.vue' import UserPopover from '../user_popover/user_popover.vue'
import UserAvatar from '../user_avatar/user_avatar.vue' import UserAvatar from '../user_avatar/user_avatar.vue'
import RichContent from 'src/components/rich_content/rich_content.jsx' import RichContent from 'src/components/rich_content/rich_content.jsx'
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'
@ -7,20 +7,12 @@ const BasicUserCard = {
props: [ props: [
'user' 'user'
], ],
data () {
return {
userExpanded: false
}
},
components: { components: {
UserCard, UserPopover,
UserAvatar, UserAvatar,
RichContent RichContent
}, },
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)
} }

View File

@ -1,24 +1,19 @@
<template> <template>
<div class="basic-user-card"> <div class="basic-user-card">
<router-link :to="userProfileLink(user)"> <router-link @click.prevent :to="userProfileLink(user)">
<UserAvatar <UserPopover
class="avatar" :userId="user.id"
:user="user" :overlayCenters="true"
@click.prevent="toggleUserExpanded" overlayCentersSelector=".avatar"
/> >
<UserAvatar
class="user-avatar avatar"
:user="user"
@click.prevent
/>
</UserPopover>
</router-link> </router-link>
<div <div
v-if="userExpanded"
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" class="basic-user-card-collapsed-content"
> >
<div <div
@ -53,6 +48,8 @@
margin: 0; margin: 0;
padding: 0.6em 1em; padding: 0.6em 1em;
--emoji-size: 14px;
&-collapsed-content { &-collapsed-content {
margin-left: 0.7em; margin-left: 0.7em;
text-align: left; text-align: left;

View File

@ -6,7 +6,7 @@ import Gallery from '../gallery/gallery.vue'
import LinkPreview from '../link-preview/link-preview.vue' import LinkPreview from '../link-preview/link-preview.vue'
import StatusContent from '../status_content/status_content.vue' import StatusContent from '../status_content/status_content.vue'
import ChatMessageDate from '../chat_message_date/chat_message_date.vue' import ChatMessageDate from '../chat_message_date/chat_message_date.vue'
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator' import { defineAsyncComponent } from 'vue'
import { library } from '@fortawesome/fontawesome-svg-core' import { library } from '@fortawesome/fontawesome-svg-core'
import { import {
faTimes, faTimes,
@ -35,7 +35,8 @@ const ChatMessage = {
UserAvatar, UserAvatar,
Gallery, Gallery,
LinkPreview, LinkPreview,
ChatMessageDate ChatMessageDate,
UserPopover: defineAsyncComponent(() => import('../user_popover/user_popover.vue'))
}, },
computed: { computed: {
// Returns HH:MM (hours and minutes) in local time. // Returns HH:MM (hours and minutes) in local time.
@ -49,9 +50,6 @@ const ChatMessage = {
message () { message () {
return this.chatViewItem.data return this.chatViewItem.data
}, },
userProfileLink () {
return generateProfileLink(this.author.id, this.author.screen_name, this.$store.state.instance.restrictedNicknames)
},
isMessage () { isMessage () {
return this.chatViewItem.type === 'message' return this.chatViewItem.type === 'message'
}, },

View File

@ -14,16 +14,16 @@
v-if="!isCurrentUser" v-if="!isCurrentUser"
class="avatar-wrapper" class="avatar-wrapper"
> >
<router-link <UserPopover
v-if="chatViewItem.isHead" v-if="chatViewItem.isHead"
:to="userProfileLink" :userId="author.id"
> >
<UserAvatar <UserAvatar
:compact="true" :compact="true"
:better-shadow="betterShadow" :better-shadow="betterShadow"
:user="author" :user="author"
/> />
</router-link> </UserPopover>
</div> </div>
<div class="chat-message-inner"> <div class="chat-message-inner">
<div <div
@ -44,7 +44,7 @@
<Popover <Popover
trigger="click" trigger="click"
placement="top" placement="top"
:bound-to-selector="isCurrentUser ? '' : '.scrollable-message-list'" bound-to-selector=".chat-view-inner"
:bound-to="{ x: 'container' }" :bound-to="{ x: 'container' }"
:margin="popoverMarginStyle" :margin="popoverMarginStyle"
@show="menuOpened = true" @show="menuOpened = true"

View File

@ -1,12 +1,13 @@
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
import UserAvatar from '../user_avatar/user_avatar.vue' import UserAvatar from '../user_avatar/user_avatar.vue'
import RichContent from 'src/components/rich_content/rich_content.jsx' import RichContent from 'src/components/rich_content/rich_content.jsx'
import { defineAsyncComponent } from 'vue'
export default { export default {
name: 'ChatTitle', name: 'ChatTitle',
components: { components: {
UserAvatar, UserAvatar,
RichContent RichContent,
UserPopover: defineAsyncComponent(() => import('../user_popover/user_popover.vue'))
}, },
props: [ props: [
'user', 'withAvatar' 'user', 'withAvatar'
@ -18,10 +19,5 @@ export default {
htmlTitle () { htmlTitle () {
return this.user ? this.user.name_html : '' return this.user ? this.user.name_html : ''
} }
},
methods: {
getUserProfileLink (user) {
return generateProfileLink(user.id, user.screen_name)
}
} }
} }

View File

@ -3,16 +3,16 @@
class="chat-title" class="chat-title"
:title="title" :title="title"
> >
<router-link <UserPopover
class="avatar-container" class="avatar-container"
v-if="withAvatar && user" v-if="withAvatar && user"
:to="getUserProfileLink(user)" :userId="user.id"
> >
<UserAvatar <UserAvatar
class="titlebar-avatar" class="titlebar-avatar"
:user="user" :user="user"
/> />
</router-link> </UserPopover>
<RichContent <RichContent
v-if="user" v-if="user"
class="username" class="username"

View File

@ -2,6 +2,7 @@
.DesktopNav { .DesktopNav {
width: 100%; width: 100%;
z-index: var(--ZI_navbar);
input { input {
color: var(--inputTopbarText, var(--inputText)); color: var(--inputTopbarText, var(--inputText));

View File

@ -38,7 +38,7 @@
/> />
<button <button
class="button-unstyled nav-icon" class="button-unstyled nav-icon"
@click.stop="openSettingsModal" @click="openSettingsModal"
> >
<FAIcon <FAIcon
fixed-width fixed-width

View File

@ -7,7 +7,8 @@
right: 0; right: 0;
left: 0; left: 0;
margin: 0 !important; margin: 0 !important;
z-index: 100; // TODO: actually use popover in emoji picker
z-index: var(--ZI_popovers);
background-color: $fallback--bg; background-color: $fallback--bg;
background-color: var(--popover, $fallback--bg); background-color: var(--popover, $fallback--bg);
color: $fallback--link; color: $fallback--link;

View File

@ -89,6 +89,9 @@ const ExtraButtons = {
canMute () { canMute () {
return !!this.currentUser return !!this.currentUser
}, },
canBookmark () {
return !!this.currentUser
},
statusLink () { statusLink () {
return `${this.$store.state.instance.server}${this.$router.resolve({ name: 'conversation', params: { id: this.status.id } }).href}` return `${this.$store.state.instance.server}${this.$router.resolve({ name: 'conversation', params: { id: this.status.id } }).href}`
} }

View File

@ -51,28 +51,30 @@
icon="thumbtack" icon="thumbtack"
/><span>{{ $t("status.unpin") }}</span> /><span>{{ $t("status.unpin") }}</span>
</button> </button>
<button <template v-if="canBookmark">
v-if="!status.bookmarked" <button
class="button-default dropdown-item dropdown-item-icon" v-if="!status.bookmarked"
@click.prevent="bookmarkStatus" class="button-default dropdown-item dropdown-item-icon"
@click="close" @click.prevent="bookmarkStatus"
> @click="close"
<FAIcon >
fixed-width <FAIcon
:icon="['far', 'bookmark']" fixed-width
/><span>{{ $t("status.bookmark") }}</span> :icon="['far', 'bookmark']"
</button> /><span>{{ $t("status.bookmark") }}</span>
<button </button>
v-if="status.bookmarked" <button
class="button-default dropdown-item dropdown-item-icon" v-if="status.bookmarked"
@click.prevent="unbookmarkStatus" class="button-default dropdown-item dropdown-item-icon"
@click="close" @click.prevent="unbookmarkStatus"
> @click="close"
<FAIcon >
fixed-width <FAIcon
icon="bookmark" fixed-width
/><span>{{ $t("status.unbookmark") }}</span> icon="bookmark"
</button> /><span>{{ $t("status.unbookmark") }}</span>
</button>
</template>
<button <button
v-if="canDelete" v-if="canDelete"
class="button-default dropdown-item dropdown-item-icon" class="button-default dropdown-item dropdown-item-icon"
@ -119,12 +121,12 @@
</div> </div>
</template> </template>
<template v-slot:trigger> <template v-slot:trigger>
<button class="button-unstyled popover-trigger"> <span class="button-unstyled popover-trigger">
<FAIcon <FAIcon
class="fa-scale-110 fa-old-padding" class="fa-scale-110 fa-old-padding"
icon="ellipsis-h" icon="ellipsis-h"
/> />
</button> </span>
</template> </template>
</Popover> </Popover>
</template> </template>

View File

@ -32,7 +32,7 @@
top: 50px; top: 50px;
width: 100%; width: 100%;
pointer-events: none; pointer-events: none;
z-index: 1001; z-index: var(--ZI_popovers);
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;

View File

@ -121,7 +121,7 @@ $modal-view-button-icon-width: 3em;
$modal-view-button-icon-margin: 0.5em; $modal-view-button-icon-margin: 0.5em;
.modal-view.media-modal-view { .modal-view.media-modal-view {
z-index: 9000; z-index: var(--ZI_media_modal);
flex-direction: column; flex-direction: column;
.modal-view-button-arrow, .modal-view-button-arrow,

View File

@ -2,6 +2,7 @@ import generateProfileLink from 'src/services/user_profile_link_generator/user_p
import { mapGetters, mapState } from 'vuex' import { mapGetters, mapState } from 'vuex'
import { highlightClass, highlightStyle } from '../../services/user_highlighter/user_highlighter.js' import { highlightClass, highlightStyle } from '../../services/user_highlighter/user_highlighter.js'
import UserAvatar from '../user_avatar/user_avatar.vue' import UserAvatar from '../user_avatar/user_avatar.vue'
import { defineAsyncComponent } from 'vue'
import { library } from '@fortawesome/fontawesome-svg-core' import { library } from '@fortawesome/fontawesome-svg-core'
import { import {
faAt faAt
@ -14,7 +15,8 @@ library.add(
const MentionLink = { const MentionLink = {
name: 'MentionLink', name: 'MentionLink',
components: { components: {
UserAvatar UserAvatar,
UserPopover: defineAsyncComponent(() => import('../user_popover/user_popover.vue'))
}, },
props: { props: {
url: { url: {
@ -34,15 +36,30 @@ const MentionLink = {
type: String type: String
} }
}, },
data () {
return {
hasSelection: false
}
},
methods: { methods: {
onClick () { onClick () {
if (this.shouldShowTooltip) return
const link = generateProfileLink( const link = generateProfileLink(
this.userId || this.user.id, this.userId || this.user.id,
this.userScreenName || this.user.screen_name this.userScreenName || this.user.screen_name
) )
this.$router.push(link) this.$router.push(link)
},
handleSelection () {
this.hasSelection = document.getSelection().containsNode(this.$refs.full, true)
} }
}, },
mounted () {
document.addEventListener('selectionchange', this.handleSelection)
},
unmounted () {
document.removeEventListener('selectionchange', this.handleSelection)
},
computed: { computed: {
user () { user () {
return this.url && this.$store && this.$store.getters.findUserByUrl(this.url) return this.url && this.$store && this.$store.getters.findUserByUrl(this.url)
@ -88,7 +105,8 @@ const MentionLink = {
return [ return [
{ {
'-you': this.isYou && this.shouldBoldenYou, '-you': this.isYou && this.shouldBoldenYou,
'-highlighted': this.highlight '-highlighted': this.highlight,
'-has-selection': this.hasSelection
}, },
this.highlightType this.highlightType
] ]
@ -110,7 +128,7 @@ const MentionLink = {
} }
}, },
shouldShowTooltip () { shouldShowTooltip () {
return this.mergedConfig.mentionLinkShowTooltip && this.mergedConfig.mentionLinkDisplay === 'short' && this.isRemote return this.mergedConfig.mentionLinkShowTooltip
}, },
shouldShowAvatar () { shouldShowAvatar () {
return this.mergedConfig.mentionLinkShowAvatar return this.mergedConfig.mentionLinkShowAvatar

View File

@ -55,11 +55,14 @@
.new { .new {
&.-you { &.-you {
& .shortName, .shortName {
& .full {
font-weight: 600; font-weight: 600;
} }
} }
&.-has-selection {
color: var(--alertNeutralText, $fallback--text);
background-color: var(--alertNeutral, $fallback--fg);
}
.at { .at {
color: var(--link); color: var(--link);
@ -72,8 +75,7 @@
} }
&.-striped { &.-striped {
& .shortName, & .shortName {
& .full {
background-image: background-image:
repeating-linear-gradient( repeating-linear-gradient(
135deg, 135deg,
@ -86,30 +88,29 @@
} }
&.-solid { &.-solid {
& .shortName, .shortName {
& .full {
background-image: linear-gradient(var(--____highlight-tintColor2), var(--____highlight-tintColor2)); background-image: linear-gradient(var(--____highlight-tintColor2), var(--____highlight-tintColor2));
} }
} }
&.-side { &.-side {
& .shortName, .shortName {
& .userNameFull {
box-shadow: 0 -5px 3px -4px inset var(--____highlight-solidColor); box-shadow: 0 -5px 3px -4px inset var(--____highlight-solidColor);
} }
} }
} }
&:hover .new .full { .full {
opacity: 1; pointer-events: none;
pointer-events: initial;
} }
.serverName.-faded { .serverName.-faded {
color: var(--faintLink, $fallback--link); color: var(--faintLink, $fallback--link);
} }
}
.full .-faded {
color: var(--faint, $fallback--faint); .mention-link-popover {
} max-width: 70ch;
max-height: 20rem;
overflow: hidden;
} }

View File

@ -9,66 +9,58 @@
class="original" class="original"
target="_blank" target="_blank"
v-html="content" v-html="content"
/><!-- eslint-enable vue/no-v-html --><span /><!-- eslint-enable vue/no-v-html -->
v-if="user" <UserPopover
class="new" v-else
:style="style" :userId="user.id"
:class="classnames" :disabled="!shouldShowTooltip"
> >
<a <span
class="short button-unstyled" v-if="user"
:class="{ '-with-tooltip': shouldShowTooltip }" class="new"
:href="url" :style="style"
@click.prevent="onClick" :class="classnames"
> >
<!-- eslint-disable vue/no-v-html --> <a
<UserAvatar class="short button-unstyled"
v-if="shouldShowAvatar" :class="{ '-with-tooltip': shouldShowTooltip }"
class="mention-avatar" :href="url"
:user="user" @click.prevent="onClick"
/><span
class="shortName"
><FAIcon
v-if="useAtIcon"
size="sm"
icon="at"
class="at"
/>{{ !useAtIcon ? '@' : '' }}<span
class="userName"
v-html="userName"
/><span
v-if="shouldShowFullUserName"
class="serverName"
:class="{ '-faded': shouldFadeDomain }"
v-html="'@' + serverName"
/>
</span>
<span
v-if="isYou && shouldShowYous"
:class="{ '-you': shouldBoldenYou }"
> {{ ' ' + $t('status.you') }}</span>
<!-- eslint-enable vue/no-v-html -->
</a><span
v-if="shouldShowTooltip"
class="full popover-default"
:class="[highlightType]"
>
<span
class="userNameFull"
> >
<!-- eslint-disable vue/no-v-html --> <!-- eslint-disable vue/no-v-html -->
@<span <UserAvatar
v-if="shouldShowAvatar"
class="mention-avatar"
:user="user"
/><span
class="shortName"
><FAIcon
v-if="useAtIcon"
size="sm"
icon="at"
class="at"
/>{{ !useAtIcon ? '@' : '' }}<span
class="userName" class="userName"
v-html="userName" v-html="userName"
/><span /><span
v-if="shouldShowFullUserName"
class="serverName" class="serverName"
:class="{ '-faded': shouldFadeDomain }" :class="{ '-faded': shouldFadeDomain }"
v-html="'@' + serverName" v-html="'@' + serverName"
/> />
</span>
<span
v-if="isYou && shouldShowYous"
:class="{ '-you': shouldBoldenYou }"
> {{ ' ' + $t('status.you') }}</span>
<!-- eslint-enable vue/no-v-html --> <!-- eslint-enable vue/no-v-html -->
</a><span class="full" ref="full">
<!-- eslint-disable vue/no-v-html -->
@<span v-html="userName" /><span v-html="'@' + serverName" />
<!-- eslint-enable vue/no-v-html -->
</span> </span>
</span> </span>
</span> </UserPopover>
</span> </span>
</template> </template>

View File

@ -13,8 +13,7 @@
<span <span
v-if="expanded" v-if="expanded"
class="fullExtraMentions" class="fullExtraMentions"
> >{{ ' ' }}<MentionLink
<MentionLink
v-for="mention in extraMentions" v-for="mention in extraMentions"
:key="mention.index" :key="mention.index"
class="mention-link" class="mention-link"

View File

@ -86,6 +86,8 @@
@import '../../_variables.scss'; @import '../../_variables.scss';
.MobileNav { .MobileNav {
z-index: var(--ZI_navbar);
.mobile-nav { .mobile-nav {
display: grid; display: grid;
line-height: var(--navbar-height); line-height: var(--navbar-height);
@ -147,7 +149,7 @@
transition-property: transform; transition-property: transform;
transition-duration: 0.25s; transition-duration: 0.25s;
transform: translateX(0); transform: translateX(0);
z-index: 1001; z-index: var(--ZI_navbar);
-webkit-overflow-scrolling: touch; -webkit-overflow-scrolling: touch;
&.-closed { &.-closed {
@ -160,7 +162,7 @@
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
z-index: 1; z-index: calc(var(--ZI_navbar) + 100);
width: 100%; width: 100%;
height: 50px; height: 50px;
line-height: 50px; line-height: 50px;

View File

@ -22,6 +22,9 @@ export default {
default: false default: false
} }
}, },
provide: {
popoversZLayer: 'modals'
},
computed: { computed: {
classes () { classes () {
return { return {
@ -35,7 +38,7 @@ export default {
<style lang="scss"> <style lang="scss">
.modal-view { .modal-view {
z-index: 2000; z-index: var(--ZI_modals);
position: fixed; position: fixed;
top: 0; top: 0;
left: 0; left: 0;

View File

@ -113,7 +113,9 @@
border-color: $fallback--border; border-color: $fallback--border;
border-color: var(--border, $fallback--border); border-color: var(--border, $fallback--border);
padding: 0; padding: 0;
}
> li {
&:first-child .menu-item { &:first-child .menu-item {
border-top-right-radius: $fallback--panelRadius; border-top-right-radius: $fallback--panelRadius;
border-top-right-radius: var(--panelRadius, $fallback--panelRadius); border-top-right-radius: var(--panelRadius, $fallback--panelRadius);

View File

@ -5,6 +5,7 @@ import UserAvatar from '../user_avatar/user_avatar.vue'
import UserCard from '../user_card/user_card.vue' import UserCard from '../user_card/user_card.vue'
import Timeago from '../timeago/timeago.vue' import Timeago from '../timeago/timeago.vue'
import RichContent from 'src/components/rich_content/rich_content.jsx' import RichContent from 'src/components/rich_content/rich_content.jsx'
import UserPopover from '../user_popover/user_popover.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'
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'
@ -46,7 +47,8 @@ const Notification = {
UserCard, UserCard,
Timeago, Timeago,
Status, Status,
RichContent RichContent,
UserPopover
}, },
methods: { methods: {
toggleUserExpanded () { toggleUserExpanded () {

View File

@ -34,21 +34,22 @@
<a <a
class="avatar-container" class="avatar-container"
:href="$router.resolve(userProfileLink).href" :href="$router.resolve(userProfileLink).href"
@click.stop.prevent.capture="toggleUserExpanded" @click.prevent
> >
<UserAvatar <UserPopover
:compact="true" :userId="notification.from_profile.id"
:better-shadow="betterShadow" :overlayCenters="true"
:user="notification.from_profile" >
/> <UserAvatar
class="post-avatar"
:bot="botIndicator"
:compact="true"
:better-shadow="betterShadow"
:user="notification.from_profile"
/>
</UserPopover>
</a> </a>
<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 -->

View File

@ -1,3 +1,4 @@
import { computed } from 'vue'
import { mapGetters } from 'vuex' import { mapGetters } from 'vuex'
import Notification from '../notification/notification.vue' import Notification from '../notification/notification.vue'
import NotificationFilters from './notification_filters.vue' import NotificationFilters from './notification_filters.vue'
@ -40,6 +41,11 @@ const Notifications = {
seenToDisplayCount: DEFAULT_SEEN_TO_DISPLAY_COUNT seenToDisplayCount: DEFAULT_SEEN_TO_DISPLAY_COUNT
} }
}, },
provide () {
return {
popoversZLayer: computed(() => this.popoversZLayer)
}
},
computed: { computed: {
mainClass () { mainClass () {
return this.minimalMode ? '' : 'panel panel-default' return this.minimalMode ? '' : 'panel panel-default'
@ -77,6 +83,10 @@ const Notifications = {
} }
return map[layoutType] || '#notifs-sidebar' return map[layoutType] || '#notifs-sidebar'
}, },
popoversZLayer () {
const { layoutType } = this.$store.state.interface
return layoutType === 'mobile' ? 'navbar' : null
},
notificationsToDisplay () { notificationsToDisplay () {
return this.filteredNotifications.slice(0, this.unseenCount + this.seenToDisplayCount) return this.filteredNotifications.slice(0, this.unseenCount + this.seenToDisplayCount)
}, },

View File

@ -31,13 +31,35 @@ const Popover = {
// If true, subtract padding when calculating position for the popover, // If true, subtract padding when calculating position for the popover,
// use it when popover offset looks to be different on top vs bottom. // use it when popover offset looks to be different on top vs bottom.
removePadding: Boolean removePadding: Boolean,
// self-explanatory (i hope)
disabled: Boolean,
// Instead of putting popover next to anchor, overlay popover's center on top of anchor's center
overlayCenters: Boolean,
// What selector (witin popover!) to use for determining center of popover
overlayCentersSelector: String,
// Lets hover popover stay when clicking inside of it
stayOnClick: Boolean
}, },
inject: ['popoversZLayer'], // override popover z layer
data () { data () {
return { return {
// lockReEntry is a flag that is set when mouse cursor is leaving the popover's content
// so that if mouse goes back into popover it won't be re-shown again to prevent annoyance
// with popovers refusing to be hidden when user wants to interact with something in below popover
lockReEntry: false,
hidden: true, hidden: true,
styles: { opacity: 0 }, styles: {},
oldSize: { width: 0, height: 0 } oldSize: { width: 0, height: 0 },
scrollable: null,
// used to avoid blinking if hovered onto popover
graceTimeout: null,
parentPopover: null,
childrenShown: new Set()
} }
}, },
methods: { methods: {
@ -47,9 +69,7 @@ const Popover = {
}, },
updateStyles () { updateStyles () {
if (this.hidden) { if (this.hidden) {
this.styles = { this.styles = {}
opacity: 0
}
return return
} }
@ -57,14 +77,26 @@ const Popover = {
// its children are what are inside the slot. Expect only one v-slot:trigger. // its children are what are inside the slot. Expect only one v-slot:trigger.
const anchorEl = (this.$refs.trigger && this.$refs.trigger.children[0]) || this.$el const anchorEl = (this.$refs.trigger && this.$refs.trigger.children[0]) || this.$el
// SVGs don't have offsetWidth/Height, use fallback // SVGs don't have offsetWidth/Height, use fallback
const anchorWidth = anchorEl.offsetWidth || anchorEl.clientWidth
const anchorHeight = anchorEl.offsetHeight || anchorEl.clientHeight const anchorHeight = anchorEl.offsetHeight || anchorEl.clientHeight
const screenBox = anchorEl.getBoundingClientRect() const anchorWidth = anchorEl.offsetWidth || anchorEl.clientWidth
// Screen position of the origin point for popover const anchorScreenBox = anchorEl.getBoundingClientRect()
const origin = { x: screenBox.left + screenBox.width * 0.5, y: screenBox.top }
const anchorStyle = getComputedStyle(anchorEl)
const topPadding = parseFloat(anchorStyle.paddingTop)
const bottomPadding = parseFloat(anchorStyle.paddingBottom)
// Screen position of the origin point for popover = center of the anchor
const origin = {
x: anchorScreenBox.left + anchorWidth * 0.5,
y: anchorScreenBox.top + anchorHeight * 0.5
}
const content = this.$refs.content const content = this.$refs.content
const overlayCenter = this.overlayCenters
? this.$refs.content.querySelector(this.overlayCentersSelector)
: null
// 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 parentScreenBox = this.boundTo &&
(this.boundTo.x === 'container' || this.boundTo.y === 'container') && (this.boundTo.x === 'container' || this.boundTo.y === 'container') &&
this.containerBoundingClientRect() this.containerBoundingClientRect()
@ -73,81 +105,151 @@ const Popover = {
// What are the screen bounds for the popover? Viewport vs container // What are the screen bounds for the popover? Viewport vs container
// when using viewport, using default margin values to dodge the navbar // when using viewport, using default margin values to dodge the navbar
const xBounds = this.boundTo && this.boundTo.x === 'container' ? { const xBounds = this.boundTo && this.boundTo.x === 'container' ? {
min: parentBounds.left + (margin.left || 0), min: parentScreenBox.left + (margin.left || 0),
max: parentBounds.right - (margin.right || 0) max: parentScreenBox.right - (margin.right || 0)
} : { } : {
min: 0 + (margin.left || 10), min: 0 + (margin.left || 10),
max: window.innerWidth - (margin.right || 10) max: window.innerWidth - (margin.right || 10)
} }
const yBounds = this.boundTo && this.boundTo.y === 'container' ? { const yBounds = this.boundTo && this.boundTo.y === 'container' ? {
min: parentBounds.top + (margin.top || 0), min: parentScreenBox.top + (margin.top || 0),
max: parentBounds.bottom - (margin.bottom || 0) max: parentScreenBox.bottom - (margin.bottom || 0)
} : { } : {
min: 0 + (margin.top || 50), min: 0 + (margin.top || 50),
max: window.innerHeight - (margin.bottom || 5) max: window.innerHeight - (margin.bottom || 5)
} }
let horizOffset = 0 let horizOffset = 0
let vertOffset = 0
if (overlayCenter) {
const box = content.getBoundingClientRect()
const overlayCenterScreenBox = overlayCenter.getBoundingClientRect()
const leftInnerOffset = overlayCenterScreenBox.left - box.left
const topInnerOffset = overlayCenterScreenBox.top - box.top
horizOffset = -leftInnerOffset - overlayCenter.offsetWidth * 0.5
vertOffset = -topInnerOffset - overlayCenter.offsetHeight * 0.5
} else {
horizOffset = content.offsetWidth * -0.5
vertOffset = content.offsetHeight * -0.5
}
const leftBorder = origin.x + horizOffset
const rightBorder = leftBorder + content.offsetWidth
const topBorder = origin.y + vertOffset
const bottomBorder = topBorder + content.offsetHeight
// If overflowing from left, move it so that it doesn't // If overflowing from left, move it so that it doesn't
if ((origin.x - content.offsetWidth * 0.5) < xBounds.min) { if (leftBorder < xBounds.min) {
horizOffset += -(origin.x - content.offsetWidth * 0.5) + xBounds.min horizOffset += xBounds.min - leftBorder
} }
// If overflowing from right, move it so that it doesn't // If overflowing from right, move it so that it doesn't
if ((origin.x + horizOffset + content.offsetWidth * 0.5) > xBounds.max) { if (rightBorder > xBounds.max) {
horizOffset -= (origin.x + horizOffset + content.offsetWidth * 0.5) - xBounds.max horizOffset -= rightBorder - xBounds.max
} }
// Default to whatever user wished with placement prop // If overflowing from top, move it so that it doesn't
let usingTop = this.placement !== 'bottom' if (topBorder < yBounds.min) {
vertOffset += yBounds.min - topBorder
// Handle special cases, first force to displaying on top if there's not space on bottom,
// regardless of what placement value was. Then check if there's not space on top, and
// force to bottom, again regardless of what placement value was.
if (origin.y + content.offsetHeight > yBounds.max) usingTop = true
if (origin.y - content.offsetHeight < yBounds.min) usingTop = false
let vPadding = 0
if (this.removePadding && usingTop) {
const anchorStyle = getComputedStyle(anchorEl)
vPadding = parseFloat(anchorStyle.paddingTop) + parseFloat(anchorStyle.paddingBottom)
} }
const yOffset = (this.offset && this.offset.y) || 0 // If overflowing from bottom, move it so that it doesn't
const translateY = usingTop if (bottomBorder > yBounds.max) {
? -anchorHeight + vPadding - yOffset - content.offsetHeight vertOffset -= bottomBorder - yBounds.max
: yOffset }
const xOffset = (this.offset && this.offset.x) || 0 let translateX = 0
const translateX = anchorWidth * 0.5 - content.offsetWidth * 0.5 + horizOffset + xOffset let translateY = 0
if (overlayCenter) {
translateX = origin.x + horizOffset
translateY = origin.y + vertOffset
} else {
// Default to whatever user wished with placement prop
let usingTop = this.placement !== 'bottom'
// Handle special cases, first force to displaying on top if there's not space on bottom,
// regardless of what placement value was. Then check if there's not space on top, and
// force to bottom, again regardless of what placement value was.
const topBoundary = origin.y - anchorHeight * 0.5 + (this.removePadding ? topPadding : 0)
const bottomBoundary = origin.y + anchorHeight * 0.5 - (this.removePadding ? bottomPadding : 0)
if (bottomBoundary + content.offsetHeight > yBounds.max) usingTop = true
if (topBoundary - content.offsetHeight < yBounds.min) usingTop = false
const yOffset = (this.offset && this.offset.y) || 0
translateY = usingTop
? topBoundary - yOffset - content.offsetHeight
: bottomBoundary + yOffset
const xOffset = (this.offset && this.offset.x) || 0
translateX = origin.x + horizOffset + xOffset
}
// Note, separate translateX and translateY avoids blurry text on chromium,
// single translate or translate3d resulted in blurry text.
this.styles = { this.styles = {
opacity: 1, left: `${Math.round(translateX)}px`,
transform: `translateX(${Math.round(translateX)}px) translateY(${Math.round(translateY)}px)` top: `${Math.round(translateY)}px`
}
if (this.popoversZLayer) {
this.styles['--ZI_popover_override'] = `var(--ZI_${this.popoversZLayer}_popovers)`
}
if (parentScreenBox) {
this.styles.maxWidth = `${Math.round(parentScreenBox.width)}px`
} }
}, },
showPopover () { showPopover () {
if (this.disabled) return
const wasHidden = this.hidden const wasHidden = this.hidden
this.hidden = false this.hidden = false
this.parentPopover && this.parentPopover.onChildPopoverState(this, true)
if (this.trigger === 'click' || this.stayOnClick) {
document.addEventListener('click', this.onClickOutside)
}
this.scrollable.addEventListener('scroll', this.onScroll)
this.scrollable.addEventListener('resize', this.onResize)
this.$nextTick(() => { this.$nextTick(() => {
if (wasHidden) this.$emit('show') if (wasHidden) this.$emit('show')
this.updateStyles() this.updateStyles()
}) })
}, },
hidePopover () { hidePopover () {
if (this.disabled) return
if (!this.hidden) this.$emit('close') if (!this.hidden) this.$emit('close')
this.hidden = true this.hidden = true
this.styles = { opacity: 0 } this.parentPopover && this.parentPopover.onChildPopoverState(this, false)
if (this.trigger === 'click') {
document.removeEventListener('click', this.onClickOutside)
}
this.scrollable.removeEventListener('scroll', this.onScroll)
this.scrollable.removeEventListener('resize', this.onResize)
}, },
onMouseenter (e) { onMouseenter (e) {
if (this.trigger === 'hover') this.showPopover() if (this.trigger === 'hover') {
this.lockReEntry = false
clearTimeout(this.graceTimeout)
this.graceTimeout = null
this.showPopover()
}
}, },
onMouseleave (e) { onMouseleave (e) {
if (this.trigger === 'hover') this.hidePopover() if (this.trigger === 'hover' && this.childrenShown.size === 0) {
this.graceTimeout = setTimeout(() => this.hidePopover(), 1)
}
},
onMouseenterContent (e) {
if (this.trigger === 'hover' && !this.lockReEntry) {
this.lockReEntry = true
clearTimeout(this.graceTimeout)
this.graceTimeout = null
this.showPopover()
}
},
onMouseleaveContent (e) {
if (this.trigger === 'hover' && this.childrenShown.size === 0) {
this.graceTimeout = setTimeout(() => this.hidePopover(), 1)
}
}, },
onClick (e) { onClick (e) {
if (this.trigger === 'click') { if (this.trigger === 'click') {
@ -160,8 +262,24 @@ const Popover = {
}, },
onClickOutside (e) { onClickOutside (e) {
if (this.hidden) return if (this.hidden) return
if (this.$refs.content && this.$refs.content.contains(e.target)) return
if (this.$el.contains(e.target)) return if (this.$el.contains(e.target)) return
if (this.childrenShown.size > 0) return
this.hidePopover() this.hidePopover()
if (this.parentPopover) this.parentPopover.onClickOutside(e)
},
onScroll (e) {
this.updateStyles()
},
onResize (e) {
this.updateStyles()
},
onChildPopoverState (childRef, state) {
if (state) {
this.childrenShown.add(childRef)
} else {
this.childrenShown.delete(childRef)
}
} }
}, },
updated () { updated () {
@ -175,11 +293,18 @@ const Popover = {
this.oldSize = { width: content.offsetWidth, height: content.offsetHeight } this.oldSize = { width: content.offsetWidth, height: content.offsetHeight }
} }
}, },
created () { mounted () {
document.addEventListener('click', this.onClickOutside) let scrollable = this.$refs.trigger.closest('.column.-scrollable') ||
this.$refs.trigger.closest('.mobile-notifications')
if (!scrollable) scrollable = window
this.scrollable = scrollable
let parent = this.$parent
while (parent && parent.$.type.name !== 'Popover') {
parent = parent.$parent
}
this.parentPopover = parent
}, },
unmounted () { beforeUnmount () {
document.removeEventListener('click', this.onClickOutside)
this.hidePopover() this.hidePopover()
} }
} }

View File

@ -1,5 +1,5 @@
<template> <template>
<div <span
@mouseenter="onMouseenter" @mouseenter="onMouseenter"
@mouseleave="onMouseleave" @mouseleave="onMouseleave"
> >
@ -11,20 +11,27 @@
> >
<slot name="trigger" /> <slot name="trigger" />
</button> </button>
<div <teleport to="#popovers">
v-if="!hidden" <transition name="fade">
ref="content" <div
:style="styles" v-if="!hidden"
class="popover" ref="content"
:class="popoverClass || 'popover-default'" :style="styles"
> class="popover"
<slot :class="popoverClass || 'popover-default'"
name="content" @mouseenter="onMouseenterContent"
class="popover-inner" @mouseleave="onMouseleaveContent"
:close="hidePopover" @click="onClickContent"
/> >
</div> <slot
</div> name="content"
class="popover-inner"
:close="hidePopover"
/>
</div>
</transition>
</teleport>
</span>
</template> </template>
<script src="./popover.js" /> <script src="./popover.js" />
@ -37,14 +44,15 @@
} }
.popover { .popover {
z-index: 500; z-index: var(--ZI_popover_override, var(--ZI_popovers));
position: absolute; position: fixed;
min-width: 0; min-width: 0;
max-width: calc(100vw - 20px);
box-shadow: 2px 2px 3px rgba(0, 0, 0, 0.5);
box-shadow: var(--popupShadow);
} }
.popover-default { .popover-default {
transition: opacity 0.3s;
&:after { &:after {
content: ''; content: '';
position: absolute; position: absolute;
@ -80,7 +88,7 @@
text-align: left; text-align: left;
list-style: none; list-style: none;
max-width: 100vw; max-width: 100vw;
z-index: 200; z-index: var(--ZI_popover_override, var(--ZI_popovers));
white-space: nowrap; white-space: nowrap;
.dropdown-divider { .dropdown-divider {

View File

@ -6,6 +6,7 @@
:offset="{ y: 5 }" :offset="{ y: 5 }"
:bound-to="{ x: 'container' }" :bound-to="{ x: 'container' }"
remove-padding remove-padding
popover-class="ReactButton popover-default"
@show="focusInput" @show="focusInput"
> >
<template v-slot:content="{close}"> <template v-slot:content="{close}">
@ -41,7 +42,7 @@
</div> </div>
</template> </template>
<template v-slot:trigger> <template v-slot:trigger>
<button <span
class="button-unstyled popover-trigger" class="button-unstyled popover-trigger"
:title="$t('tool_tip.add_reaction')" :title="$t('tool_tip.add_reaction')"
> >
@ -49,7 +50,7 @@
class="fa-scale-110 fa-old-padding" class="fa-scale-110 fa-old-padding"
:icon="['far', 'smile-beam']" :icon="['far', 'smile-beam']"
/> />
</button> </span>
</template> </template>
</Popover> </Popover>
</template> </template>

View File

@ -41,11 +41,11 @@ export default {
.ModifiedIndicator { .ModifiedIndicator {
display: inline-block; display: inline-block;
position: relative; position: relative;
}
.modified-tooltip { .modified-tooltip {
margin: 0.5em 1em; margin: 0.5em 1em;
min-width: 10em; min-width: 10em;
text-align: center; text-align: center;
}
} }
</style> </style>

View File

@ -41,11 +41,11 @@ export default {
.ServerSideIndicator { .ServerSideIndicator {
display: inline-block; display: inline-block;
position: relative; position: relative;
}
.serverside-tooltip { .serverside-tooltip {
margin: 0.5em 1em; margin: 0.5em 1em;
min-width: 10em; min-width: 10em;
text-align: center; text-align: center;
}
} }
</style> </style>

View File

@ -74,6 +74,16 @@
{{ $t('settings.show_scrollbars') }} {{ $t('settings.show_scrollbars') }}
</BooleanSetting> </BooleanSetting>
</li> </li>
<li>
<BooleanSetting path="userPopoverZoom" expert="1">
{{ $t('settings.user_popover_avatar_zoom') }}
</BooleanSetting>
</li>
<li>
<BooleanSetting path="userPopoverOverlay" expert="1">
{{ $t('settings.user_popover_avatar_overlay') }}
</BooleanSetting>
</li>
<li> <li>
<ChoiceSetting <ChoiceSetting
v-if="user" v-if="user"
@ -261,18 +271,14 @@
{{ $t('settings.mention_link_display') }} {{ $t('settings.mention_link_display') }}
</ChoiceSetting> </ChoiceSetting>
</li> </li>
<ul <li>
class="setting-list suboptions" <BooleanSetting
> path="mentionLinkShowTooltip"
<li v-if="mentionLinkDisplay === 'short'"> expert="1"
<BooleanSetting >
path="mentionLinkShowTooltip" {{ $t('settings.mention_link_use_tooltip') }}
expert="1" </BooleanSetting>
> </li>
{{ $t('settings.mention_link_show_tooltip') }}
</BooleanSetting>
</li>
</ul>
<li> <li>
<BooleanSetting <BooleanSetting
path="useAtIcon" path="useAtIcon"

View File

@ -80,7 +80,7 @@
.floating-shout { .floating-shout {
position: fixed; position: fixed;
bottom: 0.5em; bottom: 0.5em;
z-index: 1000; z-index: var(--ZI_popovers);
max-width: 25em; max-width: 25em;
&.-left { &.-left {

View File

@ -211,7 +211,7 @@
.side-drawer-container { .side-drawer-container {
position: fixed; position: fixed;
z-index: 1000; z-index: var(--ZI_navbar);
top: 0; top: 0;
left: 0; left: 0;
width: 100%; width: 100%;

View File

@ -4,13 +4,13 @@ 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 RichContent from 'src/components/rich_content/rich_content.jsx' import RichContent from 'src/components/rich_content/rich_content.jsx'
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 MentionsLine from 'src/components/mentions_line/mentions_line.vue' import MentionsLine from 'src/components/mentions_line/mentions_line.vue'
@ -105,7 +105,6 @@ const Status = {
RetweetButton, RetweetButton,
ExtraButtons, ExtraButtons,
PostStatusForm, PostStatusForm,
UserCard,
UserAvatar, UserAvatar,
AvatarList, AvatarList,
Timeago, Timeago,
@ -115,7 +114,8 @@ const Status = {
StatusContent, StatusContent,
RichContent, RichContent,
MentionLink, MentionLink,
MentionsLine MentionsLine,
UserPopover
}, },
props: [ props: [
'statusoid', 'statusoid',

View File

@ -122,27 +122,22 @@
v-if="!noHeading" v-if="!noHeading"
class="left-side" class="left-side"
> >
<a <a :href="$router.resolve(userProfileLink).href" @click.prevent>
:href="$router.resolve(userProfileLink).href" <UserPopover
@click.stop.prevent.capture="toggleUserExpanded" :userId="status.user.id"
> :overlayCenters="true"
<UserAvatar >
class="post-avatar" <UserAvatar
:bot="botIndicator" class="post-avatar"
:compact="compact" :bot="botIndicator"
:better-shadow="betterShadow" :compact="compact"
:user="status.user" :better-shadow="betterShadow"
/> :user="status.user"
/>
</UserPopover>
</a> </a>
</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"
@ -322,6 +317,7 @@
class="mentions-line-first" class="mentions-line-first"
/> />
</span> </span>
{{ ' ' }}
<MentionsLine <MentionsLine
v-if="hasMentionsLine" v-if="hasMentionsLine"
:mentions="mentionsLine.slice(1)" :mentions="mentionsLine.slice(1)"

View File

@ -38,6 +38,13 @@ const StatusPopover = {
.catch(e => (this.error = true)) .catch(e => (this.error = true))
} }
} }
},
watch: {
status (newStatus, oldStatus) {
if (newStatus !== oldStatus) {
this.$nextTick(() => this.$refs.popover.updateStyles())
}
}
} }
} }

View File

@ -1,9 +1,11 @@
<template> <template>
<Popover <Popover
trigger="hover" trigger="hover"
:stay-on-click="true"
popover-class="popover-default status-popover" popover-class="popover-default status-popover"
:bound-to="{ x: 'container' }" :bound-to="{ x: 'container' }"
@show="enter" @show="enter"
ref="popover"
> >
<template v-slot:trigger> <template v-slot:trigger>
<slot /> <slot />
@ -52,8 +54,6 @@
border-width: 1px; border-width: 1px;
border-radius: $fallback--tooltipRadius; border-radius: $fallback--tooltipRadius;
border-radius: var(--tooltipRadius, $fallback--tooltipRadius); border-radius: var(--tooltipRadius, $fallback--tooltipRadius);
box-shadow: 2px 2px 3px rgba(0, 0, 0, 0.5);
box-shadow: var(--popupShadow);
/* TODO cleanup this */ /* TODO cleanup this */
.Status.Status { .Status.Status {

View File

@ -3,19 +3,17 @@
trigger="click" trigger="click"
class="TimelineMenu" class="TimelineMenu"
:class="{ 'open': isOpen }" :class="{ 'open': isOpen }"
:margin="{ left: -15, right: -200 }"
:bound-to="{ x: 'container' }" :bound-to="{ x: 'container' }"
popover-class="timeline-menu-popover-wrap" bound-to-selector=".Timeline"
popover-class="timeline-menu-popover popover-default"
@show="openMenu" @show="openMenu"
@close="() => isOpen = false" @close="() => isOpen = false"
> >
<template v-slot:content> <template v-slot:content>
<div class="timeline-menu-popover popover-default"> <TimelineMenuContent />
<TimelineMenuContent />
</div>
</template> </template>
<template v-slot:trigger> <template v-slot:trigger>
<button class="button-unstyled title timeline-menu-title"> <span class="button-unstyled title timeline-menu-title">
<span class="timeline-title">{{ timelineName() }}</span> <span class="timeline-title">{{ timelineName() }}</span>
<span> <span>
<FAIcon <FAIcon
@ -27,7 +25,7 @@
class="click-blocker" class="click-blocker"
@click="blockOpen" @click="blockOpen"
/> />
</button> </span>
</template> </template>
</Popover> </Popover>
</template> </template>
@ -38,42 +36,18 @@
@import '../../_variables.scss'; @import '../../_variables.scss';
.TimelineMenu { .TimelineMenu {
flex-shrink: 1;
margin-right: auto; margin-right: auto;
min-width: 0; min-width: 0;
width: 24rem;
.popover-trigger-button { .popover-trigger-button {
vertical-align: bottom; vertical-align: bottom;
} }
.timeline-menu-popover-wrap {
overflow: hidden;
// Match panel heading padding to line up menu with bottom of heading
margin-top: 0.6rem;
padding: 0 15px 15px 15px;
}
.timeline-menu-popover {
width: 24rem;
max-width: 100vw;
margin: 0;
font-size: 1rem;
border-top-right-radius: 0;
border-top-left-radius: 0;
transform: translateY(-100%);
transition: transform 100ms;
}
.panel::after { .panel::after {
border-top-right-radius: 0; border-top-right-radius: 0;
border-top-left-radius: 0; border-top-left-radius: 0;
} }
&.open .timeline-menu-popover {
transform: translateY(0);
}
.timeline-menu-title { .timeline-menu-title {
margin: 0; margin: 0;
cursor: pointer; cursor: pointer;
@ -108,6 +82,16 @@
box-shadow: var(--popoverShadow); box-shadow: var(--popoverShadow);
} }
}
.timeline-menu-popover {
min-width: 24rem;
max-width: 100vw;
margin-top: 0.6rem;
font-size: 1rem;
border-top-right-radius: 0;
border-top-left-radius: 0;
ul { ul {
list-style: none; list-style: none;
margin: 0; margin: 0;
@ -134,7 +118,9 @@
a { a {
display: block; display: block;
padding: 0.6em 0.65em; padding: 0 0.65em;
height: 3.5em;
line-height: 3.5em;
&:hover { &:hover {
background-color: $fallback--lightBg; background-color: $fallback--lightBg;

View File

@ -14,7 +14,9 @@ import {
faRss, faRss,
faSearchPlus, faSearchPlus,
faExternalLinkAlt, faExternalLinkAlt,
faEdit faEdit,
faTimes,
faExpandAlt
} from '@fortawesome/free-solid-svg-icons' } from '@fortawesome/free-solid-svg-icons'
library.add( library.add(
@ -22,12 +24,21 @@ library.add(
faBell, faBell,
faSearchPlus, faSearchPlus,
faExternalLinkAlt, faExternalLinkAlt,
faEdit faEdit,
faTimes,
faExpandAlt
) )
export default { export default {
props: [ props: [
'userId', 'switcher', 'selected', 'hideBio', 'rounded', 'bordered', 'allowZoomingAvatar' 'userId',
'switcher',
'selected',
'hideBio',
'rounded',
'bordered',
'avatarAction', // default - open profile, 'zoom' - zoom, function - call function
'onClose'
], ],
data () { data () {
return { return {
@ -47,9 +58,10 @@ export default {
}, },
classes () { classes () {
return [{ return [{
'user-card-rounded-t': this.rounded === 'top', // set border-top-left-radius and border-top-right-radius '-rounded-t': this.rounded === 'top', // set border-top-left-radius and border-top-right-radius
'user-card-rounded': this.rounded === true, // set border-radius for all sides '-rounded': this.rounded === true, // set border-radius for all sides
'user-card-bordered': this.bordered === true // set border for all sides '-bordered': this.bordered === true, // set border for all sides
'-popover': !!this.onClose // set popover rounding
}] }]
}, },
style () { style () {
@ -170,6 +182,12 @@ export default {
}, },
mentionUser () { mentionUser () {
this.$store.dispatch('openPostStatusModal', { replyTo: true, repliedUser: this.user }) this.$store.dispatch('openPostStatusModal', { replyTo: true, repliedUser: this.user })
},
onAvatarClickHandler (e) {
if (this.onAvatarClick) {
e.preventDefault()
this.onAvatarClick()
}
} }
} }
} }

View File

@ -42,8 +42,10 @@
mask-composite: exclude; mask-composite: exclude;
background-size: cover; background-size: cover;
mask-size: 100% 60%; mask-size: 100% 60%;
border-top-left-radius: calc(var(--panelRadius) - 1px); border-top-left-radius: calc(var(--__roundnessTop, --panelRadius) - 1px);
border-top-right-radius: calc(var(--panelRadius) - 1px); border-top-right-radius: calc(var(--__roundnessTop, --panelRadius) - 1px);
border-bottom-left-radius: calc(var(--__roundnessBottom, --panelRadius) - 1px);
border-bottom-right-radius: calc(var(--__roundnessBottom, --panelRadius) - 1px);
background-color: var(--profileBg); background-color: var(--profileBg);
z-index: -2; z-index: -2;
@ -72,21 +74,33 @@
} }
} }
// Modifiers &.-rounded-t {
&-rounded-t {
border-top-left-radius: $fallback--panelRadius; border-top-left-radius: $fallback--panelRadius;
border-top-left-radius: var(--panelRadius, $fallback--panelRadius); border-top-left-radius: var(--panelRadius, $fallback--panelRadius);
border-top-right-radius: $fallback--panelRadius; border-top-right-radius: $fallback--panelRadius;
border-top-right-radius: var(--panelRadius, $fallback--panelRadius); border-top-right-radius: var(--panelRadius, $fallback--panelRadius);
--__roundnessTop: var(--panelRadius);
--__roundnessBottom: 0;
} }
&-rounded { &.-rounded {
border-radius: $fallback--panelRadius; border-radius: $fallback--panelRadius;
border-radius: var(--panelRadius, $fallback--panelRadius); border-radius: var(--panelRadius, $fallback--panelRadius);
--__roundnessTop: var(--panelRadius);
--__roundnessBottom: var(--panelRadius);
} }
&-bordered { &.-popover {
border-radius: $fallback--tooltipRadius;
border-radius: var(--tooltipRadius, $fallback--tooltipRadius);
--__roundnessTop: var(--tooltipRadius);
--__roundnessBottom: var(--tooltipRadius);
}
&.-bordered {
border-width: 1px; border-width: 1px;
border-style: solid; border-style: solid;
border-color: $fallback--border; border-color: $fallback--border;
@ -99,6 +113,15 @@
color: var(--lightText, $fallback--lightText); color: var(--lightText, $fallback--lightText);
padding: 0 26px; padding: 0 26px;
a {
color: $fallback--lightText;
color: var(--lightText, $fallback--lightText);
&:hover {
color: var(--icon);
}
}
.container { .container {
min-width: 0; min-width: 0;
padding: 16px 0 6px; padding: 16px 0 6px;
@ -110,23 +133,27 @@
min-width: 0; min-width: 0;
} }
> a {
vertical-align: middle;
display: flex;
}
.Avatar { .Avatar {
--_avatarShadowBox: var(--avatarShadow); --_avatarShadowBox: var(--avatarShadow);
--_avatarShadowFilter: var(--avatarShadowFilter); --_avatarShadowFilter: var(--avatarShadowFilter);
--_avatarShadowInset: var(--avatarShadowInset); --_avatarShadowInset: var(--avatarShadowInset);
flex: 1 0 100%;
width: 56px; width: 56px;
height: 56px; height: 56px;
object-fit: cover; object-fit: cover;
} }
} }
&-avatar-link { &-avatar {
position: relative; position: relative;
cursor: pointer; cursor: pointer;
&-overlay { &.-overlay {
position: absolute; position: absolute;
left: 0; left: 0;
top: 0; top: 0;
@ -146,7 +173,7 @@
} }
} }
&:hover &-overlay { &:hover &.-overlay {
opacity: 1; opacity: 1;
} }
} }
@ -206,8 +233,6 @@
flex: 0 1 auto; flex: 0 1 auto;
text-overflow: ellipsis; text-overflow: ellipsis;
overflow: hidden; overflow: hidden;
color: $fallback--lightText;
color: var(--lightText, $fallback--lightText);
} }
.dailyAvg { .dailyAvg {

View File

@ -8,25 +8,32 @@
:style="style" :style="style"
class="background-image" class="background-image"
/> />
<div class="panel-heading -flexible-height"> <div :class="onClose ? '' : panel-heading -flexible-height">
<div class="user-info"> <div class="user-info">
<div class="container"> <div class="container">
<a <a
v-if="allowZoomingAvatar" v-if="avatarAction === 'zoom'"
class="user-info-avatar-link" class="user-info-avatar -link"
@click="zoomAvatar" @click="zoomAvatar"
> >
<UserAvatar <UserAvatar
:better-shadow="betterShadow" :better-shadow="betterShadow"
:user="user" :user="user"
/> />
<div class="user-info-avatar-link-overlay"> <div class="user-info-avatar -link -overlay">
<FAIcon <FAIcon
class="fa-scale-110 fa-old-padding" class="fa-scale-110 fa-old-padding"
icon="search-plus" icon="search-plus"
/> />
</div> </div>
</a> </a>
<UserAvatar
v-else-if="typeof avatarAction === 'function'"
@click="avatarAction"
class="user-info-avatar"
:better-shadow="betterShadow"
:user="user"
/>
<router-link <router-link
v-else v-else
:to="userProfileLink(user)" :to="userProfileLink(user)"
@ -38,12 +45,16 @@
</router-link> </router-link>
<div class="user-summary"> <div class="user-summary">
<div class="top-line"> <div class="top-line">
<RichContent <router-link
:title="user.name" :to="userProfileLink(user)"
class="user-name" class="user-name"
:html="user.name" >
:emoji="user.emoji" <RichContent
/> :title="user.name"
:html="user.name"
:emoji="user.emoji"
/>
</router-link>
<button <button
v-if="!isOtherUser && user.is_local" v-if="!isOtherUser && user.is_local"
class="button-unstyled edit-profile-button" class="button-unstyled edit-profile-button"
@ -72,6 +83,27 @@
:user="user" :user="user"
:relationship="relationship" :relationship="relationship"
/> />
<router-link
v-if="onClose"
:to="userProfileLink(user)"
class="button-unstyled external-link-button"
@click="onClose"
>
<FAIcon
class="icon"
icon="expand-alt"
/>
</router-link>
<button
v-if="onClose"
class="button-unstyled external-link-button"
@click="onClose"
>
<FAIcon
class="icon"
icon="times"
/>
</button>
</div> </div>
<div class="bottom-line"> <div class="bottom-line">
<router-link <router-link

View File

@ -0,0 +1,23 @@
import UserCard from '../user_card/user_card.vue'
import { defineAsyncComponent } from 'vue'
const UserPopover = {
name: 'UserPopover',
props: [
'userId', 'overlayCenters', 'disabled', 'overlayCentersSelector'
],
components: {
UserCard,
Popover: defineAsyncComponent(() => import('../popover/popover.vue'))
},
computed: {
userPopoverZoom () {
return this.$store.getters.mergedConfig.userPopoverZoom
},
userPopoverOverlay () {
return this.$store.getters.mergedConfig.userPopoverOverlay
}
}
}
export default UserPopover

View File

@ -0,0 +1,33 @@
<template>
<Popover
trigger="click"
popover-class="popover-default user-popover"
:overlay-centers-selector="overlayCentersSelector || '.user-info .Avatar'"
:overlay-centers="overlayCenters && userPopoverOverlay"
:disabled="disabled"
>
<template v-slot:trigger>
<slot />
</template>
<template v-slot:content={close}>
<UserCard
class="user-popover"
:user-id="userId"
:hide-bio="true"
:avatar-action="userPopoverZoom ? 'zoom' : close"
:on-close="close"
/>
</template>
</Popover>
</template>
<script src="./user_popover.js" ></script>
<style lang="scss">
@import '../../_variables.scss';
/* popover styles load on-demand, so we need to override */
.user-popover.popover {
}
</style>

View File

@ -8,7 +8,7 @@
:user-id="userId" :user-id="userId"
:switcher="true" :switcher="true"
:selected="timeline.viewing" :selected="timeline.viewing"
:allow-zooming-avatar="true" avatar-action="zoom"
rounded="top" rounded="top"
/> />
<div <div

View File

@ -546,10 +546,12 @@
"mention_link_display_short": "always as short names (e.g. {'@'}foo)", "mention_link_display_short": "always as short names (e.g. {'@'}foo)",
"mention_link_display_full_for_remote": "as full names only for remote users (e.g. {'@'}foo{'@'}example.org)", "mention_link_display_full_for_remote": "as full names only for remote users (e.g. {'@'}foo{'@'}example.org)",
"mention_link_display_full": "always as full names (e.g. {'@'}foo{'@'}example.org)", "mention_link_display_full": "always as full names (e.g. {'@'}foo{'@'}example.org)",
"mention_link_show_tooltip": "Show full user names as tooltip for remote users", "mention_link_use_tooltip": "Show user card when clicking mention links",
"mention_link_show_avatar": "Show user avatar beside the link", "mention_link_show_avatar": "Show user avatar beside the link",
"mention_link_fade_domain": "Fade domains (e.g. {'@'}example.org in {'@'}foo{'@'}example.org)", "mention_link_fade_domain": "Fade domains (e.g. {'@'}example.org in {'@'}foo{'@'}example.org)",
"mention_link_bolden_you": "Highlight mention of you when you are mentioned", "mention_link_bolden_you": "Highlight mention of you when you are mentioned",
"user_popover_avatar_zoom": "Clicking on user avatar in popover zooms it instead of closing the popover",
"user_popover_avatar_overlay": "Show user popover over user avatar",
"fun": "Fun", "fun": "Fun",
"greentext": "Meme arrows", "greentext": "Meme arrows",
"show_yous": "Show (You)s", "show_yous": "Show (You)s",

View File

@ -81,6 +81,8 @@ export const defaultState = {
useContainFit: true, useContainFit: true,
disableStickyHeaders: false, disableStickyHeaders: false,
showScrollbars: false, showScrollbars: false,
userPopoverZoom: false,
userPopoverOverlay: true,
greentext: undefined, // instance default greentext: undefined, // instance default
useAtIcon: undefined, // instance default useAtIcon: undefined, // instance default
mentionLinkDisplay: undefined, // instance default mentionLinkDisplay: undefined, // instance default

View File

@ -4,7 +4,15 @@ import RichContent from 'src/components/rich_content/rich_content.jsx'
const attentions = [] const attentions = []
const global = { const global = {
mocks: { mocks: {
'$store': null '$store': {
state: {},
getters: {
mergedConfig: () => ({
mentionLinkShowTooltip: true
}),
findUserByUrl: () => null
}
}
}, },
stubs: { stubs: {
FAIcon: true FAIcon: true
@ -131,8 +139,7 @@ describe('RichContent', () => {
].join(''), ].join(''),
[ [
makeMention('John'), makeMention('John'),
makeMention('Josh'), makeMention('Josh'), makeMention('Jeremy')
makeMention('Jeremy')
].join('') ].join('')
].join('\n') ].join('\n')
@ -349,7 +356,6 @@ describe('RichContent', () => {
p( p(
'<span class="MentionsLine">', '<span class="MentionsLine">',
'<span class="MentionLink mention-link">', '<span class="MentionLink mention-link">',
'<!-- eslint-disable vue/no-v-html -->',
'<a href="lol" class="original" target="_blank">', '<a href="lol" class="original" target="_blank">',
'<span>', '<span>',
'https://</span>', 'https://</span>',
@ -358,10 +364,7 @@ describe('RichContent', () => {
'<span>', '<span>',
'</span>', '</span>',
'</a>', '</a>',
'<!-- eslint-enable vue/no-v-html -->',
'<!--v-if-->', // v-if placeholder, mentionlink's "new" (i.e. rich) display
'</span>', '</span>',
'<!--v-if-->', // v-if placeholder, mentionsline's extra mentions and stuff
'</span>' '</span>'
), ),
p( p(
@ -380,7 +383,7 @@ describe('RichContent', () => {
} }
}) })
expect(wrapper.html().replace(/\n/g, '')).to.eql(compwrap(expected)) expect(wrapper.html().replace(/\n/g, '').replace(/<!--.*?-->/g, '')).to.eql(compwrap(expected))
}) })
it('rich contents of nested mentions are handled properly', () => { it('rich contents of nested mentions are handled properly', () => {
@ -412,7 +415,6 @@ describe('RichContent', () => {
'<span class="poast-style">', '<span class="poast-style">',
'<span class="MentionsLine">', '<span class="MentionsLine">',
'<span class="MentionLink mention-link">', '<span class="MentionLink mention-link">',
'<!-- eslint-disable vue/no-v-html -->',
'<a href="lol" class="original" target="_blank">', '<a href="lol" class="original" target="_blank">',
'<span>', '<span>',
'https://</span>', 'https://</span>',
@ -421,11 +423,8 @@ describe('RichContent', () => {
'<span>', '<span>',
'</span>', '</span>',
'</a>', '</a>',
'<!-- eslint-enable vue/no-v-html -->',
'<!--v-if-->', // v-if placeholder, mentionlink's "new" (i.e. rich) display
'</span>', '</span>',
'<span class="MentionLink mention-link">', '<span class="MentionLink mention-link">',
'<!-- eslint-disable vue/no-v-html -->',
'<a href="lol" class="original" target="_blank">', '<a href="lol" class="original" target="_blank">',
'<span>', '<span>',
'https://</span>', 'https://</span>',
@ -434,10 +433,7 @@ describe('RichContent', () => {
'<span>', '<span>',
'</span>', '</span>',
'</a>', '</a>',
'<!-- eslint-enable vue/no-v-html -->',
'<!--v-if-->', // v-if placeholder, mentionlink's "new" (i.e. rich) display
'</span>', '</span>',
'<!--v-if-->', // v-if placeholder, mentionsline's extra mentions and stuff
'</span>', '</span>',
' ', ' ',
'</span>', '</span>',
@ -455,7 +451,7 @@ describe('RichContent', () => {
} }
}) })
expect(wrapper.html().replace(/\n/g, '')).to.eql(compwrap(expected)) expect(wrapper.html().replace(/\n/g, '').replace(/<!--.*?-->/g, '')).to.eql(compwrap(expected))
}) })
it('rich contents of a link are handled properly', () => { it('rich contents of a link are handled properly', () => {