initial work on quick actions

This commit is contained in:
Henry Jameson 2025-01-09 17:43:48 +02:00
parent 35409ad9eb
commit fe84a52dcc
5 changed files with 206 additions and 54 deletions

View file

@ -16,6 +16,7 @@ import EmojiReactions from '../emoji_reactions/emoji_reactions.vue'
import UserLink from '../user_link/user_link.vue' import UserLink from '../user_link/user_link.vue'
import MentionsLine from 'src/components/mentions_line/mentions_line.vue' import MentionsLine from 'src/components/mentions_line/mentions_line.vue'
import MentionLink from 'src/components/mention_link/mention_link.vue' import MentionLink from 'src/components/mention_link/mention_link.vue'
import StatusActionButtons from 'src/components/status_action_buttons/status_action_buttons.vue'
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator' import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
import { highlightClass, highlightStyle } from '../../services/user_highlighter/user_highlighter.js' import { highlightClass, highlightStyle } from '../../services/user_highlighter/user_highlighter.js'
import { muteWordHits } from '../../services/status_parser/status_parser.js' import { muteWordHits } from '../../services/status_parser/status_parser.js'
@ -119,7 +120,8 @@ const Status = {
MentionLink, MentionLink,
MentionsLine, MentionsLine,
UserPopover, UserPopover,
UserLink UserLink,
StatusActionButtons
}, },
props: [ props: [
'statusoid', 'statusoid',

View file

@ -65,7 +65,6 @@
v-if="retweet" v-if="retweet"
class="left-side repeater-avatar" class="left-side repeater-avatar"
:show-actor-type-indicator="showActorTypeIndicator" :show-actor-type-indicator="showActorTypeIndicator"
:better-shadow="betterShadow"
:user="statusoid.user" :user="statusoid.user"
/> />
<div class="right-side faint"> <div class="right-side faint">
@ -120,7 +119,6 @@
class="post-avatar" class="post-avatar"
:show-actor-type-indicator="showActorTypeIndicator" :show-actor-type-indicator="showActorTypeIndicator"
:compact="compact" :compact="compact"
:better-shadow="betterShadow"
:user="status.user" :user="status.user"
/> />
</UserPopover> </UserPopover>
@ -537,6 +535,12 @@
:status="status" :status="status"
/> />
<StatusActionButtons
v-if="!noHeading && !isPreview"
:status="status"
:replying="replying"
@toggleReplying="toggleReplying"
/>
<div <div
v-if="!noHeading && !isPreview" v-if="!noHeading && !isPreview"
class="status-actions" class="status-actions"
@ -600,12 +604,14 @@
<PostStatusForm <PostStatusForm
ref="postStatusForm" ref="postStatusForm"
class="reply-body" class="reply-body"
:closeable="true"
:reply-to="status.id" :reply-to="status.id"
:attentions="status.attentions" :attentions="status.attentions"
:replied-user="status.user" :replied-user="status.user"
:copy-message-scope="status.visibility" :copy-message-scope="status.visibility"
:subject="replySubject" :subject="replySubject"
@posted="doToggleReplying" @posted="doToggleReplying"
@draft-done="doToggleReplying"
@can-close="doToggleReplying" @can-close="doToggleReplying"
/> />
</div> </div>

View file

@ -28,7 +28,8 @@ const BUTTONS = [{
anonLink: true, anonLink: true,
toggleable: true, toggleable: true,
action ({ emit }) { action ({ emit }) {
emit('toggle') emit('toggleReplying')
return Promise.resolve()
} }
}, { }, {
// ========= // =========
@ -44,11 +45,16 @@ const BUTTONS = [{
}, },
animated: true, animated: true,
active: ({ status }) => status.repeated, active: ({ status }) => status.repeated,
counter: ({ status }) => status.replies_count, counter: ({ status }) => status.repeat_num,
anonLink: true, anonLink: true,
interactive: ({ status }) => !PRIVATE_SCOPES.has(status.visibility), interactive: ({ status }) => !PRIVATE_SCOPES.has(status.visibility),
toggleable: true, toggleable: true,
confirm: ({ status, getters }) => !status.repeated && getters.mergedConfig.modalOnRepeat, confirm: ({ status, getters }) => !status.repeated && getters.mergedConfig.modalOnRepeat,
confirmStrings: {
title: 'status.repeat_confirm_title',
confirm: 'status.repeat_confirm_accept_button',
cancel: 'status.repeat_confirm_cancel_button'
},
action ({ status, store }) { action ({ status, store }) {
if (!status.repeated) { if (!status.repeated) {
return store.dispatch('retweet', { id: status.id }) return store.dispatch('retweet', { id: status.id })
@ -62,10 +68,12 @@ const BUTTONS = [{
// ========= // =========
name: 'favorite', name: 'favorite',
label: 'tool_tip.favorite', label: 'tool_tip.favorite',
icon: 'star', icon: ({ status }) => status.favorited
? ['fas', 'star']
: ['far', 'star'],
animated: true, animated: true,
active: ({ status }) => status.favorited, active: ({ status }) => status.favorited,
counter: ({ status }) => status.fave_count, counter: ({ status }) => status.fave_num,
anonLink: true, anonLink: true,
toggleable: true, toggleable: true,
action ({ status, store }) { action ({ status, store }) {
@ -81,11 +89,9 @@ const BUTTONS = [{
// ========= // =========
name: 'emoji', name: 'emoji',
label: 'tool_lip.add_reaction', label: 'tool_lip.add_reaction',
icon: 'smile-beam', icon: ['far', 'smile-beam'],
anonLink: true, anonLink: true,
action ({ emojiPicker }) { popover: 'emoji-picker'
emojiPicker.show()
}
}, { }, {
// ========= // =========
// MUTE CONVERSATION, my beloved // MUTE CONVERSATION, my beloved
@ -141,7 +147,8 @@ const BUTTONS = [{
} else { } else {
return dispatch('bookmark', { id: status.id }) return dispatch('bookmark', { id: status.id })
} }
} },
popover: 'bookmark-folders'
}, { }, {
// ========= // =========
// EDIT // EDIT
@ -180,8 +187,13 @@ const BUTTONS = [{
currentUser.privileges.includes('messages_delete') currentUser.privileges.includes('messages_delete')
) )
}, },
confirmStrings: {
title: 'status.delete_confirm_title',
confirm: 'status.delete_confirm_cancel_button',
cancel: 'status.delete_confirm_accept_button'
},
action ({ dispatch, status }) { action ({ dispatch, status }) {
dispatch('deleteStatus', { id: status.id }) return dispatch('deleteStatus', { id: status.id })
} }
}, { }, {
// ========= // =========
@ -195,6 +207,7 @@ const BUTTONS = [{
state.instance.server, state.instance.server,
router.resolve({ name: 'conversation', params: { id: status.id } }).href router.resolve({ name: 'conversation', params: { id: status.id } }).href
].join('')) ].join(''))
return Promise.resolve()
} }
}, { }, {
// ========= // =========
@ -210,58 +223,74 @@ const BUTTONS = [{
// ========= // =========
name: 'report', name: 'report',
icon: 'flag', icon: 'flag',
label: 'status.report', label: 'user_card.report',
if: ({ loggedIn }) => loggedIn, if: ({ loggedIn }) => loggedIn,
action ({ dispatch, status }) { action ({ dispatch, status }) {
dispatch('openUserReportingModal', { userId: status.user.id, statusIds: [status.id] }) dispatch('openUserReportingModal', { userId: status.user.id, statusIds: [status.id] })
} }
}] }].map(button => {
return Object.fromEntries(
Object.entries(button).map(([k, v]) => [k, typeof v === 'function' ? v : () => v])
)
})
console.log(BUTTONS)
const StatusActionButtons = { const StatusActionButtons = {
props: ['status'], props: ['status', 'replying'],
emits: ['toggleReplying'],
data () {
return {
buttons: BUTTONS,
showingConfirmDialog: false,
currentConfirmTitle: '',
currentConfirmOkText: '',
currentConfirmCancelText: '',
currentConfirmAction: () => {}
}
},
components: { components: {
ConfirmModal ConfirmModal
}, },
data () {
return {
}
},
methods: { methods: {
retweet () { doAction (button) {
if (!this.status.repeated && this.shouldConfirmRepeat) { this.doActionReal(button)
this.showConfirmDialog() },
doActionReal (button) {
button.action(this.funcArg(button))
.then(() => this.$emit('onSuccess'))
.catch(err => this.$emit('onError', err.error.error))
},
component (button) {
if (!this.$store.state.users.currentUser && button.anonLink) {
return 'a'
} else if (button.action == null && button.link != null) {
return 'a'
} else { } else {
this.doRetweet() return 'button'
} }
}, },
doRetweet () { funcArg () {
if (!this.status.repeated) { return {
this.$store.dispatch('retweet', { id: this.status.id }) status: this.status,
} else { replying: this.replying,
this.$store.dispatch('unretweet', { id: this.status.id }) emit: this.$emit,
dispatch: this.$store.dispatch,
state: this.$store.state,
getters: this.$store.getters,
router: this.$router,
currentUser: this.$store.state.users.currentUser,
loggedIn: !!this.$store.state.users.currentUser
} }
this.animated = true
setTimeout(() => {
this.animated = false
}, 500)
this.hideConfirmDialog()
}, },
showConfirmDialog () { getClass (button) {
this.showingConfirmDialog = true return {
[button.name() + '-button']: true,
'-active': button.active?.(this.funcArg()),
'-interactive': !!this.$store.state.users.currentUser
}
}, },
hideConfirmDialog () { getRemoteInteractionLink () {
this.showingConfirmDialog = false
}
},
computed: {
mergedConfig () {
return this.$store.getters.mergedConfig
},
remoteInteractionLink () {
return this.$store.getters.remoteInteractionLink({ statusId: this.status.id }) return this.$store.getters.remoteInteractionLink({ statusId: this.status.id })
},
shouldConfirmRepeat () {
return this.mergedConfig.modalOnRepeat
} }
} }
} }

View file

@ -1,21 +1,135 @@
<template> <template>
<div class="StatusActionButtons">
<span class="quick-action-buttons">
<span
class="quick-action"
v-for="button in buttons"
:key="button.name()"
>
<component
:is="component(button)"
class="button-unstyled"
:class="getClass(button)"
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 class="fa-old-padding">
<FAIcon
class="fa-scale-110"
:icon="button.icon(funcArg())"
/>
<template v-if="button.toggleable?.(funcArg()) && button.active">
<FAIcon
v-show="!button.active(funcArg())"
class="focus-marker"
transform="shrink-6 up-9 right-17"
icon="plus"
/>
<FAIcon
v-show="button.active(funcArg())"
class="focus-marker"
transform="shrink-6 up-9 right-17"
icon="times"
/>
</template>
</FALayers>
</component>
<span
class="action-counter"
v-if="button.counter?.(funcArg()) > 0"
>
{{ button.counter?.(funcArg()) }}
</span>
</span>
</span>
<teleport to="#modal">
<confirm-modal
v-if="showingConfirmDialog"
:title="currentConfirmTitle"
:confirm-text="currentConfirmOkText"
:cancel-text="currentConfirmCancelText"
@accepted="currentConfirmAction"
@cancelled="hideConfirmDialog"
>
{{ $t('status.repeat_confirm') }}
</confirm-modal>
</teleport>
</div>
</template> </template>
<script src="./status_buttons.js"></script> <script src="./status_action_buttons.js"></script>
<style lang="scss"> <style lang="scss">
@import "../../mixins"; @import "../../mixins";
.status-actions { .StatusActionButtons {
position: relative; width: 100%;
.quick-action-buttons {
position: relative;
width: 100%; width: 100%;
display: flex; display: grid;
grid-template-columns: 1fr;
grid-auto-flow: column;
grid-auto-columns: 1fr;
grid-gap: 1em;
margin-top: var(--status-margin); margin-top: var(--status-margin);
> * { .quick-action {
display: grid;
grid-template-columns: auto 1fr;
grid-gap: 0.5em;
max-width: 4em; max-width: 4em;
flex: 1;
.reply-button {
&:hover,
&.-active {
.svg-inline--fa {
color: var(--cBlue);
}
}
}
.retweet-button {
&:hover,
&.-active {
.svg-inline--fa {
color: var(--cGreen);
}
}
}
.favorite-button {
&:hover,
&.-active {
.svg-inline--fa {
color: var(--cOrange);
}
}
}
> button,
> a {
padding: 0.5em;
margin: -0.5em;
}
@include unfocused-style {
.focus-marker {
visibility: hidden;
}
}
@include focused-style {
.focus-marker {
visibility: visible;
}
}
} }
} }
}
</style> </style>

View file

@ -1410,6 +1410,7 @@
"mentions": "Mentions", "mentions": "Mentions",
"repeat": "Repeat", "repeat": "Repeat",
"reply": "Reply", "reply": "Reply",
"add_reaction": "Add reaction",
"favorite": "Favorite", "favorite": "Favorite",
"add_reaction": "Add Reaction", "add_reaction": "Add Reaction",
"user_settings": "User Settings", "user_settings": "User Settings",