Merge branch 'disjointed-popovers' into 'develop'
Disjointed popovers See merge request pleroma/pleroma-fe!1540
This commit is contained in:
commit
33ad712852
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"
|
||||
- Attachment description is prefilled with backend-provided default when uploading
|
||||
- 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
|
||||
- Using Vue 3 now
|
||||
- (You)s are optional (opt-in) now, bolding your nickname is also optional (opt-out)
|
||||
- User highlight background now also covers the `@`
|
||||
- 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
|
||||
- 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.
|
||||
- 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
|
||||
- 3 column mode: only enables when there's space for it (opt-out, customizable)
|
||||
- Options to show domains in mentions
|
||||
- Option to show user avatars in mention links (opt-in)
|
||||
- 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)
|
||||
- Ability to rearrange order of attachments when uploading
|
||||
- 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
|
||||
|
||||
|
||||
|
@ -4,7 +4,6 @@ import InstanceSpecificPanel from './components/instance_specific_panel/instance
|
||||
import FeaturesPanel from './components/features_panel/features_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 SettingsModal from './components/settings_modal/settings_modal.vue'
|
||||
import MediaModal from './components/media_modal/media_modal.vue'
|
||||
import SideDrawer from './components/side_drawer/side_drawer.vue'
|
||||
import MobilePostStatusButton from './components/mobile_post_status_button/mobile_post_status_button.vue'
|
||||
@ -32,7 +31,7 @@ export default {
|
||||
MobilePostStatusButton,
|
||||
MobileNav,
|
||||
DesktopNav,
|
||||
SettingsModal,
|
||||
SettingsModal: defineAsyncComponent(() => import('./components/settings_modal/settings_modal.vue')),
|
||||
UserReportingModal,
|
||||
PostStatusModal,
|
||||
GlobalNoticeList
|
||||
|
11
src/App.scss
11
src/App.scss
@ -4,6 +4,13 @@
|
||||
:root {
|
||||
--navbar-height: 3.5rem;
|
||||
--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 {
|
||||
@ -117,7 +124,7 @@ i[class*=icon-],
|
||||
}
|
||||
|
||||
nav {
|
||||
z-index: 1000;
|
||||
z-index: var(--ZI_navbar);
|
||||
color: var(--topBarText);
|
||||
background-color: $fallback--fg;
|
||||
background-color: var(--topBar, $fallback--fg);
|
||||
@ -828,7 +835,7 @@ option {
|
||||
// Vue transitions
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition: opacity 0.2s;
|
||||
transition: opacity 0.3s;
|
||||
}
|
||||
|
||||
.fade-enter-from,
|
||||
|
@ -42,7 +42,7 @@
|
||||
</div>
|
||||
<div id="notifs-column" class="column -scrollable" :class="{ '-show-scrollbar': showScrollbars }"/>
|
||||
</div>
|
||||
<media-modal />
|
||||
<MediaModal />
|
||||
<shout-panel
|
||||
v-if="currentUser && shout && !hideShoutbox"
|
||||
:floating="true"
|
||||
@ -55,6 +55,7 @@
|
||||
<SettingsModal />
|
||||
<div id="modal" />
|
||||
<GlobalNoticeList />
|
||||
<div id="popovers" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@ -396,6 +396,9 @@ const afterStoreSetup = async ({ store, i18n }) => {
|
||||
app.component('FAIcon', FontAwesomeIcon)
|
||||
app.component('FALayers', FontAwesomeLayers)
|
||||
|
||||
// remove after vue 3.3
|
||||
app.config.unwrapInjectedRef = true
|
||||
|
||||
app.mount('#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 RichContent from 'src/components/rich_content/rich_content.jsx'
|
||||
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
|
||||
@ -7,20 +7,12 @@ const BasicUserCard = {
|
||||
props: [
|
||||
'user'
|
||||
],
|
||||
data () {
|
||||
return {
|
||||
userExpanded: false
|
||||
}
|
||||
},
|
||||
components: {
|
||||
UserCard,
|
||||
UserPopover,
|
||||
UserAvatar,
|
||||
RichContent
|
||||
},
|
||||
methods: {
|
||||
toggleUserExpanded () {
|
||||
this.userExpanded = !this.userExpanded
|
||||
},
|
||||
userProfileLink (user) {
|
||||
return generateProfileLink(user.id, user.screen_name, this.$store.state.instance.restrictedNicknames)
|
||||
}
|
||||
|
@ -1,24 +1,19 @@
|
||||
<template>
|
||||
<div class="basic-user-card">
|
||||
<router-link :to="userProfileLink(user)">
|
||||
<router-link @click.prevent :to="userProfileLink(user)">
|
||||
<UserPopover
|
||||
:userId="user.id"
|
||||
:overlayCenters="true"
|
||||
overlayCentersSelector=".avatar"
|
||||
>
|
||||
<UserAvatar
|
||||
class="avatar"
|
||||
class="user-avatar avatar"
|
||||
:user="user"
|
||||
@click.prevent="toggleUserExpanded"
|
||||
@click.prevent
|
||||
/>
|
||||
</UserPopover>
|
||||
</router-link>
|
||||
<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"
|
||||
>
|
||||
<div
|
||||
@ -53,6 +48,8 @@
|
||||
margin: 0;
|
||||
padding: 0.6em 1em;
|
||||
|
||||
--emoji-size: 14px;
|
||||
|
||||
&-collapsed-content {
|
||||
margin-left: 0.7em;
|
||||
text-align: left;
|
||||
|
@ -6,7 +6,7 @@ import Gallery from '../gallery/gallery.vue'
|
||||
import LinkPreview from '../link-preview/link-preview.vue'
|
||||
import StatusContent from '../status_content/status_content.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 {
|
||||
faTimes,
|
||||
@ -35,7 +35,8 @@ const ChatMessage = {
|
||||
UserAvatar,
|
||||
Gallery,
|
||||
LinkPreview,
|
||||
ChatMessageDate
|
||||
ChatMessageDate,
|
||||
UserPopover: defineAsyncComponent(() => import('../user_popover/user_popover.vue'))
|
||||
},
|
||||
computed: {
|
||||
// Returns HH:MM (hours and minutes) in local time.
|
||||
@ -49,9 +50,6 @@ const ChatMessage = {
|
||||
message () {
|
||||
return this.chatViewItem.data
|
||||
},
|
||||
userProfileLink () {
|
||||
return generateProfileLink(this.author.id, this.author.screen_name, this.$store.state.instance.restrictedNicknames)
|
||||
},
|
||||
isMessage () {
|
||||
return this.chatViewItem.type === 'message'
|
||||
},
|
||||
|
@ -14,16 +14,16 @@
|
||||
v-if="!isCurrentUser"
|
||||
class="avatar-wrapper"
|
||||
>
|
||||
<router-link
|
||||
<UserPopover
|
||||
v-if="chatViewItem.isHead"
|
||||
:to="userProfileLink"
|
||||
:userId="author.id"
|
||||
>
|
||||
<UserAvatar
|
||||
:compact="true"
|
||||
:better-shadow="betterShadow"
|
||||
:user="author"
|
||||
/>
|
||||
</router-link>
|
||||
</UserPopover>
|
||||
</div>
|
||||
<div class="chat-message-inner">
|
||||
<div
|
||||
@ -44,7 +44,7 @@
|
||||
<Popover
|
||||
trigger="click"
|
||||
placement="top"
|
||||
:bound-to-selector="isCurrentUser ? '' : '.scrollable-message-list'"
|
||||
bound-to-selector=".chat-view-inner"
|
||||
:bound-to="{ x: 'container' }"
|
||||
:margin="popoverMarginStyle"
|
||||
@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 RichContent from 'src/components/rich_content/rich_content.jsx'
|
||||
import { defineAsyncComponent } from 'vue'
|
||||
|
||||
export default {
|
||||
name: 'ChatTitle',
|
||||
components: {
|
||||
UserAvatar,
|
||||
RichContent
|
||||
RichContent,
|
||||
UserPopover: defineAsyncComponent(() => import('../user_popover/user_popover.vue'))
|
||||
},
|
||||
props: [
|
||||
'user', 'withAvatar'
|
||||
@ -18,10 +19,5 @@ export default {
|
||||
htmlTitle () {
|
||||
return this.user ? this.user.name_html : ''
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getUserProfileLink (user) {
|
||||
return generateProfileLink(user.id, user.screen_name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -3,16 +3,16 @@
|
||||
class="chat-title"
|
||||
:title="title"
|
||||
>
|
||||
<router-link
|
||||
<UserPopover
|
||||
class="avatar-container"
|
||||
v-if="withAvatar && user"
|
||||
:to="getUserProfileLink(user)"
|
||||
:userId="user.id"
|
||||
>
|
||||
<UserAvatar
|
||||
class="titlebar-avatar"
|
||||
:user="user"
|
||||
/>
|
||||
</router-link>
|
||||
</UserPopover>
|
||||
<RichContent
|
||||
v-if="user"
|
||||
class="username"
|
||||
|
@ -2,6 +2,7 @@
|
||||
|
||||
.DesktopNav {
|
||||
width: 100%;
|
||||
z-index: var(--ZI_navbar);
|
||||
|
||||
input {
|
||||
color: var(--inputTopbarText, var(--inputText));
|
||||
|
@ -38,7 +38,7 @@
|
||||
/>
|
||||
<button
|
||||
class="button-unstyled nav-icon"
|
||||
@click.stop="openSettingsModal"
|
||||
@click="openSettingsModal"
|
||||
>
|
||||
<FAIcon
|
||||
fixed-width
|
||||
|
@ -7,7 +7,8 @@
|
||||
right: 0;
|
||||
left: 0;
|
||||
margin: 0 !important;
|
||||
z-index: 100;
|
||||
// TODO: actually use popover in emoji picker
|
||||
z-index: var(--ZI_popovers);
|
||||
background-color: $fallback--bg;
|
||||
background-color: var(--popover, $fallback--bg);
|
||||
color: $fallback--link;
|
||||
|
@ -89,6 +89,9 @@ const ExtraButtons = {
|
||||
canMute () {
|
||||
return !!this.currentUser
|
||||
},
|
||||
canBookmark () {
|
||||
return !!this.currentUser
|
||||
},
|
||||
statusLink () {
|
||||
return `${this.$store.state.instance.server}${this.$router.resolve({ name: 'conversation', params: { id: this.status.id } }).href}`
|
||||
}
|
||||
|
@ -51,6 +51,7 @@
|
||||
icon="thumbtack"
|
||||
/><span>{{ $t("status.unpin") }}</span>
|
||||
</button>
|
||||
<template v-if="canBookmark">
|
||||
<button
|
||||
v-if="!status.bookmarked"
|
||||
class="button-default dropdown-item dropdown-item-icon"
|
||||
@ -73,6 +74,7 @@
|
||||
icon="bookmark"
|
||||
/><span>{{ $t("status.unbookmark") }}</span>
|
||||
</button>
|
||||
</template>
|
||||
<button
|
||||
v-if="canDelete"
|
||||
class="button-default dropdown-item dropdown-item-icon"
|
||||
@ -119,12 +121,12 @@
|
||||
</div>
|
||||
</template>
|
||||
<template v-slot:trigger>
|
||||
<button class="button-unstyled popover-trigger">
|
||||
<span class="button-unstyled popover-trigger">
|
||||
<FAIcon
|
||||
class="fa-scale-110 fa-old-padding"
|
||||
icon="ellipsis-h"
|
||||
/>
|
||||
</button>
|
||||
</span>
|
||||
</template>
|
||||
</Popover>
|
||||
</template>
|
||||
|
@ -32,7 +32,7 @@
|
||||
top: 50px;
|
||||
width: 100%;
|
||||
pointer-events: none;
|
||||
z-index: 1001;
|
||||
z-index: var(--ZI_popovers);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
@ -121,7 +121,7 @@ $modal-view-button-icon-width: 3em;
|
||||
$modal-view-button-icon-margin: 0.5em;
|
||||
|
||||
.modal-view.media-modal-view {
|
||||
z-index: 9000;
|
||||
z-index: var(--ZI_media_modal);
|
||||
flex-direction: column;
|
||||
|
||||
.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 { highlightClass, highlightStyle } from '../../services/user_highlighter/user_highlighter.js'
|
||||
import UserAvatar from '../user_avatar/user_avatar.vue'
|
||||
import { defineAsyncComponent } from 'vue'
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import {
|
||||
faAt
|
||||
@ -14,7 +15,8 @@ library.add(
|
||||
const MentionLink = {
|
||||
name: 'MentionLink',
|
||||
components: {
|
||||
UserAvatar
|
||||
UserAvatar,
|
||||
UserPopover: defineAsyncComponent(() => import('../user_popover/user_popover.vue'))
|
||||
},
|
||||
props: {
|
||||
url: {
|
||||
@ -34,15 +36,30 @@ const MentionLink = {
|
||||
type: String
|
||||
}
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
hasSelection: false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onClick () {
|
||||
if (this.shouldShowTooltip) return
|
||||
const link = generateProfileLink(
|
||||
this.userId || this.user.id,
|
||||
this.userScreenName || this.user.screen_name
|
||||
)
|
||||
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: {
|
||||
user () {
|
||||
return this.url && this.$store && this.$store.getters.findUserByUrl(this.url)
|
||||
@ -88,7 +105,8 @@ const MentionLink = {
|
||||
return [
|
||||
{
|
||||
'-you': this.isYou && this.shouldBoldenYou,
|
||||
'-highlighted': this.highlight
|
||||
'-highlighted': this.highlight,
|
||||
'-has-selection': this.hasSelection
|
||||
},
|
||||
this.highlightType
|
||||
]
|
||||
@ -110,7 +128,7 @@ const MentionLink = {
|
||||
}
|
||||
},
|
||||
shouldShowTooltip () {
|
||||
return this.mergedConfig.mentionLinkShowTooltip && this.mergedConfig.mentionLinkDisplay === 'short' && this.isRemote
|
||||
return this.mergedConfig.mentionLinkShowTooltip
|
||||
},
|
||||
shouldShowAvatar () {
|
||||
return this.mergedConfig.mentionLinkShowAvatar
|
||||
|
@ -55,11 +55,14 @@
|
||||
|
||||
.new {
|
||||
&.-you {
|
||||
& .shortName,
|
||||
& .full {
|
||||
.shortName {
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
&.-has-selection {
|
||||
color: var(--alertNeutralText, $fallback--text);
|
||||
background-color: var(--alertNeutral, $fallback--fg);
|
||||
}
|
||||
|
||||
.at {
|
||||
color: var(--link);
|
||||
@ -72,8 +75,7 @@
|
||||
}
|
||||
|
||||
&.-striped {
|
||||
& .shortName,
|
||||
& .full {
|
||||
& .shortName {
|
||||
background-image:
|
||||
repeating-linear-gradient(
|
||||
135deg,
|
||||
@ -86,30 +88,29 @@
|
||||
}
|
||||
|
||||
&.-solid {
|
||||
& .shortName,
|
||||
& .full {
|
||||
.shortName {
|
||||
background-image: linear-gradient(var(--____highlight-tintColor2), var(--____highlight-tintColor2));
|
||||
}
|
||||
}
|
||||
|
||||
&.-side {
|
||||
& .shortName,
|
||||
& .userNameFull {
|
||||
.shortName {
|
||||
box-shadow: 0 -5px 3px -4px inset var(--____highlight-solidColor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:hover .new .full {
|
||||
opacity: 1;
|
||||
pointer-events: initial;
|
||||
.full {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.serverName.-faded {
|
||||
color: var(--faintLink, $fallback--link);
|
||||
}
|
||||
}
|
||||
|
||||
.full .-faded {
|
||||
color: var(--faint, $fallback--faint);
|
||||
}
|
||||
.mention-link-popover {
|
||||
max-width: 70ch;
|
||||
max-height: 20rem;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
@ -9,7 +9,13 @@
|
||||
class="original"
|
||||
target="_blank"
|
||||
v-html="content"
|
||||
/><!-- eslint-enable vue/no-v-html --><span
|
||||
/><!-- eslint-enable vue/no-v-html -->
|
||||
<UserPopover
|
||||
v-else
|
||||
:userId="user.id"
|
||||
:disabled="!shouldShowTooltip"
|
||||
>
|
||||
<span
|
||||
v-if="user"
|
||||
class="new"
|
||||
:style="style"
|
||||
@ -48,27 +54,13 @@
|
||||
: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"
|
||||
>
|
||||
</a><span class="full" ref="full">
|
||||
<!-- eslint-disable vue/no-v-html -->
|
||||
@<span
|
||||
class="userName"
|
||||
v-html="userName"
|
||||
/><span
|
||||
class="serverName"
|
||||
:class="{ '-faded': shouldFadeDomain }"
|
||||
v-html="'@' + serverName"
|
||||
/>
|
||||
@<span v-html="userName" /><span v-html="'@' + serverName" />
|
||||
<!-- eslint-enable vue/no-v-html -->
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
</UserPopover>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
|
@ -13,8 +13,7 @@
|
||||
<span
|
||||
v-if="expanded"
|
||||
class="fullExtraMentions"
|
||||
>
|
||||
<MentionLink
|
||||
>{{ ' ' }}<MentionLink
|
||||
v-for="mention in extraMentions"
|
||||
:key="mention.index"
|
||||
class="mention-link"
|
||||
|
@ -86,6 +86,8 @@
|
||||
@import '../../_variables.scss';
|
||||
|
||||
.MobileNav {
|
||||
z-index: var(--ZI_navbar);
|
||||
|
||||
.mobile-nav {
|
||||
display: grid;
|
||||
line-height: var(--navbar-height);
|
||||
@ -147,7 +149,7 @@
|
||||
transition-property: transform;
|
||||
transition-duration: 0.25s;
|
||||
transform: translateX(0);
|
||||
z-index: 1001;
|
||||
z-index: var(--ZI_navbar);
|
||||
-webkit-overflow-scrolling: touch;
|
||||
|
||||
&.-closed {
|
||||
@ -160,7 +162,7 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
z-index: 1;
|
||||
z-index: calc(var(--ZI_navbar) + 100);
|
||||
width: 100%;
|
||||
height: 50px;
|
||||
line-height: 50px;
|
||||
|
@ -22,6 +22,9 @@ export default {
|
||||
default: false
|
||||
}
|
||||
},
|
||||
provide: {
|
||||
popoversZLayer: 'modals'
|
||||
},
|
||||
computed: {
|
||||
classes () {
|
||||
return {
|
||||
@ -35,7 +38,7 @@ export default {
|
||||
|
||||
<style lang="scss">
|
||||
.modal-view {
|
||||
z-index: 2000;
|
||||
z-index: var(--ZI_modals);
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
|
@ -113,7 +113,9 @@
|
||||
border-color: $fallback--border;
|
||||
border-color: var(--border, $fallback--border);
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
> li {
|
||||
&:first-child .menu-item {
|
||||
border-top-right-radius: $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 Timeago from '../timeago/timeago.vue'
|
||||
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 { highlightClass, highlightStyle } from '../../services/user_highlighter/user_highlighter.js'
|
||||
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
|
||||
@ -46,7 +47,8 @@ const Notification = {
|
||||
UserCard,
|
||||
Timeago,
|
||||
Status,
|
||||
RichContent
|
||||
RichContent,
|
||||
UserPopover
|
||||
},
|
||||
methods: {
|
||||
toggleUserExpanded () {
|
||||
|
@ -34,21 +34,22 @@
|
||||
<a
|
||||
class="avatar-container"
|
||||
:href="$router.resolve(userProfileLink).href"
|
||||
@click.stop.prevent.capture="toggleUserExpanded"
|
||||
@click.prevent
|
||||
>
|
||||
<UserPopover
|
||||
:userId="notification.from_profile.id"
|
||||
:overlayCenters="true"
|
||||
>
|
||||
<UserAvatar
|
||||
class="post-avatar"
|
||||
:bot="botIndicator"
|
||||
:compact="true"
|
||||
:better-shadow="betterShadow"
|
||||
:user="notification.from_profile"
|
||||
/>
|
||||
</UserPopover>
|
||||
</a>
|
||||
<div class="notification-right">
|
||||
<UserCard
|
||||
v-if="userExpanded"
|
||||
:user-id="getUser(notification).id"
|
||||
:rounded="true"
|
||||
:bordered="true"
|
||||
/>
|
||||
<span class="notification-details">
|
||||
<div class="name-and-action">
|
||||
<!-- eslint-disable vue/no-v-html -->
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { computed } from 'vue'
|
||||
import { mapGetters } from 'vuex'
|
||||
import Notification from '../notification/notification.vue'
|
||||
import NotificationFilters from './notification_filters.vue'
|
||||
@ -40,6 +41,11 @@ const Notifications = {
|
||||
seenToDisplayCount: DEFAULT_SEEN_TO_DISPLAY_COUNT
|
||||
}
|
||||
},
|
||||
provide () {
|
||||
return {
|
||||
popoversZLayer: computed(() => this.popoversZLayer)
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
mainClass () {
|
||||
return this.minimalMode ? '' : 'panel panel-default'
|
||||
@ -77,6 +83,10 @@ const Notifications = {
|
||||
}
|
||||
return map[layoutType] || '#notifs-sidebar'
|
||||
},
|
||||
popoversZLayer () {
|
||||
const { layoutType } = this.$store.state.interface
|
||||
return layoutType === 'mobile' ? 'navbar' : null
|
||||
},
|
||||
notificationsToDisplay () {
|
||||
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,
|
||||
// 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 () {
|
||||
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,
|
||||
styles: { opacity: 0 },
|
||||
oldSize: { width: 0, height: 0 }
|
||||
styles: {},
|
||||
oldSize: { width: 0, height: 0 },
|
||||
scrollable: null,
|
||||
// used to avoid blinking if hovered onto popover
|
||||
graceTimeout: null,
|
||||
parentPopover: null,
|
||||
childrenShown: new Set()
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
@ -47,9 +69,7 @@ const Popover = {
|
||||
},
|
||||
updateStyles () {
|
||||
if (this.hidden) {
|
||||
this.styles = {
|
||||
opacity: 0
|
||||
}
|
||||
this.styles = {}
|
||||
return
|
||||
}
|
||||
|
||||
@ -57,14 +77,26 @@ const Popover = {
|
||||
// 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
|
||||
// SVGs don't have offsetWidth/Height, use fallback
|
||||
const anchorWidth = anchorEl.offsetWidth || anchorEl.clientWidth
|
||||
const anchorHeight = anchorEl.offsetHeight || anchorEl.clientHeight
|
||||
const screenBox = anchorEl.getBoundingClientRect()
|
||||
// Screen position of the origin point for popover
|
||||
const origin = { x: screenBox.left + screenBox.width * 0.5, y: screenBox.top }
|
||||
const anchorWidth = anchorEl.offsetWidth || anchorEl.clientWidth
|
||||
const anchorScreenBox = anchorEl.getBoundingClientRect()
|
||||
|
||||
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 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
|
||||
const parentBounds = this.boundTo &&
|
||||
const parentScreenBox = this.boundTo &&
|
||||
(this.boundTo.x === 'container' || this.boundTo.y === 'container') &&
|
||||
this.containerBoundingClientRect()
|
||||
|
||||
@ -73,81 +105,151 @@ const Popover = {
|
||||
// What are the screen bounds for the popover? Viewport vs container
|
||||
// when using viewport, using default margin values to dodge the navbar
|
||||
const xBounds = this.boundTo && this.boundTo.x === 'container' ? {
|
||||
min: parentBounds.left + (margin.left || 0),
|
||||
max: parentBounds.right - (margin.right || 0)
|
||||
min: parentScreenBox.left + (margin.left || 0),
|
||||
max: parentScreenBox.right - (margin.right || 0)
|
||||
} : {
|
||||
min: 0 + (margin.left || 10),
|
||||
max: window.innerWidth - (margin.right || 10)
|
||||
}
|
||||
|
||||
const yBounds = this.boundTo && this.boundTo.y === 'container' ? {
|
||||
min: parentBounds.top + (margin.top || 0),
|
||||
max: parentBounds.bottom - (margin.bottom || 0)
|
||||
min: parentScreenBox.top + (margin.top || 0),
|
||||
max: parentScreenBox.bottom - (margin.bottom || 0)
|
||||
} : {
|
||||
min: 0 + (margin.top || 50),
|
||||
max: window.innerHeight - (margin.bottom || 5)
|
||||
}
|
||||
|
||||
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 ((origin.x - content.offsetWidth * 0.5) < xBounds.min) {
|
||||
horizOffset += -(origin.x - content.offsetWidth * 0.5) + xBounds.min
|
||||
if (leftBorder < xBounds.min) {
|
||||
horizOffset += xBounds.min - leftBorder
|
||||
}
|
||||
|
||||
// If overflowing from right, move it so that it doesn't
|
||||
if ((origin.x + horizOffset + content.offsetWidth * 0.5) > xBounds.max) {
|
||||
horizOffset -= (origin.x + horizOffset + content.offsetWidth * 0.5) - xBounds.max
|
||||
if (rightBorder > xBounds.max) {
|
||||
horizOffset -= rightBorder - xBounds.max
|
||||
}
|
||||
|
||||
// If overflowing from top, move it so that it doesn't
|
||||
if (topBorder < yBounds.min) {
|
||||
vertOffset += yBounds.min - topBorder
|
||||
}
|
||||
|
||||
// If overflowing from bottom, move it so that it doesn't
|
||||
if (bottomBorder > yBounds.max) {
|
||||
vertOffset -= bottomBorder - yBounds.max
|
||||
}
|
||||
|
||||
let translateX = 0
|
||||
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.
|
||||
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 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
|
||||
const translateY = usingTop
|
||||
? -anchorHeight + vPadding - yOffset - content.offsetHeight
|
||||
: yOffset
|
||||
translateY = usingTop
|
||||
? topBoundary - yOffset - content.offsetHeight
|
||||
: bottomBoundary + yOffset
|
||||
|
||||
const xOffset = (this.offset && this.offset.x) || 0
|
||||
const translateX = anchorWidth * 0.5 - content.offsetWidth * 0.5 + horizOffset + xOffset
|
||||
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 = {
|
||||
opacity: 1,
|
||||
transform: `translateX(${Math.round(translateX)}px) translateY(${Math.round(translateY)}px)`
|
||||
left: `${Math.round(translateX)}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 () {
|
||||
if (this.disabled) return
|
||||
const wasHidden = this.hidden
|
||||
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(() => {
|
||||
if (wasHidden) this.$emit('show')
|
||||
this.updateStyles()
|
||||
})
|
||||
},
|
||||
hidePopover () {
|
||||
if (this.disabled) return
|
||||
if (!this.hidden) this.$emit('close')
|
||||
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) {
|
||||
if (this.trigger === 'hover') this.showPopover()
|
||||
if (this.trigger === 'hover') {
|
||||
this.lockReEntry = false
|
||||
clearTimeout(this.graceTimeout)
|
||||
this.graceTimeout = null
|
||||
this.showPopover()
|
||||
}
|
||||
},
|
||||
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) {
|
||||
if (this.trigger === 'click') {
|
||||
@ -160,8 +262,24 @@ const Popover = {
|
||||
},
|
||||
onClickOutside (e) {
|
||||
if (this.hidden) return
|
||||
if (this.$refs.content && this.$refs.content.contains(e.target)) return
|
||||
if (this.$el.contains(e.target)) return
|
||||
if (this.childrenShown.size > 0) return
|
||||
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 () {
|
||||
@ -175,11 +293,18 @@ const Popover = {
|
||||
this.oldSize = { width: content.offsetWidth, height: content.offsetHeight }
|
||||
}
|
||||
},
|
||||
created () {
|
||||
document.addEventListener('click', this.onClickOutside)
|
||||
mounted () {
|
||||
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 () {
|
||||
document.removeEventListener('click', this.onClickOutside)
|
||||
beforeUnmount () {
|
||||
this.hidePopover()
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div
|
||||
<span
|
||||
@mouseenter="onMouseenter"
|
||||
@mouseleave="onMouseleave"
|
||||
>
|
||||
@ -11,12 +11,17 @@
|
||||
>
|
||||
<slot name="trigger" />
|
||||
</button>
|
||||
<teleport to="#popovers">
|
||||
<transition name="fade">
|
||||
<div
|
||||
v-if="!hidden"
|
||||
ref="content"
|
||||
:style="styles"
|
||||
class="popover"
|
||||
:class="popoverClass || 'popover-default'"
|
||||
@mouseenter="onMouseenterContent"
|
||||
@mouseleave="onMouseleaveContent"
|
||||
@click="onClickContent"
|
||||
>
|
||||
<slot
|
||||
name="content"
|
||||
@ -24,7 +29,9 @@
|
||||
:close="hidePopover"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
</teleport>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script src="./popover.js" />
|
||||
@ -37,14 +44,15 @@
|
||||
}
|
||||
|
||||
.popover {
|
||||
z-index: 500;
|
||||
position: absolute;
|
||||
z-index: var(--ZI_popover_override, var(--ZI_popovers));
|
||||
position: fixed;
|
||||
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 {
|
||||
transition: opacity 0.3s;
|
||||
|
||||
&:after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
@ -80,7 +88,7 @@
|
||||
text-align: left;
|
||||
list-style: none;
|
||||
max-width: 100vw;
|
||||
z-index: 200;
|
||||
z-index: var(--ZI_popover_override, var(--ZI_popovers));
|
||||
white-space: nowrap;
|
||||
|
||||
.dropdown-divider {
|
||||
|
@ -6,6 +6,7 @@
|
||||
:offset="{ y: 5 }"
|
||||
:bound-to="{ x: 'container' }"
|
||||
remove-padding
|
||||
popover-class="ReactButton popover-default"
|
||||
@show="focusInput"
|
||||
>
|
||||
<template v-slot:content="{close}">
|
||||
@ -41,7 +42,7 @@
|
||||
</div>
|
||||
</template>
|
||||
<template v-slot:trigger>
|
||||
<button
|
||||
<span
|
||||
class="button-unstyled popover-trigger"
|
||||
:title="$t('tool_tip.add_reaction')"
|
||||
>
|
||||
@ -49,7 +50,7 @@
|
||||
class="fa-scale-110 fa-old-padding"
|
||||
:icon="['far', 'smile-beam']"
|
||||
/>
|
||||
</button>
|
||||
</span>
|
||||
</template>
|
||||
</Popover>
|
||||
</template>
|
||||
|
@ -41,11 +41,11 @@ export default {
|
||||
.ModifiedIndicator {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.modified-tooltip {
|
||||
margin: 0.5em 1em;
|
||||
min-width: 10em;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@ -41,11 +41,11 @@ export default {
|
||||
.ServerSideIndicator {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.serverside-tooltip {
|
||||
margin: 0.5em 1em;
|
||||
min-width: 10em;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@ -74,6 +74,16 @@
|
||||
{{ $t('settings.show_scrollbars') }}
|
||||
</BooleanSetting>
|
||||
</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>
|
||||
<ChoiceSetting
|
||||
v-if="user"
|
||||
@ -261,18 +271,14 @@
|
||||
{{ $t('settings.mention_link_display') }}
|
||||
</ChoiceSetting>
|
||||
</li>
|
||||
<ul
|
||||
class="setting-list suboptions"
|
||||
>
|
||||
<li v-if="mentionLinkDisplay === 'short'">
|
||||
<li>
|
||||
<BooleanSetting
|
||||
path="mentionLinkShowTooltip"
|
||||
expert="1"
|
||||
>
|
||||
{{ $t('settings.mention_link_show_tooltip') }}
|
||||
{{ $t('settings.mention_link_use_tooltip') }}
|
||||
</BooleanSetting>
|
||||
</li>
|
||||
</ul>
|
||||
<li>
|
||||
<BooleanSetting
|
||||
path="useAtIcon"
|
||||
|
@ -80,7 +80,7 @@
|
||||
.floating-shout {
|
||||
position: fixed;
|
||||
bottom: 0.5em;
|
||||
z-index: 1000;
|
||||
z-index: var(--ZI_popovers);
|
||||
max-width: 25em;
|
||||
|
||||
&.-left {
|
||||
|
@ -211,7 +211,7 @@
|
||||
|
||||
.side-drawer-container {
|
||||
position: fixed;
|
||||
z-index: 1000;
|
||||
z-index: var(--ZI_navbar);
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
|
@ -4,13 +4,13 @@ import ReactButton from '../react_button/react_button.vue'
|
||||
import RetweetButton from '../retweet_button/retweet_button.vue'
|
||||
import ExtraButtons from '../extra_buttons/extra_buttons.vue'
|
||||
import PostStatusForm from '../post_status_form/post_status_form.vue'
|
||||
import UserCard from '../user_card/user_card.vue'
|
||||
import UserAvatar from '../user_avatar/user_avatar.vue'
|
||||
import AvatarList from '../avatar_list/avatar_list.vue'
|
||||
import Timeago from '../timeago/timeago.vue'
|
||||
import StatusContent from '../status_content/status_content.vue'
|
||||
import RichContent from 'src/components/rich_content/rich_content.jsx'
|
||||
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 EmojiReactions from '../emoji_reactions/emoji_reactions.vue'
|
||||
import MentionsLine from 'src/components/mentions_line/mentions_line.vue'
|
||||
@ -105,7 +105,6 @@ const Status = {
|
||||
RetweetButton,
|
||||
ExtraButtons,
|
||||
PostStatusForm,
|
||||
UserCard,
|
||||
UserAvatar,
|
||||
AvatarList,
|
||||
Timeago,
|
||||
@ -115,7 +114,8 @@ const Status = {
|
||||
StatusContent,
|
||||
RichContent,
|
||||
MentionLink,
|
||||
MentionsLine
|
||||
MentionsLine,
|
||||
UserPopover
|
||||
},
|
||||
props: [
|
||||
'statusoid',
|
||||
|
@ -122,9 +122,10 @@
|
||||
v-if="!noHeading"
|
||||
class="left-side"
|
||||
>
|
||||
<a
|
||||
:href="$router.resolve(userProfileLink).href"
|
||||
@click.stop.prevent.capture="toggleUserExpanded"
|
||||
<a :href="$router.resolve(userProfileLink).href" @click.prevent>
|
||||
<UserPopover
|
||||
:userId="status.user.id"
|
||||
:overlayCenters="true"
|
||||
>
|
||||
<UserAvatar
|
||||
class="post-avatar"
|
||||
@ -133,16 +134,10 @@
|
||||
:better-shadow="betterShadow"
|
||||
:user="status.user"
|
||||
/>
|
||||
</UserPopover>
|
||||
</a>
|
||||
</div>
|
||||
<div class="right-side">
|
||||
<UserCard
|
||||
v-if="userExpanded"
|
||||
:user-id="status.user.id"
|
||||
:rounded="true"
|
||||
:bordered="true"
|
||||
class="usercard"
|
||||
/>
|
||||
<div
|
||||
v-if="!noHeading"
|
||||
class="status-heading"
|
||||
@ -322,6 +317,7 @@
|
||||
class="mentions-line-first"
|
||||
/>
|
||||
</span>
|
||||
{{ ' ' }}
|
||||
<MentionsLine
|
||||
v-if="hasMentionsLine"
|
||||
:mentions="mentionsLine.slice(1)"
|
||||
|
@ -38,6 +38,13 @@ const StatusPopover = {
|
||||
.catch(e => (this.error = true))
|
||||
}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
status (newStatus, oldStatus) {
|
||||
if (newStatus !== oldStatus) {
|
||||
this.$nextTick(() => this.$refs.popover.updateStyles())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,9 +1,11 @@
|
||||
<template>
|
||||
<Popover
|
||||
trigger="hover"
|
||||
:stay-on-click="true"
|
||||
popover-class="popover-default status-popover"
|
||||
:bound-to="{ x: 'container' }"
|
||||
@show="enter"
|
||||
ref="popover"
|
||||
>
|
||||
<template v-slot:trigger>
|
||||
<slot />
|
||||
@ -52,8 +54,6 @@
|
||||
border-width: 1px;
|
||||
border-radius: $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 */
|
||||
.Status.Status {
|
||||
|
@ -3,19 +3,17 @@
|
||||
trigger="click"
|
||||
class="TimelineMenu"
|
||||
:class="{ 'open': isOpen }"
|
||||
:margin="{ left: -15, right: -200 }"
|
||||
:bound-to="{ x: 'container' }"
|
||||
popover-class="timeline-menu-popover-wrap"
|
||||
bound-to-selector=".Timeline"
|
||||
popover-class="timeline-menu-popover popover-default"
|
||||
@show="openMenu"
|
||||
@close="() => isOpen = false"
|
||||
>
|
||||
<template v-slot:content>
|
||||
<div class="timeline-menu-popover popover-default">
|
||||
<TimelineMenuContent />
|
||||
</div>
|
||||
</template>
|
||||
<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>
|
||||
<FAIcon
|
||||
@ -27,7 +25,7 @@
|
||||
class="click-blocker"
|
||||
@click="blockOpen"
|
||||
/>
|
||||
</button>
|
||||
</span>
|
||||
</template>
|
||||
</Popover>
|
||||
</template>
|
||||
@ -38,42 +36,18 @@
|
||||
@import '../../_variables.scss';
|
||||
|
||||
.TimelineMenu {
|
||||
flex-shrink: 1;
|
||||
margin-right: auto;
|
||||
min-width: 0;
|
||||
width: 24rem;
|
||||
|
||||
.popover-trigger-button {
|
||||
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 {
|
||||
border-top-right-radius: 0;
|
||||
border-top-left-radius: 0;
|
||||
}
|
||||
|
||||
&.open .timeline-menu-popover {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.timeline-menu-title {
|
||||
margin: 0;
|
||||
cursor: pointer;
|
||||
@ -108,6 +82,16 @@
|
||||
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 {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
@ -134,7 +118,9 @@
|
||||
|
||||
a {
|
||||
display: block;
|
||||
padding: 0.6em 0.65em;
|
||||
padding: 0 0.65em;
|
||||
height: 3.5em;
|
||||
line-height: 3.5em;
|
||||
|
||||
&:hover {
|
||||
background-color: $fallback--lightBg;
|
||||
|
@ -14,7 +14,9 @@ import {
|
||||
faRss,
|
||||
faSearchPlus,
|
||||
faExternalLinkAlt,
|
||||
faEdit
|
||||
faEdit,
|
||||
faTimes,
|
||||
faExpandAlt
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
library.add(
|
||||
@ -22,12 +24,21 @@ library.add(
|
||||
faBell,
|
||||
faSearchPlus,
|
||||
faExternalLinkAlt,
|
||||
faEdit
|
||||
faEdit,
|
||||
faTimes,
|
||||
faExpandAlt
|
||||
)
|
||||
|
||||
export default {
|
||||
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 () {
|
||||
return {
|
||||
@ -47,9 +58,10 @@ export default {
|
||||
},
|
||||
classes () {
|
||||
return [{
|
||||
'user-card-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
|
||||
'user-card-bordered': this.bordered === true // set border for all sides
|
||||
'-rounded-t': this.rounded === 'top', // set border-top-left-radius and border-top-right-radius
|
||||
'-rounded': this.rounded === true, // set border-radius for all sides
|
||||
'-bordered': this.bordered === true, // set border for all sides
|
||||
'-popover': !!this.onClose // set popover rounding
|
||||
}]
|
||||
},
|
||||
style () {
|
||||
@ -170,6 +182,12 @@ export default {
|
||||
},
|
||||
mentionUser () {
|
||||
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;
|
||||
background-size: cover;
|
||||
mask-size: 100% 60%;
|
||||
border-top-left-radius: calc(var(--panelRadius) - 1px);
|
||||
border-top-right-radius: calc(var(--panelRadius) - 1px);
|
||||
border-top-left-radius: calc(var(--__roundnessTop, --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);
|
||||
z-index: -2;
|
||||
|
||||
@ -72,21 +74,33 @@
|
||||
}
|
||||
}
|
||||
|
||||
// Modifiers
|
||||
|
||||
&-rounded-t {
|
||||
&.-rounded-t {
|
||||
border-top-left-radius: $fallback--panelRadius;
|
||||
border-top-left-radius: var(--panelRadius, $fallback--panelRadius);
|
||||
border-top-right-radius: $fallback--panelRadius;
|
||||
border-top-right-radius: var(--panelRadius, $fallback--panelRadius);
|
||||
|
||||
--__roundnessTop: var(--panelRadius);
|
||||
--__roundnessBottom: 0;
|
||||
}
|
||||
|
||||
&-rounded {
|
||||
&.-rounded {
|
||||
border-radius: $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-style: solid;
|
||||
border-color: $fallback--border;
|
||||
@ -99,6 +113,15 @@
|
||||
color: var(--lightText, $fallback--lightText);
|
||||
padding: 0 26px;
|
||||
|
||||
a {
|
||||
color: $fallback--lightText;
|
||||
color: var(--lightText, $fallback--lightText);
|
||||
|
||||
&:hover {
|
||||
color: var(--icon);
|
||||
}
|
||||
}
|
||||
|
||||
.container {
|
||||
min-width: 0;
|
||||
padding: 16px 0 6px;
|
||||
@ -110,23 +133,27 @@
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
> a {
|
||||
vertical-align: middle;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.Avatar {
|
||||
--_avatarShadowBox: var(--avatarShadow);
|
||||
--_avatarShadowFilter: var(--avatarShadowFilter);
|
||||
--_avatarShadowInset: var(--avatarShadowInset);
|
||||
|
||||
flex: 1 0 100%;
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
|
||||
&-avatar-link {
|
||||
&-avatar {
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
|
||||
&-overlay {
|
||||
&.-overlay {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
@ -146,7 +173,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
&:hover &-overlay {
|
||||
&:hover &.-overlay {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
@ -206,8 +233,6 @@
|
||||
flex: 0 1 auto;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
color: $fallback--lightText;
|
||||
color: var(--lightText, $fallback--lightText);
|
||||
}
|
||||
|
||||
.dailyAvg {
|
||||
|
@ -8,11 +8,11 @@
|
||||
:style="style"
|
||||
class="background-image"
|
||||
/>
|
||||
<div class="panel-heading -flexible-height">
|
||||
<div :class="onClose ? '' : panel-heading -flexible-height">
|
||||
<div class="user-info">
|
||||
<div class="container">
|
||||
<a
|
||||
v-if="allowZoomingAvatar"
|
||||
v-if="avatarAction === 'zoom'"
|
||||
class="user-info-avatar -link"
|
||||
@click="zoomAvatar"
|
||||
>
|
||||
@ -27,6 +27,13 @@
|
||||
/>
|
||||
</div>
|
||||
</a>
|
||||
<UserAvatar
|
||||
v-else-if="typeof avatarAction === 'function'"
|
||||
@click="avatarAction"
|
||||
class="user-info-avatar"
|
||||
:better-shadow="betterShadow"
|
||||
:user="user"
|
||||
/>
|
||||
<router-link
|
||||
v-else
|
||||
:to="userProfileLink(user)"
|
||||
@ -38,12 +45,16 @@
|
||||
</router-link>
|
||||
<div class="user-summary">
|
||||
<div class="top-line">
|
||||
<router-link
|
||||
:to="userProfileLink(user)"
|
||||
class="user-name"
|
||||
>
|
||||
<RichContent
|
||||
:title="user.name"
|
||||
class="user-name"
|
||||
:html="user.name"
|
||||
:emoji="user.emoji"
|
||||
/>
|
||||
</router-link>
|
||||
<button
|
||||
v-if="!isOtherUser && user.is_local"
|
||||
class="button-unstyled edit-profile-button"
|
||||
@ -72,6 +83,27 @@
|
||||
:user="user"
|
||||
: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 class="bottom-line">
|
||||
<router-link
|
||||
|
23
src/components/user_popover/user_popover.js
Normal file
23
src/components/user_popover/user_popover.js
Normal 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
|
33
src/components/user_popover/user_popover.vue
Normal file
33
src/components/user_popover/user_popover.vue
Normal 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>
|
@ -8,7 +8,7 @@
|
||||
:user-id="userId"
|
||||
:switcher="true"
|
||||
:selected="timeline.viewing"
|
||||
:allow-zooming-avatar="true"
|
||||
avatar-action="zoom"
|
||||
rounded="top"
|
||||
/>
|
||||
<div
|
||||
|
@ -546,10 +546,12 @@
|
||||
"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": "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_fade_domain": "Fade domains (e.g. {'@'}example.org in {'@'}foo{'@'}example.org)",
|
||||
"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",
|
||||
"greentext": "Meme arrows",
|
||||
"show_yous": "Show (You)s",
|
||||
|
@ -81,6 +81,8 @@ export const defaultState = {
|
||||
useContainFit: true,
|
||||
disableStickyHeaders: false,
|
||||
showScrollbars: false,
|
||||
userPopoverZoom: false,
|
||||
userPopoverOverlay: true,
|
||||
greentext: undefined, // instance default
|
||||
useAtIcon: undefined, // instance default
|
||||
mentionLinkDisplay: undefined, // instance default
|
||||
|
@ -4,7 +4,15 @@ import RichContent from 'src/components/rich_content/rich_content.jsx'
|
||||
const attentions = []
|
||||
const global = {
|
||||
mocks: {
|
||||
'$store': null
|
||||
'$store': {
|
||||
state: {},
|
||||
getters: {
|
||||
mergedConfig: () => ({
|
||||
mentionLinkShowTooltip: true
|
||||
}),
|
||||
findUserByUrl: () => null
|
||||
}
|
||||
}
|
||||
},
|
||||
stubs: {
|
||||
FAIcon: true
|
||||
@ -131,8 +139,7 @@ describe('RichContent', () => {
|
||||
].join(''),
|
||||
[
|
||||
makeMention('John'),
|
||||
makeMention('Josh'),
|
||||
makeMention('Jeremy')
|
||||
makeMention('Josh'), makeMention('Jeremy')
|
||||
].join('')
|
||||
].join('\n')
|
||||
|
||||
@ -349,7 +356,6 @@ describe('RichContent', () => {
|
||||
p(
|
||||
'<span class="MentionsLine">',
|
||||
'<span class="MentionLink mention-link">',
|
||||
'<!-- eslint-disable vue/no-v-html -->',
|
||||
'<a href="lol" class="original" target="_blank">',
|
||||
'<span>',
|
||||
'https://</span>',
|
||||
@ -358,10 +364,7 @@ describe('RichContent', () => {
|
||||
'<span>',
|
||||
'</span>',
|
||||
'</a>',
|
||||
'<!-- eslint-enable vue/no-v-html -->',
|
||||
'<!--v-if-->', // v-if placeholder, mentionlink's "new" (i.e. rich) display
|
||||
'</span>',
|
||||
'<!--v-if-->', // v-if placeholder, mentionsline's extra mentions and stuff
|
||||
'</span>'
|
||||
),
|
||||
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', () => {
|
||||
@ -412,7 +415,6 @@ describe('RichContent', () => {
|
||||
'<span class="poast-style">',
|
||||
'<span class="MentionsLine">',
|
||||
'<span class="MentionLink mention-link">',
|
||||
'<!-- eslint-disable vue/no-v-html -->',
|
||||
'<a href="lol" class="original" target="_blank">',
|
||||
'<span>',
|
||||
'https://</span>',
|
||||
@ -421,11 +423,8 @@ describe('RichContent', () => {
|
||||
'<span>',
|
||||
'</span>',
|
||||
'</a>',
|
||||
'<!-- eslint-enable vue/no-v-html -->',
|
||||
'<!--v-if-->', // v-if placeholder, mentionlink's "new" (i.e. rich) display
|
||||
'</span>',
|
||||
'<span class="MentionLink mention-link">',
|
||||
'<!-- eslint-disable vue/no-v-html -->',
|
||||
'<a href="lol" class="original" target="_blank">',
|
||||
'<span>',
|
||||
'https://</span>',
|
||||
@ -434,10 +433,7 @@ describe('RichContent', () => {
|
||||
'<span>',
|
||||
'</span>',
|
||||
'</a>',
|
||||
'<!-- eslint-enable vue/no-v-html -->',
|
||||
'<!--v-if-->', // v-if placeholder, mentionlink's "new" (i.e. rich) display
|
||||
'</span>',
|
||||
'<!--v-if-->', // v-if placeholder, mentionsline's extra mentions and stuff
|
||||
'</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', () => {
|
||||
|
Loading…
Reference in New Issue
Block a user