Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F85711906
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Award Token
Flag For Later
Size
39 KB
Referenced Files
None
Subscribers
None
View Options
diff --git a/src/components/popover/popover.js b/src/components/popover/popover.js
index c076a61c52..6b49cf682a 100644
--- a/src/components/popover/popover.js
+++ b/src/components/popover/popover.js
@@ -1,364 +1,364 @@
const Popover = {
name: 'Popover',
props: {
// Action to trigger popover: either 'hover' or 'click'
trigger: String,
// 'top', 'bottom', 'left', 'right'
placement: String,
// Takes object with properties 'x' and 'y', values of these can be
// 'container' for using offsetParent as boundaries for either axis
// or 'viewport'
boundTo: Object,
// Takes a selector to use as a replacement for the parent container
// for getting boundaries for x an y axis
boundToSelector: String,
// Takes a top/bottom/left/right object, how much space to leave
// between boundary and popover element
margin: Object,
// Takes a x/y object and tells how many pixels to offset from
// anchor point on either axis
offset: Object,
// Replaces the classes you may want for the popover container.
// Use 'popover-default' in addition to get the default popover
// styles with your custom class.
popoverClass: String,
// 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,
// 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,
// Use styled button (to avoid nested buttons)
normalButton: Boolean,
triggerAttrs: {
type: Object,
default: {}
}
},
inject: { // override popover z layer
popoversZLayer: {
default: ''
}
},
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
anchorEl: null,
// There's an issue where having teleport enabled by default causes things just...
// not render at all, i.e. main post status form and its emoji inputs
teleport: false,
lockReEntry: false,
hidden: true,
styles: {},
oldSize: { width: 0, height: 0 },
scrollable: null,
// used to avoid blinking if hovered onto popover
graceTimeout: null,
parentPopover: null,
disableClickOutside: false,
childrenShown: new Set()
}
},
methods: {
setAnchorEl (el) {
this.anchorEl = el
this.updateStyles()
},
containerBoundingClientRect () {
const container = this.boundToSelector ? this.$el.closest(this.boundToSelector) : this.$el.offsetParent
return container.getBoundingClientRect()
},
updateStyles () {
if (this.hidden) {
this.styles = {}
return
}
// Popover will be anchored around this element, trigger ref is the container, so
// its children are what are inside the slot. Expect only one v-slot:trigger.
const anchorEl = this.anchorEl || (this.$refs.trigger && this.$refs.trigger.children[0]) || this.$el
// SVGs don't have offsetWidth/Height, use fallback
const anchorHeight = anchorEl.offsetHeight || anchorEl.clientHeight
const anchorWidth = anchorEl.offsetWidth || anchorEl.clientWidth
const anchorScreenBox = anchorEl.getBoundingClientRect()
const anchorStyle = getComputedStyle(anchorEl)
const topPadding = parseFloat(anchorStyle.paddingTop)
const bottomPadding = parseFloat(anchorStyle.paddingBottom)
const rightPadding = parseFloat(anchorStyle.paddingRight)
const leftPadding = parseFloat(anchorStyle.paddingLeft)
// 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 parentScreenBox = this.boundTo &&
(this.boundTo.x === 'container' || this.boundTo.y === 'container') &&
this.containerBoundingClientRect()
const margin = this.margin || {}
// 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: 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: 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 (leftBorder < xBounds.min) {
horizOffset += xBounds.min - leftBorder
}
// If overflowing from right, move it so that it doesn't
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 if (this.placement !== 'right' && this.placement !== 'left') {
// 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,
+ // Handle special cases, first force to displaying on top if there's no space on bottom,
// regardless of what placement value was. Then check if there's no 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
} else {
// Default to whatever user wished with placement prop
let usingLeft = this.placement !== 'right'
- // Handle special cases, first force to displaying on left if there's not space on right,
+ // Handle special cases, first force to displaying on left if there's no space on right,
// regardless of what placement value was. Then check if there's no space on right, and
- // force to bottom, again regardless of what placement value was.
+ // force to left, again regardless of what placement value was.
const leftBoundary = origin.x - anchorWidth * 0.5 + (this.removePadding ? leftPadding : 0)
const rightBoundary = origin.x + anchorWidth * 0.5 - (this.removePadding ? rightPadding : 0)
- if (leftBoundary - content.offsetWidth > xBounds.max) usingLeft = true
- if (rightBoundary + content.offsetWidth < xBounds.min) usingLeft = false
+ if (rightBoundary + content.offsetWidth > xBounds.max) usingLeft = true
+ if (leftBoundary - content.offsetWidth < xBounds.min) usingLeft = false
const xOffset = (this.offset && this.offset.x) || 0
translateX = usingLeft
? leftBoundary - xOffset - content.offsetWidth
: rightBoundary + xOffset
const yOffset = (this.offset && this.offset.y) || 0
translateY = origin.y + vertOffset + yOffset
}
this.styles = {
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
this.disableClickOutside = true
setTimeout(() => {
this.disableClickOutside = false
}, 0)
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.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.lockReEntry = false
clearTimeout(this.graceTimeout)
this.graceTimeout = null
this.showPopover()
}
},
onMouseleave (e) {
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') {
if (this.hidden) {
this.showPopover()
} else {
this.hidePopover()
}
}
},
onClickOutside (e) {
if (this.disableClickOutside) return
if (this.hidden) return
if (this.$refs.content && this.$refs.content.contains(e.target)) return
if (this.$el.contains(e.target)) return
if (this.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 () {
// Monitor changes to content size, update styles only when content sizes have changed,
// that should be the only time we need to move the popover box if we don't care about scroll
// or resize
const content = this.$refs.content
if (!content) return
if (this.oldSize.width !== content.offsetWidth || this.oldSize.height !== content.offsetHeight) {
this.updateStyles()
this.oldSize = { width: content.offsetWidth, height: content.offsetHeight }
}
},
mounted () {
this.teleport = true
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
},
beforeUnmount () {
this.hidePopover()
}
}
export default Popover
diff --git a/src/components/popover/popover.scss b/src/components/popover/popover.scss
index 6fe06299f0..b7f426e08c 100644
--- a/src/components/popover/popover.scss
+++ b/src/components/popover/popover.scss
@@ -1,112 +1,120 @@
.popover-trigger-button {
display: inline-block;
}
.popover {
z-index: var(--ZI_popover_override, var(--ZI_popovers));
position: fixed;
min-width: 0;
max-width: calc(100vw - 20px);
box-shadow: var(--shadow);
}
.popover-default {
&::after {
content: "";
position: absolute;
top: -1px;
bottom: -1px;
left: -1px;
right: -1px;
z-index: -1px;
box-shadow: var(--shadow);
pointer-events: none;
}
border-radius: var(--roundness);
border-color: var(--border);
border-style: solid;
border-width: 1px;
background-color: var(--background);
}
.dropdown-menu {
display: block;
padding: 0;
font-size: 1em;
text-align: left;
list-style: none;
max-width: 100vw;
z-index: var(--ZI_popover_override, var(--ZI_popovers));
white-space: nowrap;
background-color: var(--background);
.dropdown-divider {
height: 0;
margin: 0.5rem 0;
overflow: hidden;
border-top: 1px solid var(--border);
}
.dropdown-item:not(button, a) {
padding: 0;
}
a.dropdown-item,
button.dropdown-item,
.dropdown-item:not(button, a) {
&.disabled {
cursor: not-allowed;
}
&:not(.disabled) {
cursor: pointer;
}
- .main-button {
+ .main-button,
+ .extra-button {
+ display: grid;
box-sizing: border-box;
padding: var(--__horizontal-gap) var(--__horizontal-gap);
- grid-gap: var(--__horizontal-gap);
- display: grid;
border: none;
align-items: center;
+ }
+
+ .extra-button {
+ border-left: 1px solid var(--icon);
+ }
+
+ .main-button {
+ grid-gap: var(--__horizontal-gap);
grid-template-columns: 1fr var(--__line-height);
grid-auto-flow: column;
grid-auto-columns: auto;
.menu-checkbox {
display: inline-block;
vertical-align: middle;
min-width: calc(var(--__line-height) + 1px);
max-width: calc(var(--__line-height) + 1px);
min-height: calc(var(--__line-height) + 1px);
max-height: calc(var(--__line-height) + 1px);
line-height: var(--__line-height);
text-align: center;
border-radius: 0;
box-shadow: var(--shadow);
margin-right: var(--__horizontal-gap);
&.menu-checkbox-checked::after {
font-size: 1.25em;
content: "✓";
}
&.-radio {
border-radius: 9999px;
&.menu-checkbox-checked::after {
font-size: 2em;
content: "•";
}
}
}
}
&.-icon {
.main-button {
grid-template-columns: var(--__line-height) 1fr;
}
}
}
}
diff --git a/src/components/status_action_buttons/action_button.vue b/src/components/status_action_buttons/action_button.vue
index 9510d0bad8..a634308d9e 100644
--- a/src/components/status_action_buttons/action_button.vue
+++ b/src/components/status_action_buttons/action_button.vue
@@ -1,112 +1,112 @@
<template>
<div>
<component
:is="getComponent(button)"
class="main-button"
role="menuitem"
:tabindex="0"
:disabled="getClass(button).disabled"
:href="getComponent(button) == 'a' ? button.link?.(funcArg) || getRemoteInteractionLink : undefined"
@click.stop="getComponent(button) === 'button' && doAction(button)"
@click="close"
>
<FALayers>
<FAIcon
class="fa-scale-110"
:icon="button.icon(funcArg)"
:spin="!extra && button.animated?.() && animationState[button.name]"
:fixed-width="extra"
/>
<template v-if="!getClass(button).disabled && button.toggleable?.(funcArg) && button.active">
<FAIcon
v-if="button.active(funcArg)"
class="active-marker"
transform="shrink-6 up-9 right-12"
:icon="button.activeIndicator?.(funcArg) || 'check'"
/>
<FAIcon
v-if="!button.active(funcArg)"
class="focus-marker"
transform="shrink-6 up-9 right-12"
:icon="button.openIndicator?.(funcArg) || 'plus'"
/>
<FAIcon
v-else
class="focus-marker"
transform="shrink-6 up-9 right-12"
:icon="button.closeIndicator?.(funcArg) || 'minus'"
/>
</template>
</FALayers><span>{{ $t(button.label(funcArg)) }}</span>
<FAIcon
- v-if="button.name === 'bookmark'"
+ v-if="button.name === 'mute'"
class="chevron-icon"
size="lg"
icon="chevron-right"
/>
</component>
</div>
</template>
<script>
import { library } from '@fortawesome/fontawesome-svg-core'
import {
faPlus,
faMinus,
faCheck,
faTimes,
faWrench,
faReply,
faRetweet,
faStar,
faSmileBeam,
faEllipsisH,
faBookmark,
faEyeSlash,
faThumbtack,
faShareAlt,
faExternalLinkAlt,
faHistory
} from '@fortawesome/free-solid-svg-icons'
import {
faStar as faStarRegular
} from '@fortawesome/free-regular-svg-icons'
library.add(
faPlus,
faMinus,
faCheck,
faTimes,
faWrench,
faReply,
faRetweet,
faStar,
faStarRegular,
faSmileBeam,
faEllipsisH,
faBookmark,
faEyeSlash,
faThumbtack,
faShareAlt,
faExternalLinkAlt,
faHistory
)
export default {
props: [
'button',
'extra',
'status',
'funcArg',
'animationState',
'getClass',
'getComponent',
'doAction',
'close'
]
}
</script>
diff --git a/src/components/status_action_buttons/status_action_buttons.js b/src/components/status_action_buttons/status_action_buttons.js
index 6eea5e0bb8..60775b3098 100644
--- a/src/components/status_action_buttons/status_action_buttons.js
+++ b/src/components/status_action_buttons/status_action_buttons.js
@@ -1,404 +1,404 @@
import { mapState } from 'vuex'
import ConfirmModal from 'src/components/confirm_modal/confirm_modal.vue'
import ActionButton from './action_button.vue'
import StatusBookmarkFolderMenu from 'src/components/status_bookmark_folder_menu/status_bookmark_folder_menu.vue'
import Popover from 'src/components/popover/popover.vue'
import genRandomSeed from 'src/services/random_seed/random_seed.service.js'
import { library } from '@fortawesome/fontawesome-svg-core'
import {
faPlus,
faMinus,
faCheck,
faTimes,
faWrench,
faReply,
faRetweet,
faStar,
faSmileBeam,
faEllipsisH,
faBookmark,
faEyeSlash,
faThumbtack,
faShareAlt,
faExternalLinkAlt,
faHistory
} from '@fortawesome/free-solid-svg-icons'
import {
faStar as faStarRegular
} from '@fortawesome/free-regular-svg-icons'
library.add(
faPlus,
faMinus,
faCheck,
faTimes,
faWrench,
faReply,
faRetweet,
faStar,
faStarRegular,
faSmileBeam,
faEllipsisH,
faBookmark,
faEyeSlash,
faThumbtack,
faShareAlt,
faExternalLinkAlt,
faHistory
)
const PRIVATE_SCOPES = new Set(['private', 'direct'])
const PUBLIC_SCOPES = new Set(['public', 'unlisted'])
const BUTTONS = [{
// =========
// REPLY
// =========
name: 'reply',
label: 'tool_tip.reply',
icon: 'reply',
active: ({ replying }) => replying,
counter: ({ status }) => status.replies_count,
anon: true,
anonLink: true,
toggleable: true,
closeIndicator: 'times',
activeIndicator: 'none',
action ({ emit }) {
emit('toggleReplying')
return Promise.resolve()
}
}, {
// =========
// REPEAT
// =========
name: 'retweet',
label: 'tool_tip.repeat',
icon ({ status }) {
if (PRIVATE_SCOPES.has(status.visibility)) {
return 'lock'
}
return 'retweet'
},
animated: true,
active: ({ status }) => status.repeated,
counter: ({ status }) => status.repeat_num,
anonLink: true,
interactive: ({ status, loggedIn }) => loggedIn && !PRIVATE_SCOPES.has(status.visibility),
toggleable: true,
confirm: ({ status, getters }) => !status.repeated && getters.mergedConfig.modalOnRepeat,
confirmStrings: {
title: 'status.repeat_confirm_title',
body: 'status.repeat_confirm',
confirm: 'status.repeat_confirm_accept_button',
cancel: 'status.repeat_confirm_cancel_button'
},
action ({ status, dispatch }) {
if (!status.repeated) {
return dispatch('retweet', { id: status.id })
} else {
return dispatch('unretweet', { id: status.id })
}
}
}, {
// =========
// FAVORITE
// =========
name: 'favorite',
label: 'tool_tip.favorite',
icon: ({ status }) => status.favorited
? ['fas', 'star']
: ['far', 'star'],
animated: true,
active: ({ status }) => status.favorited,
counter: ({ status }) => status.fave_num,
anonLink: true,
toggleable: true,
action ({ status, dispatch }) {
if (!status.favorited) {
return dispatch('favorite', { id: status.id })
} else {
return dispatch('unfavorite', { id: status.id })
}
}
}, {
// =========
// EMOJI REACTIONS
// =========
name: 'emoji',
label: 'tool_tip.add_reaction',
icon: ['far', 'smile-beam'],
anonLink: true,
popover: 'emoji-picker'
}, {
// =========
// MUTE CONVERSATION, my beloved
// =========
- name: 'mute-conversation',
+ name: 'mute',
icon: 'eye-slash',
label: ({ status }) => status.thread_muted
? 'status.unmute_conversation'
: 'status.mute_conversation',
if: ({ loggedIn }) => loggedIn,
toggleable: true,
action ({ status, dispatch, emit }) {
if (status.thread_muted) {
return dispatch('unmuteConversation', { id: status.id })
} else {
return dispatch('muteConversation', { id: status.id })
}
}
}, {
// =========
// PIN STATUS
// =========
name: 'pin',
icon: 'thumbtack',
label: ({ status }) => status.pinned
? 'status.pin'
: 'status.unpin',
if ({ status, loggedIn, currentUser }) {
return loggedIn &&
status.user.id === currentUser.id &&
PUBLIC_SCOPES.has(status.visibility)
},
action ({ status, dispatch, emit }) {
if (status.pinned) {
return dispatch('unpinStatus', { id: status.id })
} else {
return dispatch('pinStatus', { id: status.id })
}
}
}, {
// =========
// BOOKMARK
// =========
name: 'bookmark',
icon: 'bookmark',
label: ({ status }) => status.bookmarked
? 'status.bookmark'
: 'status.unbookmark',
if: ({ loggedIn }) => loggedIn,
action ({ status, dispatch, emit }) {
if (status.bookmarked) {
return dispatch('unbookmark', { id: status.id })
} else {
return dispatch('bookmark', { id: status.id })
}
}
}, {
// =========
// EDIT
// =========
name: 'edit',
icon: 'pen',
label: 'status.edit',
if ({ status, loggedIn, currentUser, state }) {
return loggedIn &&
state.instance.editingAvailable &&
status.user.id === currentUser.id
},
action ({ dispatch, status }) {
return dispatch('fetchStatusSource', { id: status.id })
.then(data => dispatch('openEditStatusModal', {
statusId: status.id,
subject: data.spoiler_text,
statusText: data.text,
statusIsSensitive: status.nsfw,
statusPoll: status.poll,
statusFiles: [...status.attachments],
visibility: status.visibility,
statusContentType: data.content_type
}))
}
}, {
// =========
// DELETE
// =========
name: 'delete',
icon: 'times',
label: 'status.delete',
if ({ status, loggedIn, currentUser }) {
return loggedIn && (
status.user.id === currentUser.id ||
currentUser.privileges.includes('messages_delete')
)
},
confirm: ({ status, getters }) => getters.mergedConfig.modalOnDelete,
confirmStrings: {
title: 'status.delete_confirm_title',
body: 'status.delete_confirm',
confirm: 'status.delete_confirm_accept_button',
cancel: 'status.delete_confirm_cancel_button'
},
action ({ dispatch, status }) {
return dispatch('deleteStatus', { id: status.id })
}
}, {
// =========
// SHARE/COPY
// =========
name: 'share',
icon: 'share-alt',
label: 'status.copy_link',
action ({ state, status, router }) {
navigator.clipboard.writeText([
state.instance.server,
router.resolve({ name: 'conversation', params: { id: status.id } }).href
].join(''))
return Promise.resolve()
}
}, {
// =========
// EXTERNAL
// =========
name: 'external',
icon: 'external-link-alt',
label: 'status.external_source',
link: ({ status }) => status.external_url
}, {
// =========
// REPORT
// =========
name: 'report',
icon: 'flag',
label: 'user_card.report',
if: ({ loggedIn }) => loggedIn,
action ({ dispatch, status }) {
dispatch('openUserReportingModal', { userId: status.user.id, statusIds: [status.id] })
}
}].map(button => {
return Object.fromEntries(
Object.entries(button).map(([k, v]) => [
k,
(typeof v === 'function' || k === 'name') ? v : () => v
])
)
})
const StatusActionButtons = {
props: ['status', 'replying'],
emits: ['toggleReplying'],
data () {
return {
animationState: {
retweet: false,
favorite: false
},
showPin: false,
showingConfirmDialog: false,
currentConfirmTitle: '',
currentConfirmOkText: '',
currentConfirmCancelText: '',
currentConfirmAction: () => {},
randomSeed: genRandomSeed()
}
},
components: {
Popover,
ConfirmModal,
ActionButton,
StatusBookmarkFolderMenu
},
computed: {
...mapState({
pinnedItems: state => new Set(state.serverSideStorage.prefsStorage.collections.pinnedStatusActions)
}),
buttons () {
return BUTTONS.filter(x => x.if ? x.if(this.funcArg) : true)
},
quickButtons () {
return this.buttons.filter(x => this.pinnedItems.has(x.name))
},
extraButtons () {
return this.buttons.filter(x => !this.pinnedItems.has(x.name))
},
currentUser () {
return this.$store.state.users.currentUser
},
funcArg () {
return {
status: this.status,
replying: this.replying,
emit: this.$emit,
dispatch: this.$store.dispatch,
state: this.$store.state,
getters: this.$store.getters,
router: this.$router,
currentUser: this.currentUser,
loggedIn: !!this.currentUser
}
},
triggerAttrs () {
return {
title: this.$t('status.more_actions'),
'aria-controls': `popup-menu-${this.randomSeed}`,
'aria-expanded': this.expanded,
'aria-haspopup': 'menu'
}
}
},
methods: {
doAction (button) {
if (button.confirm?.(this.funcArg)) {
this.currentConfirmTitle = this.$t(button.confirmStrings(this.funcArg).title)
this.currentConfirmOkText = this.$t(button.confirmStrings(this.funcArg).confirm)
this.currentConfirmCancelText = this.$t(button.confirmStrings(this.funcArg).cancel)
this.currentConfirmBody = this.$t(button.confirmStrings(this.funcArg).body)
this.currentConfirmAction = () => {
this.showingConfirmDialog = false
this.doActionReal(button)
}
this.showingConfirmDialog = true
} else {
this.doActionReal(button)
}
},
doActionReal (button) {
this.animationState[button.name] = true
button.action(this.funcArg)
.then(() => this.$emit('onSuccess'))
.catch(err => this.$emit('onError', err.error.error))
.finally(() => setTimeout(() => { this.animationState[button.name] = false }))
},
isPinned (button) {
console.log(this.pinnedItems, button.name)
return this.pinnedItems.has(button.name)
},
unpin (button) {
this.$store.commit('removeCollectionPreference', { path: 'collections.pinnedStatusActions', value: button.name })
this.$store.dispatch('pushServerSideStorage')
},
pin (button) {
this.$store.commit('addCollectionPreference', { path: 'collections.pinnedStatusActions', value: button.name })
this.$store.dispatch('pushServerSideStorage')
},
getComponent (button) {
if (!this.$store.state.users.currentUser && button.anonLink) {
return 'a'
} else if (button.action == null && button.link != null) {
return 'a'
} else {
return 'button'
}
},
getClass (button) {
return {
[button.name + '-button']: true,
disabled: button.interactive ? !button.interactive(this.funcArg) : false,
'-pin-edit': this.showPin,
'-dropdown': button.dropdown?.(),
'-active': button.active?.(this.funcArg)
}
},
getRemoteInteractionLink () {
return this.$store.getters.remoteInteractionLink({ statusId: this.status.id })
}
}
}
export default StatusActionButtons
diff --git a/src/components/status_action_buttons/status_action_buttons.vue b/src/components/status_action_buttons/status_action_buttons.vue
index d8573cdf7d..79b7a4d6ae 100644
--- a/src/components/status_action_buttons/status_action_buttons.vue
+++ b/src/components/status_action_buttons/status_action_buttons.vue
@@ -1,175 +1,192 @@
<template>
<div class="StatusActionButtons">
<span class="quick-action-buttons">
<span
class="quick-action"
:class="{ '-pin': showPin, '-toggle': button.dropdown?.() }"
v-for="button in quickButtons"
:key="button.name"
>
<component
:is="getComponent(button)"
class="button-unstyled action-button"
:class="getClass(button)"
:disabled="getClass(button).disabled"
role="button"
:tabindex="0"
:title="$t(button.label(funcArg))"
@click.stop="getComponent(button) === 'button' && doAction(button)"
:href="getComponent(button) == 'a' ? button.link?.(funcArg) || getRemoteInteractionLink : undefined"
>
<FALayers>
<FAIcon
class="fa-scale-110"
:icon="button.icon(funcArg)"
/>
<template v-if="!getClass(button).disabled && button.toggleable?.(funcArg) && button.active">
<FAIcon
v-if="button.active(funcArg)"
class="active-marker"
transform="shrink-6 up-9 right-12"
:icon="button.activeIndicator?.(funcArg) || 'check'"
/>
<FAIcon
v-if="!button.active(funcArg)"
class="focus-marker"
transform="shrink-6 up-9 right-12"
:icon="button.openIndicator?.(funcArg) || 'plus'"
/>
<FAIcon
v-else
class="focus-marker"
transform="shrink-6 up-9 right-12"
:icon="button.closeIndicator?.(funcArg) || 'minus'"
/>
</template>
</FALayers>
<span
class="action-counter"
v-if="button.counter?.(funcArg) > 0"
>
{{ button.counter?.(funcArg) }}
</span>
</component>
<button
v-if="showPin && currentUser"
type="button"
class="button-unstyled pin-action-button"
:title="$t('general.unpin')"
:aria-pressed="true"
@click.stop.prevent="unpin(button)"
>
<FAIcon
v-if="showPin && currentUser"
fixed-width
class="fa-scale-110"
icon="thumbtack"
/>
</button>
</span>
<Popover
trigger="click"
:trigger-attrs="triggerAttrs"
:tabindex="0"
placement="top"
:offset="{ y: 5 }"
:bound-to="{ x: 'container' }"
remove-padding
@show="onShow"
@close="onClose"
>
<template #trigger>
<FAIcon
class="fa-scale-110 "
icon="ellipsis-h"
/>
</template>
<template #content="{close}">
<div
:id="`popup-menu-${randomSeed}`"
class="dropdown-menu extra-action-buttons"
role="menu"
>
<div class="menu-item dropdown-item extra-action -icon">
<button
class="main-button"
role="menuitem"
:tabindex="0"
@click.stop="showPin = !showPin"
>
<FAIcon
class="fa-scale-110"
fixed-width
icon="wrench"
/><span>{{ $t('nav.edit_pinned') }}</span>
</button>
</div>
<div
v-for="button in extraButtons"
:key="button.name"
class="menu-item dropdown-item extra-action -icon"
:disabled="getClass(button).disabled"
:class="{ disabled: getClass(button).disabled }"
>
<Popover
v-if="getComponent(button) === 'button'"
trigger="hover"
placement="right"
>
<template #trigger>
<ActionButton
:button="button"
:status="status"
:extra="true"
:funcArg="funcArg"
:get-class="getClass"
:get-component="getComponent"
:animation-state="animationState"
:close="close"
:do-action="doAction"
/>
</template>
+ <template #content>
+ <StatusBookmarkFolderMenu v-if="button.name === 'mute'" :status="funcArg.status" />
+ </template>
+ </Popover>
+ <Popover
+ trigger="hover"
+ placement="right"
+ :trigger-attrs="{ class: 'extra-button' }"
+ v-if="button.name === 'bookmark'"
+ >
+ <template #trigger>
+ <FAIcon
+ class="chevron-icon"
+ size="lg"
+ icon="chevron-right"
+ />
+ </template>
<template #content>
<StatusBookmarkFolderMenu v-if="button.name === 'bookmark'" :status="funcArg.status" />
</template>
</Popover>
<button
v-if="showPin && currentUser"
type="button"
class="button-unstyled pin-action-button"
:title="$t('general.pin')"
:aria-pressed="false"
@click.stop.prevent="pin(button)"
>
<FAIcon
v-if="showPin && currentUser"
fixed-width
class="fa-scale-110"
transform="rotate-45"
icon="thumbtack"
/>
</button>
</div>
</div>
</template>
</Popover>
</span>
<teleport to="#modal">
<confirm-modal
v-if="showingConfirmDialog"
:title="currentConfirmTitle"
:confirm-text="currentConfirmOkText"
:cancel-text="currentConfirmCancelText"
@accepted="currentConfirmAction"
@cancelled="showingConfirmDialog = false"
>
{{ currentConfirmBody }}
</confirm-modal>
</teleport>
</div>
</template>
<script src="./status_action_buttons.js"></script>
<style lang="scss" src="./status_action_buttons.scss"></style>
diff --git a/src/components/user_list_menu/user_list_menu.vue b/src/components/user_list_menu/user_list_menu.vue
index 446441b38b..a1ba96d89a 100644
--- a/src/components/user_list_menu/user_list_menu.vue
+++ b/src/components/user_list_menu/user_list_menu.vue
@@ -1,39 +1,39 @@
<template>
<div class="UserListMenu">
<Popover
trigger="hover"
- placement="left"
+ placement="right"
:trigger-attrs="triggerAttrs"
remove-padding
>
<template #content>
<div class="dropdown-menu">
<button
v-for="list in lists"
:key="list.id"
class="menu-item dropdown-item -icon"
@click="toggleList(list.id)"
>
<span
class="input menu-checkbox"
:class="{ 'menu-checkbox-checked': list.inList }"
/>
{{ list.title }}
</button>
</div>
</template>
<template #trigger>
<span class="main-button">
{{ $t('lists.manage_lists') }}
<FAIcon
class="chevron-icon"
size="lg"
icon="chevron-right"
/>
</span>
</template>
</Popover>
</div>
</template>
<script src="./user_list_menu.js"></script>
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Sat, Sep 19, 8:14 AM (1 d, 18 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1769150
Default Alt Text
(39 KB)
Attached To
Mode
rPUFE pleroma-fe-upstream
Attached
Detach File
Event Timeline
Log In to Comment