Page MenuHomePhorge

No OneTemporary

Size
25 KB
Referenced Files
None
Subscribers
None
diff --git a/src/components/popover/popover.scss b/src/components/popover/popover.scss
index 9ec1ff1100..5dfedf7299 100644
--- a/src/components/popover/popover.scss
+++ b/src/components/popover/popover.scss
@@ -1,105 +1,118 @@
.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) > button:first-child,
.dropdown-item:not(button, a) > a:first-child {
box-sizing: border-box;
padding: var(--__horizontal-gap) var(--__horizontal-gap);
grid-gap: var(--__horizontal-gap);
display: grid;
border: none;
align-items: center;
grid-template-columns: 1fr var(--__line-height);
grid-auto-flow: column;
grid-auto-columns: auto;
- cursor: pointer;
.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: "•";
}
}
}
}
a.dropdown-item-icon,
button.dropdown-item-icon,
.dropdown-item-icon:not(button, a) > button:first-child,
.dropdown-item-icon:not(button, a) > a:first-child {
grid-template-columns: var(--__line-height) 1fr;
}
+
+ a.dropdown-item:not(.disabled),
+ button.dropdown:not(.disabled),
+ .dropdown-item:not(.disabled, button, a) > button:first-child,
+ .dropdown-item:not(.disabled, button, a) > a:first-child {
+ cursor: pointer;
+ }
+
+ a.dropdown-item.disabled,
+ button.dropdown.disabled,
+ .dropdown-item.disabled:not(button, a) > button:first-child,
+ .dropdown-item.disabled:not(button, a) > a:first-child {
+ cursor: not-allowed;
+ }
}
diff --git a/src/components/status_action_buttons/status_action_buttons.js b/src/components/status_action_buttons/status_action_buttons.js
index 0ce3b4a5d8..c45aa82277 100644
--- a/src/components/status_action_buttons/status_action_buttons.js
+++ b/src/components/status_action_buttons/status_action_buttons.js
@@ -1,396 +1,402 @@
import { mapState } from 'vuex'
import ConfirmModal from 'src/components/confirm_modal/confirm_modal.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 }) => !PRIVATE_SCOPES.has(status.visibility),
+ 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',
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 })
}
},
popover: 'bookmark-folders'
}, {
// =========
// 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
},
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'),
id: `popup-trigger-${this.randomSeed}`,
'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')
},
component (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),
- '-interactive': !!this.$store.state.users.currentUser
+ '-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.scss b/src/components/status_action_buttons/status_action_buttons.scss
index 5a230af8e5..a366c61d36 100644
--- a/src/components/status_action_buttons/status_action_buttons.scss
+++ b/src/components/status_action_buttons/status_action_buttons.scss
@@ -1,134 +1,161 @@
@import "../../mixins";
.StatusActionButtons {
.quick-action-buttons {
display: grid;
grid-template-columns: 1fr;
grid-auto-flow: column;
grid-auto-columns: 1fr;
grid-gap: 1em;
margin-top: var(--status-margin);
.quick-action {
display: grid;
grid-template-columns: 1fr auto;
.action-button {
display: grid;
grid-template-columns: max-content auto;
grid-gap: 1em;
align-items: center;
+
+ &.disabled {
+ cursor: not-allowed;
+ }
}
&.-pin {
margin: calc(-2px - 0.25em);
padding: 0.25em;
border: 2px dashed var(--icon);
border-radius: var(--roundness);
}
&.-pin,
&.-dropdown {
grid-template-columns: 1fr max-content;
}
- .reply-button {
+ .reply-button:not(.disabled) {
&:hover,
&.-active {
.svg-inline--fa {
color: var(--cBlue);
}
}
}
- .retweet-button {
+ .retweet-button:not(.disabled) {
&:hover,
&.-active {
.svg-inline--fa {
color: var(--cGreen);
}
}
}
- .favorite-button {
+ .favorite-button:not(.disabled) {
&:hover,
&.-active {
.svg-inline--fa {
color: var(--cOrange);
}
}
}
> button,
> a {
padding: 0.5em;
margin: -0.5em;
@include unfocused-style {
.focus-marker {
visibility: hidden;
}
.active-marker {
visibility: visible;
}
}
@include focused-style {
.focus-marker {
visibility: visible;
}
.active-marker {
visibility: hidden;
}
}
}
}
}
}
// popover
/* stylelint-disable no-descending-specificity */
.extra-action-buttons {
.extra-action {
display: grid;
grid-template-columns: 1fr;
grid-auto-flow: column;
grid-auto-columns: auto;
grid-gap: 1em;
.pin-action-button {
margin: 0;
padding: var(--__horizontal-gap) var(--__horizontal-gap);
}
}
- .reply-button {
+ > a,
+ > button {
+ @include unfocused-style {
+ .focus-marker {
+ visibility: hidden;
+ }
+
+ .active-marker {
+ visibility: visible;
+ }
+ }
+
+ @include focused-style {
+ .focus-marker {
+ visibility: visible;
+ }
+
+ .active-marker {
+ visibility: hidden;
+ }
+ }
+ }
+
+ .reply-button:not(.disabled) {
&:hover,
&.-active {
.svg-inline--fa {
color: var(--cBlue);
}
}
}
- .retweet-button {
+ .retweet-button:not(.disabled) {
&:hover,
&.-active {
.svg-inline--fa {
color: var(--cGreen);
}
}
}
- .favorite-button {
+ .favorite-button:not(.disabled) {
&:hover,
&.-active {
.svg-inline--fa {
color: var(--cOrange);
}
}
}
}
/* stylelint-enable no-descending-specificity */
diff --git a/src/components/status_action_buttons/status_action_buttons.vue b/src/components/status_action_buttons/status_action_buttons.vue
index 41356942e5..a0a58461d4 100644
--- a/src/components/status_action_buttons/status_action_buttons.vue
+++ b/src/components/status_action_buttons/status_action_buttons.vue
@@ -1,188 +1,193 @@
<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="component(button)"
class="button-unstyled action-button"
:class="getClass(button)"
+ :disabled="getClass(button).disabled"
role="button"
:tabindex="0"
:title="$t(button.label(funcArg))"
@click.stop="component(button) === 'button' && doAction(button)"
:href="component(button) == 'a' ? button.link?.(funcArg) || getRemoteInteractionLink : undefined"
>
<FALayers>
<FAIcon
class="fa-scale-110"
:icon="button.icon(funcArg)"
/>
- <template v-if="button.toggleable?.(funcArg) && button.active">
+ <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 dropdown-item-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 dropdown-item-icon"
+ :disabled="getClass(button).disabled"
+ :class="{ disabled: getClass(button).disabled }"
>
<component
:is="component(button)"
class="main-button"
role="menuitem"
:class="getClass(button)"
:tabindex="0"
+ :disabled="getClass(button).disabled"
@click.stop="component(button) === 'button' && doAction(button)"
@click="close"
:href="component(button) == 'a' ? button.link?.(funcArg) || getRemoteInteractionLink : undefined"
>
<FALayers>
<FAIcon
class="fa-scale-110"
:icon="button.icon(funcArg)"
+ :spin="button.animated?.() && animationState[button.name]"
fixed-width
/>
- <template v-if="button.toggleable?.(funcArg) && button.active">
+ <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>
</component>
<button
v-if="showPin && currentUser"
type="button"
class="button-unstyled pin-action-button"
- :title="$t('general.pin' )"
+ :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>

File Metadata

Mime Type
text/x-diff
Expires
Sat, Sep 19, 12:03 AM (7 h, 17 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1768504
Default Alt Text
(25 KB)

Event Timeline