feat(schedule): action to post scheduled messages immediately

Signed-off-by: Maksim Sukharev <antreesy.web@gmail.com>
This commit is contained in:
Maksim Sukharev 2025-12-10 19:15:29 +01:00
commit aadada5948
3 changed files with 76 additions and 8 deletions

View file

@ -5,9 +5,11 @@
<script setup lang="ts"> <script setup lang="ts">
import type { BigIntChatMessage } from '../../../../../types/index.ts' import type { BigIntChatMessage } from '../../../../../types/index.ts'
import type { RawTemporaryMessagePayload } from '../../../../../utils/prepareTemporaryMessage.ts'
import { t } from '@nextcloud/l10n' import { t } from '@nextcloud/l10n'
import { computed, inject, ref } from 'vue' import { computed, inject, ref } from 'vue'
import { useStore } from 'vuex'
import NcActionButton from '@nextcloud/vue/components/NcActionButton' import NcActionButton from '@nextcloud/vue/components/NcActionButton'
import NcActionInput from '@nextcloud/vue/components/NcActionInput' import NcActionInput from '@nextcloud/vue/components/NcActionInput'
import NcActions from '@nextcloud/vue/components/NcActions' import NcActions from '@nextcloud/vue/components/NcActions'
@ -20,8 +22,11 @@ import IconCalendarClockOutline from 'vue-material-design-icons/CalendarClockOut
import IconCheck from 'vue-material-design-icons/Check.vue' import IconCheck from 'vue-material-design-icons/Check.vue'
import IconDotsHorizontal from 'vue-material-design-icons/DotsHorizontal.vue' import IconDotsHorizontal from 'vue-material-design-icons/DotsHorizontal.vue'
import IconPencilOutline from 'vue-material-design-icons/PencilOutline.vue' import IconPencilOutline from 'vue-material-design-icons/PencilOutline.vue'
import IconSend from 'vue-material-design-icons/Send.vue'
import IconSendVariantClock from 'vue-material-design-icons/SendVariantClock.vue' import IconSendVariantClock from 'vue-material-design-icons/SendVariantClock.vue'
import IconTrashCanOutline from 'vue-material-design-icons/TrashCanOutline.vue' import IconTrashCanOutline from 'vue-material-design-icons/TrashCanOutline.vue'
import { useTemporaryMessage } from '../../../../../composables/useTemporaryMessage.ts'
import { EventBus } from '../../../../../services/EventBus.ts'
import { useChatExtrasStore } from '../../../../../stores/chatExtras.ts' import { useChatExtrasStore } from '../../../../../stores/chatExtras.ts'
import { convertToUnix, formatDateTime } from '../../../../../utils/formattedTime.ts' import { convertToUnix, formatDateTime } from '../../../../../utils/formattedTime.ts'
import { getCustomDateOptions } from '../../../../../utils/getCustomDateOptions.ts' import { getCustomDateOptions } from '../../../../../utils/getCustomDateOptions.ts'
@ -39,6 +44,9 @@ const emit = defineEmits<{
const getMessagesListScroller = inject('getMessagesListScroller', () => undefined) const getMessagesListScroller = inject('getMessagesListScroller', () => undefined)
const chatExtrasStore = useChatExtrasStore() const chatExtrasStore = useChatExtrasStore()
const vuexStore = useStore()
const { createTemporaryMessage } = useTemporaryMessage()
const submenu = ref<'schedule' | null>(null) const submenu = ref<'schedule' | null>(null)
const customScheduleTimestamp = ref(new Date(new Date().setHours(new Date().getHours() + 1, 0, 0, 0))) const customScheduleTimestamp = ref(new Date(new Date().setHours(new Date().getHours() + 1, 0, 0, 0)))
@ -73,6 +81,40 @@ async function handleDelete() {
await chatExtrasStore.deleteScheduledMessage(props.message.token, props.message.id) await chatExtrasStore.deleteScheduledMessage(props.message.token, props.message.id)
} }
/**
* Delete the scheduled message
*/
async function handleSubmit() {
const temporaryMessagePayload: RawTemporaryMessagePayload = {
message: props.message.message,
token: props.message.token,
silent: props.message.silent,
}
if ((props.message.threadId ?? 0) > 0) {
temporaryMessagePayload.threadId = props.message.threadId
temporaryMessagePayload.isThread = true
}
if (props.message.parent?.id && !props.message.parent.deleted) {
temporaryMessagePayload.parent = props.message.parent
}
if (props.message.threadId === -1) {
// Substitute thread title with message text, if missing
temporaryMessagePayload.threadTitle = props.message.threadTitle
temporaryMessagePayload.threadReplies = 0
temporaryMessagePayload.isThread = true
}
const temporaryMessage = createTemporaryMessage(temporaryMessagePayload)
// FIXME: quite scheduled messages view
// FIXME: Scroll to bottom after sending the scheduled message
EventBus.emit('scroll-chat-to-bottom', { smooth: true, force: true })
await vuexStore.dispatch('postNewMessage', { token: props.message.token, temporaryMessage })
await chatExtrasStore.deleteScheduledMessage(props.message.token, props.message.id)
}
/** /**
* Toggle action menu open state * Toggle action menu open state
*/ */
@ -130,7 +172,6 @@ function onMenuClose() {
<NcActionButton <NcActionButton
key="edit-message" key="edit-message"
:aria-label="t('spreed', 'Edit message')"
close-after-click close-after-click
@click.stop="handleEdit"> @click.stop="handleEdit">
<template #icon> <template #icon>
@ -147,6 +188,18 @@ function onMenuClose() {
</template> </template>
{{ t('spreed', 'Delete') }} {{ t('spreed', 'Delete') }}
</NcActionButton> </NcActionButton>
<NcActionSeparator />
<NcActionButton
key="send-message"
close-after-click
@click.stop="handleSubmit">
<template #icon>
<IconSend :size="20" />
</template>
{{ t('spreed', 'Send message now') }}
</NcActionButton>
</template> </template>
<template v-else-if="submenu === 'schedule'"> <template v-else-if="submenu === 'schedule'">

View file

@ -3,7 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-or-later * SPDX-License-Identifier: AGPL-3.0-or-later
*/ */
import type { PrepareTemporaryMessagePayload } from '../utils/prepareTemporaryMessage.ts' import type { RawTemporaryMessagePayload } from '../utils/prepareTemporaryMessage.ts'
import { useActorStore } from '../stores/actor.ts' import { useActorStore } from '../stores/actor.ts'
import { prepareTemporaryMessage } from '../utils/prepareTemporaryMessage.ts' import { prepareTemporaryMessage } from '../utils/prepareTemporaryMessage.ts'
@ -17,7 +17,7 @@ export function useTemporaryMessage() {
/** /**
* @param payload payload for generating a temporary message * @param payload payload for generating a temporary message
*/ */
function createTemporaryMessage(payload: PrepareTemporaryMessagePayload) { function createTemporaryMessage(payload: RawTemporaryMessagePayload) {
return prepareTemporaryMessage({ return prepareTemporaryMessage({
...payload, ...payload,
actorId: actorStore.actorId ?? '', actorId: actorStore.actorId ?? '',

View file

@ -10,6 +10,21 @@ import SHA256 from 'crypto-js/sha256.js'
import { MESSAGE } from '../constants.ts' import { MESSAGE } from '../constants.ts'
import { hasTalkFeature } from '../services/CapabilitiesManager.ts' import { hasTalkFeature } from '../services/CapabilitiesManager.ts'
export type RawTemporaryMessagePayload = Pick<ChatMessage,
| 'message'
| 'token'
| 'silent'
> & Partial<Pick<ChatMessage,
| 'actorId'
| 'actorType'
| 'actorDisplayName'
| 'threadId'
| 'isThread'
| 'threadTitle'
| 'threadReplies'
| 'parent'
>>
export type PrepareTemporaryMessagePayload = Pick<ChatMessage, export type PrepareTemporaryMessagePayload = Pick<ChatMessage,
| 'message' | 'message'
| 'token' | 'token'
@ -21,13 +36,13 @@ export type PrepareTemporaryMessagePayload = Pick<ChatMessage,
| 'isThread' | 'isThread'
| 'threadTitle' | 'threadTitle'
| 'threadReplies' | 'threadReplies'
| 'parent'
> & { > & {
uploadId: string uploadId?: string
index: number index?: number
file: File & { newName?: string } file?: File & { newName?: string }
localUrl: string localUrl?: string
messageType?: typeof MESSAGE.TYPE['VOICE_MESSAGE' | 'COMMENT'] messageType?: typeof MESSAGE.TYPE['VOICE_MESSAGE' | 'COMMENT']
parent: Omit<ChatMessage, 'parent'>
} }
/** /**