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:
Henry Jameson 2022-07-31 11:44:15 +03:00
commit 1cf7af3374
50 changed files with 658 additions and 358 deletions

View file

@ -1,4 +1,4 @@
import UserCard from '../user_card/user_card.vue'
import UserPopover from '../user_popover/user_popover.vue'
import UserAvatar from '../user_avatar/user_avatar.vue'
import 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)
}

View file

@ -1,24 +1,19 @@
<template>
<div class="basic-user-card">
<router-link :to="userProfileLink(user)">
<UserAvatar
class="avatar"
:user="user"
@click.prevent="toggleUserExpanded"
/>
<router-link @click.prevent :to="userProfileLink(user)">
<UserPopover
:userId="user.id"
:overlayCenters="true"
overlayCentersSelector=".avatar"
>
<UserAvatar
class="user-avatar avatar"
:user="user"
@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;

View file

@ -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'
},

View file

@ -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"

View file

@ -1,12 +1,13 @@
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
import UserAvatar from '../user_avatar/user_avatar.vue'
import 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)
}
}
}

View file

@ -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"

View file

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

View file

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

View file

@ -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;

View file

@ -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}`
}

View file

@ -51,28 +51,30 @@
icon="thumbtack"
/><span>{{ $t("status.unpin") }}</span>
</button>
<button
v-if="!status.bookmarked"
class="button-default dropdown-item dropdown-item-icon"
@click.prevent="bookmarkStatus"
@click="close"
>
<FAIcon
fixed-width
:icon="['far', 'bookmark']"
/><span>{{ $t("status.bookmark") }}</span>
</button>
<button
v-if="status.bookmarked"
class="button-default dropdown-item dropdown-item-icon"
@click.prevent="unbookmarkStatus"
@click="close"
>
<FAIcon
fixed-width
icon="bookmark"
/><span>{{ $t("status.unbookmark") }}</span>
</button>
<template v-if="canBookmark">
<button
v-if="!status.bookmarked"
class="button-default dropdown-item dropdown-item-icon"
@click.prevent="bookmarkStatus"
@click="close"
>
<FAIcon
fixed-width
:icon="['far', 'bookmark']"
/><span>{{ $t("status.bookmark") }}</span>
</button>
<button
v-if="status.bookmarked"
class="button-default dropdown-item dropdown-item-icon"
@click.prevent="unbookmarkStatus"
@click="close"
>
<FAIcon
fixed-width
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>

View file

@ -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;

View file

@ -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,

View file

@ -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

View file

@ -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;
}

View file

@ -9,66 +9,58 @@
class="original"
target="_blank"
v-html="content"
/><!-- eslint-enable vue/no-v-html --><span
v-if="user"
class="new"
:style="style"
:class="classnames"
/><!-- eslint-enable vue/no-v-html -->
<UserPopover
v-else
:userId="user.id"
:disabled="!shouldShowTooltip"
>
<a
class="short button-unstyled"
:class="{ '-with-tooltip': shouldShowTooltip }"
:href="url"
@click.prevent="onClick"
<span
v-if="user"
class="new"
:style="style"
:class="classnames"
>
<!-- eslint-disable vue/no-v-html -->
<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"
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"
<a
class="short button-unstyled"
:class="{ '-with-tooltip': shouldShowTooltip }"
:href="url"
@click.prevent="onClick"
>
<!-- 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"
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 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>
</UserPopover>
</span>
</template>

View file

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

View file

@ -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;

View file

@ -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;

View file

@ -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);

View file

@ -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 () {

View file

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

View file

@ -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)
},

View file

@ -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
}
// 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)
// If overflowing from top, move it so that it doesn't
if (topBorder < yBounds.min) {
vertOffset += yBounds.min - topBorder
}
const yOffset = (this.offset && this.offset.y) || 0
const translateY = usingTop
? -anchorHeight + vPadding - yOffset - content.offsetHeight
: yOffset
// If overflowing from bottom, move it so that it doesn't
if (bottomBorder > yBounds.max) {
vertOffset -= bottomBorder - yBounds.max
}
const xOffset = (this.offset && this.offset.x) || 0
const translateX = anchorWidth * 0.5 - content.offsetWidth * 0.5 + horizOffset + xOffset
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.
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 = {
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()
}
}

View file

@ -1,5 +1,5 @@
<template>
<div
<span
@mouseenter="onMouseenter"
@mouseleave="onMouseleave"
>
@ -11,20 +11,27 @@
>
<slot name="trigger" />
</button>
<div
v-if="!hidden"
ref="content"
:style="styles"
class="popover"
:class="popoverClass || 'popover-default'"
>
<slot
name="content"
class="popover-inner"
:close="hidePopover"
/>
</div>
</div>
<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"
class="popover-inner"
:close="hidePopover"
/>
</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 {

View file

@ -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>

View file

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

View file

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

View file

@ -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'">
<BooleanSetting
path="mentionLinkShowTooltip"
expert="1"
>
{{ $t('settings.mention_link_show_tooltip') }}
</BooleanSetting>
</li>
</ul>
<li>
<BooleanSetting
path="mentionLinkShowTooltip"
expert="1"
>
{{ $t('settings.mention_link_use_tooltip') }}
</BooleanSetting>
</li>
<li>
<BooleanSetting
path="useAtIcon"

View file

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

View file

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

View file

@ -4,13 +4,13 @@ import ReactButton from '../react_button/react_button.vue'
import RetweetButton from '../retweet_button/retweet_button.vue'
import 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',

View file

@ -122,27 +122,22 @@
v-if="!noHeading"
class="left-side"
>
<a
:href="$router.resolve(userProfileLink).href"
@click.stop.prevent.capture="toggleUserExpanded"
>
<UserAvatar
class="post-avatar"
:bot="botIndicator"
:compact="compact"
:better-shadow="betterShadow"
:user="status.user"
/>
<a :href="$router.resolve(userProfileLink).href" @click.prevent>
<UserPopover
:userId="status.user.id"
:overlayCenters="true"
>
<UserAvatar
class="post-avatar"
:bot="botIndicator"
:compact="compact"
: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)"

View file

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

View file

@ -1,9 +1,11 @@
<template>
<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 {

View file

@ -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>
<TimelineMenuContent />
</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;

View file

@ -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()
}
}
}
}

View file

@ -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 {

View file

@ -8,25 +8,32 @@
: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"
class="user-info-avatar-link"
v-if="avatarAction === 'zoom'"
class="user-info-avatar -link"
@click="zoomAvatar"
>
<UserAvatar
:better-shadow="betterShadow"
:user="user"
/>
<div class="user-info-avatar-link-overlay">
<div class="user-info-avatar -link -overlay">
<FAIcon
class="fa-scale-110 fa-old-padding"
icon="search-plus"
/>
</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">
<RichContent
:title="user.name"
<router-link
:to="userProfileLink(user)"
class="user-name"
:html="user.name"
:emoji="user.emoji"
/>
>
<RichContent
:title="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

View file

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

View file

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

View file

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