Merge branch 'disjointed-popovers' into eslint-update
* disjointed-popovers: (56 commits) fix typo fix errors in console pinned no longer needed popover stack add stay-on-click prop to solve case of clicking user avatar in status popover fix settings tooltips vertical nudge for popovers, especially for overlay-centers ones make user popover options expert use same sizing for timeline dropdown as in the main nav fix avatar not zooming in profile page fix spacing in mentionsline add popovers to chats fix avatar not closing, add option to put popovers next to avatar instead of over it fix the incorrect rounding in nav list re-unfuck the timeline popover Revert "unify styling of timelines dropdown with other dropdown menus" close on avatar click again, add zooming as option fix basicusercard make hover popovers less annoying to close move tooltips setting ...
This commit is contained in:
commit
1cf7af3374
12
CHANGELOG.md
12
CHANGELOG.md
|
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
11
src/App.scss
11
src/App.scss
|
@ -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,
|
||||||
|
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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'
|
||||||
},
|
},
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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));
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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}`
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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 () {
|
||||||
|
|
|
@ -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 -->
|
||||||
|
|
|
@ -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)
|
||||||
},
|
},
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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%;
|
||||||
|
|
|
@ -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',
|
||||||
|
|
|
@ -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)"
|
||||||
|
|
|
@ -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())
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
|
@ -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>
|
|
@ -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
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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', () => {
|
||||||
|
|
Loading…
Reference in New Issue