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 MentionsLine from 'src/components/mentions_line/mentions_line.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 { highlightClass, highlightStyle } from '../../services/user_highlighter/user_highlighter.js'
import { muteWordHits } from '../../services/status_parser/status_parser.js'
@ -119,7 +120,8 @@ const Status = {
MentionLink,
MentionsLine,
UserPopover,
UserLink
UserLink,
StatusActionButtons
},
props: [
'statusoid',

View file

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

View file

@ -28,7 +28,8 @@ const BUTTONS = [{
anonLink: true,
toggleable: true,
action ({ emit }) {
emit('toggle')
emit('toggleReplying')
return Promise.resolve()
}
}, {
// =========
@ -44,11 +45,16 @@ const BUTTONS = [{
},
animated: true,
active: ({ status }) => status.repeated,
counter: ({ status }) => status.replies_count,
counter: ({ status }) => status.repeat_num,
anonLink: true,
interactive: ({ status }) => !PRIVATE_SCOPES.has(status.visibility),
toggleable: true,
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 }) {
if (!status.repeated) {
return store.dispatch('retweet', { id: status.id })
@ -62,10 +68,12 @@ const BUTTONS = [{
// =========
name: 'favorite',
label: 'tool_tip.favorite',
icon: 'star',
icon: ({ status }) => status.favorited
? ['fas', 'star']
: ['far', 'star'],
animated: true,
active: ({ status }) => status.favorited,
counter: ({ status }) => status.fave_count,
counter: ({ status }) => status.fave_num,
anonLink: true,
toggleable: true,
action ({ status, store }) {
@ -81,11 +89,9 @@ const BUTTONS = [{
// =========
name: 'emoji',
label: 'tool_lip.add_reaction',
icon: 'smile-beam',
icon: ['far', 'smile-beam'],
anonLink: true,
action ({ emojiPicker }) {
emojiPicker.show()
}
popover: 'emoji-picker'
}, {
// =========
// MUTE CONVERSATION, my beloved
@ -141,7 +147,8 @@ const BUTTONS = [{
} else {
return dispatch('bookmark', { id: status.id })
}
}
},
popover: 'bookmark-folders'
}, {
// =========
// EDIT
@ -180,8 +187,13 @@ const BUTTONS = [{
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 }) {
dispatch('deleteStatus', { id: status.id })
return dispatch('deleteStatus', { id: status.id })
}
}, {
// =========
@ -195,6 +207,7 @@ const BUTTONS = [{
state.instance.server,
router.resolve({ name: 'conversation', params: { id: status.id } }).href
].join(''))
return Promise.resolve()
}
}, {
// =========
@ -210,58 +223,74 @@ const BUTTONS = [{
// =========
name: 'report',
icon: 'flag',
label: 'status.report',
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' ? v : () => v])
)
})
console.log(BUTTONS)
const StatusActionButtons = {
props: ['status'],
props: ['status', 'replying'],
emits: ['toggleReplying'],
data () {
return {
buttons: BUTTONS,
showingConfirmDialog: false,
currentConfirmTitle: '',
currentConfirmOkText: '',
currentConfirmCancelText: '',
currentConfirmAction: () => {}
}
},
components: {
ConfirmModal
},
data () {
return {
}
},
methods: {
retweet () {
if (!this.status.repeated && this.shouldConfirmRepeat) {
this.showConfirmDialog()
doAction (button) {
this.doActionReal(button)
},
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 {
this.doRetweet()
return 'button'
}
},
doRetweet () {
if (!this.status.repeated) {
this.$store.dispatch('retweet', { id: this.status.id })
} else {
this.$store.dispatch('unretweet', { id: this.status.id })
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.$store.state.users.currentUser,
loggedIn: !!this.$store.state.users.currentUser
}
this.animated = true
setTimeout(() => {
this.animated = false
}, 500)
this.hideConfirmDialog()
},
showConfirmDialog () {
this.showingConfirmDialog = true
getClass (button) {
return {
[button.name() + '-button']: true,
'-active': button.active?.(this.funcArg()),
'-interactive': !!this.$store.state.users.currentUser
}
},
hideConfirmDialog () {
this.showingConfirmDialog = false
}
},
computed: {
mergedConfig () {
return this.$store.getters.mergedConfig
},
remoteInteractionLink () {
getRemoteInteractionLink () {
return this.$store.getters.remoteInteractionLink({ statusId: this.status.id })
},
shouldConfirmRepeat () {
return this.mergedConfig.modalOnRepeat
}
}
}

View file

@ -1,21 +1,135 @@
<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>
<script src="./status_buttons.js"></script>
<script src="./status_action_buttons.js"></script>
<style lang="scss">
@import "../../mixins";
.status-actions {
position: relative;
.StatusActionButtons {
width: 100%;
.quick-action-buttons {
position: relative;
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);
> * {
.quick-action {
display: grid;
grid-template-columns: auto 1fr;
grid-gap: 0.5em;
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>

View file

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