From 35409ad9ebb366202bfe4f685c62bd05d433df99 Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Thu, 9 Jan 2025 00:01:32 +0200 Subject: [PATCH 01/48] initial buttons definitions --- .../status_action_buttons.js | 269 ++++++++++++++++++ .../status_action_buttons.vue | 21 ++ 2 files changed, 290 insertions(+) create mode 100644 src/components/status_action_buttons/status_action_buttons.js create mode 100644 src/components/status_action_buttons/status_action_buttons.vue diff --git a/src/components/status_action_buttons/status_action_buttons.js b/src/components/status_action_buttons/status_action_buttons.js new file mode 100644 index 000000000..5de69d56d --- /dev/null +++ b/src/components/status_action_buttons/status_action_buttons.js @@ -0,0 +1,269 @@ +import ConfirmModal from '../confirm_modal/confirm_modal.vue' +import { library } from '@fortawesome/fontawesome-svg-core' +import { + faRetweet, + faPlus, + faMinus, + faCheck +} from '@fortawesome/free-solid-svg-icons' + +library.add( + faRetweet, + faPlus, + faMinus, + faCheck +) +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, + action ({ emit }) { + emit('toggle') + } +}, { + // ========= + // 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.replies_count, + anonLink: true, + interactive: ({ status }) => !PRIVATE_SCOPES.has(status.visibility), + toggleable: true, + confirm: ({ status, getters }) => !status.repeated && getters.mergedConfig.modalOnRepeat, + action ({ status, store }) { + if (!status.repeated) { + return store.dispatch('retweet', { id: status.id }) + } else { + return store.dispatch('unretweet', { id: status.id }) + } + } +}, { + // ========= + // FAVORITE + // ========= + name: 'favorite', + label: 'tool_tip.favorite', + icon: 'star', + animated: true, + active: ({ status }) => status.favorited, + counter: ({ status }) => status.fave_count, + anonLink: true, + toggleable: true, + action ({ status, store }) { + if (!status.favorited) { + return store.dispatch('favorite', { id: status.id }) + } else { + return store.dispatch('unfavorite', { id: status.id }) + } + } +}, { + // ========= + // EMOJI REACTIONS + // ========= + name: 'emoji', + label: 'tool_lip.add_reaction', + icon: 'smile-beam', + anonLink: true, + action ({ emojiPicker }) { + emojiPicker.show() + } +}, { + // ========= + // 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 }) + } + } +}, { + // ========= + // 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') + ) + }, + action ({ dispatch, status }) { + 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('')) + } +}, { + // ========= + // EXTERNAL + // ========= + name: 'external', + icon: 'external-link-alt', + label: 'status.external_source', + link: ({ status }) => status.external_url +}, { + // ========= + // REPORT + // ========= + name: 'report', + icon: 'flag', + label: 'status.report', + if: ({ loggedIn }) => loggedIn, + action ({ dispatch, status }) { + dispatch('openUserReportingModal', { userId: status.user.id, statusIds: [status.id] }) + } +}] + +const StatusActionButtons = { + props: ['status'], + components: { + ConfirmModal + }, + data () { + return { + } + }, + methods: { + retweet () { + if (!this.status.repeated && this.shouldConfirmRepeat) { + this.showConfirmDialog() + } else { + this.doRetweet() + } + }, + doRetweet () { + if (!this.status.repeated) { + this.$store.dispatch('retweet', { id: this.status.id }) + } else { + this.$store.dispatch('unretweet', { id: this.status.id }) + } + this.animated = true + setTimeout(() => { + this.animated = false + }, 500) + this.hideConfirmDialog() + }, + showConfirmDialog () { + this.showingConfirmDialog = true + }, + hideConfirmDialog () { + this.showingConfirmDialog = false + } + }, + computed: { + mergedConfig () { + return this.$store.getters.mergedConfig + }, + remoteInteractionLink () { + return this.$store.getters.remoteInteractionLink({ statusId: this.status.id }) + }, + shouldConfirmRepeat () { + return this.mergedConfig.modalOnRepeat + } + } +} + +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 new file mode 100644 index 000000000..83cd1eb73 --- /dev/null +++ b/src/components/status_action_buttons/status_action_buttons.vue @@ -0,0 +1,21 @@ + + + + + From fe84a52dcc0a1cfe5088363edd5116bd0a61f36e Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Thu, 9 Jan 2025 17:43:48 +0200 Subject: [PATCH 02/48] initial work on quick actions --- src/components/status/status.js | 4 +- src/components/status/status.vue | 10 +- .../status_action_buttons.js | 119 ++++++++++------- .../status_action_buttons.vue | 126 +++++++++++++++++- src/i18n/en.json | 1 + 5 files changed, 206 insertions(+), 54 deletions(-) diff --git a/src/components/status/status.js b/src/components/status/status.js index ccecaac7e..903542564 100644 --- a/src/components/status/status.js +++ b/src/components/status/status.js @@ -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', diff --git a/src/components/status/status.vue b/src/components/status/status.vue index c58178b3c..4e6c3f2d1 100644 --- a/src/components/status/status.vue +++ b/src/components/status/status.vue @@ -65,7 +65,6 @@ v-if="retweet" class="left-side repeater-avatar" :show-actor-type-indicator="showActorTypeIndicator" - :better-shadow="betterShadow" :user="statusoid.user" />
@@ -120,7 +119,6 @@ class="post-avatar" :show-actor-type-indicator="showActorTypeIndicator" :compact="compact" - :better-shadow="betterShadow" :user="status.user" /> @@ -537,6 +535,12 @@ :status="status" /> +
diff --git a/src/components/status_action_buttons/status_action_buttons.js b/src/components/status_action_buttons/status_action_buttons.js index 5de69d56d..2a7c70a41 100644 --- a/src/components/status_action_buttons/status_action_buttons.js +++ b/src/components/status_action_buttons/status_action_buttons.js @@ -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 } } } diff --git a/src/components/status_action_buttons/status_action_buttons.vue b/src/components/status_action_buttons/status_action_buttons.vue index 83cd1eb73..3950e22db 100644 --- a/src/components/status_action_buttons/status_action_buttons.vue +++ b/src/components/status_action_buttons/status_action_buttons.vue @@ -1,21 +1,135 @@ - + diff --git a/src/i18n/en.json b/src/i18n/en.json index 3d7a674ce..a083e5757 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -1410,6 +1410,7 @@ "mentions": "Mentions", "repeat": "Repeat", "reply": "Reply", + "add_reaction": "Add reaction", "favorite": "Favorite", "add_reaction": "Add Reaction", "user_settings": "User Settings", From 08f8b975b68212dc2ff7616c1b3c163402327a6a Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Sat, 11 Jan 2025 18:01:53 +0200 Subject: [PATCH 03/48] use computed instead of methods when possible --- .../status_action_buttons.js | 33 ++++++++++--------- .../status_action_buttons.vue | 18 +++++----- 2 files changed, 27 insertions(+), 24 deletions(-) diff --git a/src/components/status_action_buttons/status_action_buttons.js b/src/components/status_action_buttons/status_action_buttons.js index 2a7c70a41..52e3b3f8b 100644 --- a/src/components/status_action_buttons/status_action_buttons.js +++ b/src/components/status_action_buttons/status_action_buttons.js @@ -233,14 +233,12 @@ const BUTTONS = [{ Object.entries(button).map(([k, v]) => [k, typeof v === 'function' ? v : () => v]) ) }) -console.log(BUTTONS) const StatusActionButtons = { props: ['status', 'replying'], emits: ['toggleReplying'], data () { return { - buttons: BUTTONS, showingConfirmDialog: false, currentConfirmTitle: '', currentConfirmOkText: '', @@ -251,6 +249,24 @@ const StatusActionButtons = { components: { ConfirmModal }, + computed: { + buttons () { + return BUTTONS.filter(x => x.if(this.funcArg)) + }, + 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 + } + } + }, methods: { doAction (button) { this.doActionReal(button) @@ -269,19 +285,6 @@ const StatusActionButtons = { return 'button' } }, - 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 - } - }, getClass (button) { return { [button.name() + '-button']: true, diff --git a/src/components/status_action_buttons/status_action_buttons.vue b/src/components/status_action_buttons/status_action_buttons.vue index 3950e22db..b779ea46b 100644 --- a/src/components/status_action_buttons/status_action_buttons.vue +++ b/src/components/status_action_buttons/status_action_buttons.vue @@ -4,7 +4,7 @@ -