Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
feat(schedule): action to edit scheduled messages
Signed-off-by: Maksim Sukharev <[email protected]>
  • Loading branch information
Antreesy committed Dec 22, 2025
commit 9d46a36cdededaa0bfbcfff8f46200897b0a438f
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
<!--
- 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 IconSendVariantClockOutline from 'vue-material-design-icons/SendVariantClockOutline.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(props.message.timestamp * 1000))

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),
})
EventBus.emit('focus-message', { messageId: props.message.id })
}

/**
* 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>
<IconSendVariantClockOutline :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') }}
</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()"
:label="t('spreed', 'Choose a time')"
: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 at custom time') }}
</NcActionButton>
</template>
</NcActions>
</div>
</template>
Original file line number Diff line number Diff line change
Expand Up @@ -52,10 +52,13 @@
overlay: isSplitViewEnabled && !isShortSimpleMessage && isReactionsMenuOpen && !(isSmallMobile || isSidebar),
}">
<template v-if="showMessageButtonsBar">
<div
<ScheduledMessageActions
v-if="showScheduledMessages"
v-model:is-action-menu-open="isActionMenuOpen"
:message="message"
class="message-buttons-bar"
:class="{ outlined: buttonsBarOutlined }" />
:class="{ outlined: buttonsBarOutlined }"
@edit="handleEdit" />
<MessageButtonsBar
v-else
v-model:is-action-menu-open="isActionMenuOpen"
Expand Down Expand Up @@ -106,6 +109,7 @@ import IconPin from 'vue-material-design-icons/PinOutline.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'
Expand All @@ -132,6 +136,7 @@ export default {
MessageForwarder,
MessageTranslateDialog,
ReactionsWrapper,
ScheduledMessageActions,
IconPin,
},

Expand Down
65 changes: 55 additions & 10 deletions src/components/NewMessage/NewMessage.vue
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@
size="small"
:aria-label="t('spreed', 'Cancel')"
:title="t('spreed', 'Cancel')"
@click="scheduleMessageTime = null">
@click="handleAbortEdit">
<template #icon>
<IconClose :size="16" />
</template>
Expand Down Expand Up @@ -520,7 +520,18 @@ export default {
},

disabledEdit() {
return this.disabled || this.text === this.messageToEdit.message || this.text === ''
if (this.disabled || this.text === '') {
return true
}

if (!this.showScheduledMessages) {
return this.text === this.messageToEdit.message
}

return this.text === this.messageToEdit.message
&& this.scheduleMessageTime === this.messageToEdit.timestamp * 1_000
&& this.silentChat === this.messageToEdit.silent
&& (!this.threadCreating || this.threadTitle === this.messageToEdit.threadTitle)
},

placeholderText() {
Expand Down Expand Up @@ -552,7 +563,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() {
Expand Down Expand Up @@ -606,7 +622,7 @@ export default {
},

showSendActions() {
return !this.broadcast && !this.isRecordingAudio && !this.messageToEdit
return !this.broadcast && !this.isRecordingAudio && (!this.messageToEdit || this.showScheduledMessages)
},

showAttachmentsMenu() {
Expand Down Expand Up @@ -733,7 +749,17 @@ 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.showScheduledMessages) {
this.chatExtrasStore.setScheduleMessageTime(newValue.timestamp * 1_000)
this.silentChat = newValue.silent
}

if (this.parentMessage) {
this.chatExtrasStore.removeParentIdToReply(this.token)
}
Expand All @@ -747,6 +773,10 @@ export default {
})
},

showScheduledMessages() {
this.handleAbortEdit()
},

parentMessage(newValue) {
if (newValue) {
this.chatExtrasStore.removeThreadTitle(this.token)
Expand Down Expand Up @@ -1014,13 +1044,25 @@ 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: convertToUnix(this.scheduleMessageTime),
silent: this.silentChat,
threadTitle: this.threadTitle,
})
} else {
await this.$store.dispatch('editMessage', {
token: this.token,
messageId: this.messageToEdit.id,
updatedMessage: parseSpecialSymbols(this.text.trim()),
})
}
EventBus.emit('focus-message', { messageId: this.messageToEdit.id })
this.chatExtrasStore.removeMessageIdToEdit(this.token)
this.chatExtrasStore.removeThreadTitle(this.token)
this.chatExtrasStore.setScheduleMessageTime(null)
this.silentChat = false
this.resetTypingIndicator()
// refocus input as the user might want to type further
this.focusInput()
Expand Down Expand Up @@ -1063,6 +1105,7 @@ export default {
})
} else {
this.chatExtrasStore.removeThreadTitle(this.token)
this.chatExtrasStore.removeMessageIdToEdit(this.token)
this.chatExtrasStore.setScheduleMessageTime(null)
}
},
Expand Down Expand Up @@ -1257,7 +1300,9 @@ export default {

handleAbortEdit() {
this.chatExtrasStore.removeMessageIdToEdit(this.token)
this.chatExtrasStore.removeThreadTitle(this.token)
this.chatExtrasStore.setScheduleMessageTime(null)
this.silentChat = false
},

toggleSilentChat() {
Expand Down
2 changes: 1 addition & 1 deletion src/services/EventBus.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ export type Events = {
'editing-message': void
'editing-message-processing': { messageId: number, value: boolean }
'focus-chat-input': void
'focus-message': { messageId: number, smooth?: boolean, highlight?: boolean }
'focus-message': { messageId: number | string, smooth?: boolean, highlight?: boolean }
'forbidden-route': { error: string }
'joined-conversation': { token: string }
'poll-drafts-open': { token: string, selector?: string }
Expand Down
Loading