mirror of
https://github.com/nextcloud/spreed.git
synced 2025-12-18 05:20:50 +01:00
feat(schedule): action to edit scheduled messages
Signed-off-by: Maksim Sukharev <antreesy.web@gmail.com>
This commit is contained in:
parent
80e10164a4
commit
91f3fbb1d6
4 changed files with 228 additions and 13 deletions
|
|
@ -0,0 +1,181 @@
|
|||
<!--
|
||||
- SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
|
||||
- SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
-->
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { BigIntChatMessage } from '../../../../../types/index.ts'
|
||||
|
||||
import { t } from '@nextcloud/l10n'
|
||||
import { computed, inject, ref } from 'vue'
|
||||
import NcActionButton from '@nextcloud/vue/components/NcActionButton'
|
||||
import NcActionInput from '@nextcloud/vue/components/NcActionInput'
|
||||
import NcActions from '@nextcloud/vue/components/NcActions'
|
||||
import NcActionSeparator from '@nextcloud/vue/components/NcActionSeparator'
|
||||
import NcActionText from '@nextcloud/vue/components/NcActionText'
|
||||
import NcButton from '@nextcloud/vue/components/NcButton'
|
||||
import IconAlarm from 'vue-material-design-icons/Alarm.vue'
|
||||
import IconArrowLeft from 'vue-material-design-icons/ArrowLeft.vue'
|
||||
import IconCalendarClockOutline from 'vue-material-design-icons/CalendarClockOutline.vue'
|
||||
import IconCheck from 'vue-material-design-icons/Check.vue'
|
||||
import IconDotsHorizontal from 'vue-material-design-icons/DotsHorizontal.vue'
|
||||
import IconPencilOutline from 'vue-material-design-icons/PencilOutline.vue'
|
||||
import IconSendVariantClock from 'vue-material-design-icons/SendVariantClock.vue'
|
||||
import { useChatExtrasStore } from '../../../../../stores/chatExtras.ts'
|
||||
import { convertToUnix, formatDateTime } from '../../../../../utils/formattedTime.ts'
|
||||
import { getCustomDateOptions } from '../../../../../utils/getCustomDateOptions.ts'
|
||||
|
||||
const props = defineProps<{
|
||||
message: BigIntChatMessage
|
||||
isActionMenuOpen: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(event: 'update:isActionMenuOpen', value: boolean): void
|
||||
(event: 'edit'): void
|
||||
}>()
|
||||
|
||||
const getMessagesListScroller = inject('getMessagesListScroller', () => undefined)
|
||||
|
||||
const chatExtrasStore = useChatExtrasStore()
|
||||
|
||||
const submenu = ref<'schedule' | null>(null)
|
||||
const customScheduleTimestamp = ref(new Date(new Date().setHours(new Date().getHours() + 1, 0, 0, 0)))
|
||||
|
||||
const messageDateTime = computed(() => {
|
||||
return formatDateTime(props.message.timestamp * 1000, 'shortDateWithTime')
|
||||
})
|
||||
|
||||
/**
|
||||
* Edit the scheduled message (trigger editing mode)
|
||||
*/
|
||||
async function handleEdit() {
|
||||
emit('edit')
|
||||
}
|
||||
|
||||
/**
|
||||
* Edit the scheduled message (sendAt only)
|
||||
*
|
||||
* @param timestamp new scheduled timestamp (in ms)
|
||||
*/
|
||||
async function handleReschedule(timestamp: number) {
|
||||
await chatExtrasStore.editScheduledMessage(props.message.token, props.message.id, {
|
||||
message: props.message.message,
|
||||
sendAt: convertToUnix(timestamp),
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle action menu open state
|
||||
*/
|
||||
function onMenuOpen() {
|
||||
emit('update:isActionMenuOpen', true)
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle action menu open state
|
||||
*/
|
||||
function onMenuClose() {
|
||||
emit('update:isActionMenuOpen', false)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<NcButton
|
||||
v-if="!isActionMenuOpen"
|
||||
variant="tertiary"
|
||||
:aria-label="t('spreed', 'More actions')"
|
||||
:title="t('spreed', 'More actions')"
|
||||
@click="onMenuOpen">
|
||||
<template #icon>
|
||||
<IconDotsHorizontal :size="20" />
|
||||
</template>
|
||||
</NcButton>
|
||||
<NcActions
|
||||
v-else
|
||||
force-menu
|
||||
open
|
||||
placement="bottom-end"
|
||||
:boundaries-element="getMessagesListScroller()"
|
||||
@close="onMenuClose">
|
||||
<template v-if="submenu === null">
|
||||
<!-- Message timestamp -->
|
||||
<NcActionText>
|
||||
<template #icon>
|
||||
<IconSendVariantClock :size="20" />
|
||||
</template>
|
||||
{{ messageDateTime }}
|
||||
</NcActionText>
|
||||
|
||||
<NcActionButton
|
||||
key="set-schedule-menu"
|
||||
is-menu
|
||||
@click.stop="submenu = 'schedule'">
|
||||
<template #icon>
|
||||
<IconAlarm :size="20" />
|
||||
</template>
|
||||
{{ t('spreed', 'Reschedule') }}
|
||||
</NcActionButton>
|
||||
|
||||
<NcActionSeparator />
|
||||
|
||||
<NcActionButton
|
||||
key="edit-message"
|
||||
:aria-label="t('spreed', 'Edit message')"
|
||||
close-after-click
|
||||
@click.stop="handleEdit">
|
||||
<template #icon>
|
||||
<IconPencilOutline :size="20" />
|
||||
</template>
|
||||
{{ t('spreed', 'Edit message') }}
|
||||
</NcActionButton>
|
||||
</template>
|
||||
|
||||
<template v-else-if="submenu === 'schedule'">
|
||||
<NcActionButton
|
||||
key="action-back"
|
||||
:aria-label="t('spreed', 'Back')"
|
||||
@click.stop="submenu = null">
|
||||
<template #icon>
|
||||
<IconArrowLeft class="bidirectional-icon" />
|
||||
</template>
|
||||
{{ t('spreed', 'Back') }}
|
||||
</NcActionButton>
|
||||
|
||||
<NcActionSeparator />
|
||||
|
||||
<NcActionButton
|
||||
v-for="option in getCustomDateOptions()"
|
||||
:key="option.key"
|
||||
:aria-label="option.ariaLabel"
|
||||
close-after-click
|
||||
@click.stop="handleReschedule(option.timestamp)">
|
||||
{{ option.label }}
|
||||
</NcActionButton>
|
||||
|
||||
<NcActionInput
|
||||
v-model="customScheduleTimestamp"
|
||||
type="datetime-local"
|
||||
:min="new Date()"
|
||||
:step="300"
|
||||
is-native-picker>
|
||||
<template #icon>
|
||||
<IconCalendarClockOutline :size="20" />
|
||||
</template>
|
||||
</NcActionInput>
|
||||
|
||||
<NcActionButton
|
||||
key="custom-time-submit"
|
||||
:disabled="!customScheduleTimestamp"
|
||||
close-after-click
|
||||
@click.stop="handleReschedule(customScheduleTimestamp.valueOf())">
|
||||
<template #icon>
|
||||
<IconCheck :size="20" />
|
||||
</template>
|
||||
{{ t('spreed', 'Send on custom time') }}
|
||||
</NcActionButton>
|
||||
</template>
|
||||
</NcActions>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -50,9 +50,12 @@
|
|||
'bottom-side': isSplitViewEnabled && !isShortSimpleMessage && (isSmallMobile || isSidebar),
|
||||
overlay: isSplitViewEnabled && !isShortSimpleMessage && isReactionsMenuOpen && !(isSmallMobile || isSidebar),
|
||||
}">
|
||||
<div
|
||||
<ScheduledMessageActions
|
||||
v-if="showScheduledMessages && showMessageButtonsBar"
|
||||
class="message-buttons-bar" />
|
||||
v-model:is-action-menu-open="isActionMenuOpen"
|
||||
:message="message"
|
||||
class="message-buttons-bar"
|
||||
@edit="handleEdit" />
|
||||
<MessageButtonsBar
|
||||
v-else-if="showMessageButtonsBar"
|
||||
v-model:is-action-menu-open="isActionMenuOpen"
|
||||
|
|
@ -94,6 +97,7 @@ import { inject } from 'vue'
|
|||
import MessageButtonsBar from './MessageButtonsBar/MessageButtonsBar.vue'
|
||||
import MessageForwarder from './MessageButtonsBar/MessageForwarder.vue'
|
||||
import MessageTranslateDialog from './MessageButtonsBar/MessageTranslateDialog.vue'
|
||||
import ScheduledMessageActions from './MessageButtonsBar/ScheduledMessageActions.vue'
|
||||
import ContactCard from './MessagePart/ContactCard.vue'
|
||||
import DeckCard from './MessagePart/DeckCard.vue'
|
||||
import DefaultParameter from './MessagePart/DefaultParameter.vue'
|
||||
|
|
@ -120,6 +124,7 @@ export default {
|
|||
MessageForwarder,
|
||||
MessageTranslateDialog,
|
||||
ReactionsWrapper,
|
||||
ScheduledMessageActions,
|
||||
},
|
||||
|
||||
props: {
|
||||
|
|
|
|||
|
|
@ -547,7 +547,12 @@ export default {
|
|||
|
||||
messageToEdit() {
|
||||
const messageToEditId = this.chatExtrasStore.getMessageIdToEdit(this.token)
|
||||
return messageToEditId && this.$store.getters.message(this.token, messageToEditId)
|
||||
if (!messageToEditId) {
|
||||
return undefined
|
||||
}
|
||||
return (this.showScheduledMessages)
|
||||
? this.chatExtrasStore.getScheduledMessage(this.token, messageToEditId)
|
||||
: this.$store.getters.message(this.token, messageToEditId)
|
||||
},
|
||||
|
||||
canShareFiles() {
|
||||
|
|
@ -721,7 +726,12 @@ export default {
|
|||
messageToEdit(newValue) {
|
||||
if (newValue) {
|
||||
this.text = this.chatExtrasStore.getChatEditInput(this.token)
|
||||
this.chatExtrasStore.removeThreadTitle(this.token)
|
||||
|
||||
// Clear thread title when editing a message (unless it's a scheduled thread)
|
||||
if (newValue.threadId !== -1) {
|
||||
this.chatExtrasStore.removeThreadTitle(this.token)
|
||||
}
|
||||
|
||||
if (this.parentMessage) {
|
||||
this.chatExtrasStore.removeParentIdToReply(this.token)
|
||||
}
|
||||
|
|
@ -992,12 +1002,21 @@ export default {
|
|||
|
||||
async handleEdit() {
|
||||
try {
|
||||
await this.$store.dispatch('editMessage', {
|
||||
token: this.token,
|
||||
messageId: this.messageToEdit.id,
|
||||
updatedMessage: parseSpecialSymbols(this.text.trim()),
|
||||
})
|
||||
if (this.showScheduledMessages) {
|
||||
await this.chatExtrasStore.editScheduledMessage(this.token, this.messageToEdit.id, {
|
||||
message: parseSpecialSymbols(this.text.trim()),
|
||||
sendAt: this.messageToEdit.timestamp,
|
||||
threadTitle: this.threadTitle,
|
||||
})
|
||||
} else {
|
||||
await this.$store.dispatch('editMessage', {
|
||||
token: this.token,
|
||||
messageId: this.messageToEdit.id,
|
||||
updatedMessage: parseSpecialSymbols(this.text.trim()),
|
||||
})
|
||||
}
|
||||
this.chatExtrasStore.removeMessageIdToEdit(this.token)
|
||||
this.chatExtrasStore.removeThreadTitle(this.token)
|
||||
this.resetTypingIndicator()
|
||||
// refocus input as the user might want to type further
|
||||
this.focusInput()
|
||||
|
|
|
|||
|
|
@ -38,6 +38,13 @@ import {
|
|||
import { parseMentions, parseSpecialSymbols } from '../utils/textParse.ts'
|
||||
import { useActorStore } from './actor.ts'
|
||||
|
||||
type InitiateEditingMessagePayload = {
|
||||
token: string
|
||||
id: number | string
|
||||
message: string
|
||||
messageParameters: ChatMessage['messageParameters']
|
||||
}
|
||||
|
||||
const FOLLOWED_THREADS_FETCH_LIMIT = 100
|
||||
const pendingFetchSingleThreadRequests = new Set<number>()
|
||||
|
||||
|
|
@ -52,7 +59,7 @@ export const useChatExtrasStore = defineStore('chatExtras', () => {
|
|||
const threadTitle = ref<Record<string, string>>({})
|
||||
const parentToReply = ref<Record<string, number>>({})
|
||||
const chatInput = ref<Record<string, string>>({})
|
||||
const messageIdToEdit = ref<Record<string, number>>({})
|
||||
const messageIdToEdit = ref<Record<string, number | string>>({})
|
||||
const chatEditInput = ref<Record<string, string>>({})
|
||||
const tasksCount = ref(0)
|
||||
const tasksDoneCount = ref(0)
|
||||
|
|
@ -134,7 +141,7 @@ export const useChatExtrasStore = defineStore('chatExtras', () => {
|
|||
*
|
||||
* @param token - conversation token
|
||||
*/
|
||||
function getMessageIdToEdit(token: string) {
|
||||
function getMessageIdToEdit(token: string): number | string | undefined {
|
||||
return messageIdToEdit.value[token]
|
||||
}
|
||||
|
||||
|
|
@ -521,7 +528,7 @@ export const useChatExtrasStore = defineStore('chatExtras', () => {
|
|||
* @param token - conversation token
|
||||
* @param id The id of message
|
||||
*/
|
||||
function setMessageIdToEdit(token: string, id: number) {
|
||||
function setMessageIdToEdit(token: string, id: number | string) {
|
||||
messageIdToEdit.value[token] = id
|
||||
}
|
||||
|
||||
|
|
@ -554,7 +561,7 @@ export const useChatExtrasStore = defineStore('chatExtras', () => {
|
|||
* @param payload.message - message text
|
||||
* @param payload.messageParameters - message parameters
|
||||
*/
|
||||
function initiateEditingMessage({ token, id, message, messageParameters }: { token: string, id: number, message: string, messageParameters: ChatMessage['messageParameters'] }) {
|
||||
function initiateEditingMessage({ token, id, message, messageParameters }: InitiateEditingMessagePayload) {
|
||||
setMessageIdToEdit(token, id)
|
||||
const isFileShareOnly = Object.keys(messageParameters ?? {}).some((key) => key.startsWith('file'))
|
||||
&& message === '{file}'
|
||||
|
|
@ -567,6 +574,9 @@ export const useChatExtrasStore = defineStore('chatExtras', () => {
|
|||
parameters: messageParameters,
|
||||
})
|
||||
}
|
||||
if (scheduledMessages.value[token]?.[id] && scheduledMessages.value[token][id].threadId === -1) {
|
||||
setThreadTitle(token, scheduledMessages.value[token][id].threadTitle!)
|
||||
}
|
||||
EventBus.emit('editing-message')
|
||||
EventBus.emit('focus-chat-input')
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue