feat(schedule): implement store and services

Signed-off-by: Maksim Sukharev <antreesy.web@gmail.com>
This commit is contained in:
Maksim Sukharev 2025-12-02 13:58:52 +01:00
commit 7b5931a386
7 changed files with 305 additions and 1 deletions

View file

@ -30,10 +30,19 @@ import storeConfig from '../../../../store/storeConfig.js'
import { useActorStore } from '../../../../stores/actor.ts'
import { useTokenStore } from '../../../../stores/token.ts'
let store
vi.mock('vuex', async () => {
const vuex = await vi.importActual('vuex')
return {
...vuex,
useStore: vi.fn(() => store),
}
})
describe('MessageItem.vue', () => {
const TOKEN = 'XXTOKENXX'
let testStoreConfig
let store
let messageProps
let conversationProps
let injected

View file

@ -25,6 +25,14 @@ import { useActorStore } from '../../../../../stores/actor.ts'
import { useReactionsStore } from '../../../../../stores/reactions.js'
import { generateOCSResponse } from '../../../../../test-helpers.js'
vi.mock('vuex', async () => {
const vuex = await vi.importActual('vuex')
return {
...vuex,
useStore: vi.fn(),
}
})
vi.mock('../../../../../services/reactionsService', () => ({
getReactionsDetails: vi.fn(),
addReactionToMessage: vi.fn(),

View file

@ -7,12 +7,16 @@ import type { AxiosRequestConfig } from '@nextcloud/axios'
import type {
clearHistoryResponse,
deleteMessageResponse,
deleteScheduledMessageResponse,
editMessageParams,
editMessageResponse,
editScheduledMessageParams,
editScheduledMessageResponse,
getMessageContextParams,
getMessageContextResponse,
getRecentThreadsParams,
getRecentThreadsResponse,
getScheduledMessagesResponse,
getSubscribedThreadsParams,
getSubscribedThreadsResponse,
getThreadResponse,
@ -25,6 +29,8 @@ import type {
receiveMessagesResponse,
renameThreadParams,
renameThreadResponse,
scheduleMessageParams,
scheduleMessageResponse,
setReadMarkerParams,
setReadMarkerResponse,
setThreadNotificationLevelParams,
@ -340,19 +346,104 @@ async function renameThread(token: string, threadId: number, threadTitle: string
} as renameThreadParams, options)
}
/**
* Get a list of scheduled messages of this user for given conversation
*
* @param token the conversation token
* @param [options] Axios request options
*/
async function getScheduledMessages(token: string, options?: AxiosRequestConfig): getScheduledMessagesResponse {
return axios.get(generateOcsUrl('apps/spreed/api/v1/chat/{token}/schedule', { token }), options)
}
/**
* Schedules a new message to be poster
*
* @param payload The request payload
* @param payload.token The conversation token
* @param payload.message The message text
* @param payload.sendAt The timestamp of when message should be posted
* @param payload.replyTo The message id to be replied to
* @param payload.silent whether the message should trigger a notifications
* @param payload.threadId The thread id to post the message in
* @param payload.threadTitle The thread title to set when creating a new thread
* @param [options] Axios request options
*/
async function scheduleMessage({
token,
message,
sendAt,
replyTo,
silent,
threadId,
threadTitle,
}: scheduleMessageParams & { token: string }, options?: AxiosRequestConfig): scheduleMessageResponse {
return axios.post(generateOcsUrl('apps/spreed/api/v1/chat/{token}/schedule', { token }), {
message,
sendAt,
replyTo,
silent,
threadId,
threadTitle,
} as scheduleMessageParams, options)
}
/**
* Edit an already scheduled message
*
* @param payload The request payload
* @param payload.token The conversation token
* @param payload.messageId The id of scheduled message
* @param payload.message The message text
* @param payload.sendAt The timestamp of when message should be posted
* @param payload.silent whether the message should trigger a notifications
* @param payload.threadTitle The thread title to set when creating a new thread
* @param [options] Axios request options
*/
async function editScheduledMessage({
token,
messageId,
message,
sendAt,
silent,
threadTitle,
}: editScheduledMessageParams & { token: string, messageId: string }, options?: AxiosRequestConfig): editScheduledMessageResponse {
return axios.post(generateOcsUrl('apps/spreed/api/v1/chat/{token}/schedule/{messageId}', { token, messageId }), {
message,
sendAt,
silent,
threadTitle,
} as editScheduledMessageParams, options)
}
/**
* Delete a scheduled message from the queue
*
* @param token The conversation token
* @param messageId The id of scheduled message
* @param [options] Axios request options
*/
async function deleteScheduledMessage(token: string, messageId: string, options?: AxiosRequestConfig): deleteScheduledMessageResponse {
return axios.delete(generateOcsUrl('apps/spreed/api/v1/chat/{token}/schedule/{messageId}', { token, messageId }), options)
}
export {
clearConversationHistory,
deleteMessage,
deleteScheduledMessage,
editMessage,
editScheduledMessage,
fetchMessages,
getMessageContext,
getRecentThreadsForConversation,
getScheduledMessages,
getSingleThreadForConversation,
getSubscribedThreads,
pollNewMessages,
postNewMessage,
postRichObjectToConversation,
renameThread,
scheduleMessage,
setConversationUnread,
setThreadNotificationLevel,
summarizeChat,

View file

@ -62,6 +62,14 @@ vi.mock('../services/conversationsService', () => ({
setCallPermissions: vi.fn(),
}))
vi.mock('vuex', async () => {
const vuex = await vi.importActual('vuex')
return {
...vuex,
useStore: vi.fn(),
}
})
vi.mock('../services/messagesService', () => ({
updateLastReadMessage: vi.fn(),
setConversationUnread: vi.fn(),

View file

@ -9,6 +9,14 @@ import BrowserStorage from '../../services/BrowserStorage.js'
import { EventBus } from '../../services/EventBus.ts'
import { useChatExtrasStore } from '../chatExtras.ts'
vi.mock('vuex', async () => {
const vuex = await vi.importActual('vuex')
return {
...vuex,
useStore: vi.fn(),
}
})
describe('chatExtrasStore', () => {
const token = 'TOKEN'
let chatExtrasStore

View file

@ -4,8 +4,12 @@
*/
import type {
BigIntChatMessage,
ChatMessage,
ChatTask,
editScheduledMessageParams,
ScheduledMessage,
scheduleMessageParams,
ThreadInfo,
} from '../types/index.ts'
@ -14,19 +18,25 @@ import { t } from '@nextcloud/l10n'
import { spawnDialog } from '@nextcloud/vue/functions/dialog'
import { defineStore } from 'pinia'
import { computed, ref } from 'vue'
import { useStore } from 'vuex'
import ConfirmDialog from '../components/UIShared/ConfirmDialog.vue'
import { PARTICIPANT } from '../constants.ts'
import BrowserStorage from '../services/BrowserStorage.js'
import { EventBus } from '../services/EventBus.ts'
import {
deleteScheduledMessage as deleteScheduledMessageApi,
editScheduledMessage as editScheduledMessageApi,
getRecentThreadsForConversation,
getScheduledMessages as getScheduledMessagesApi,
getSingleThreadForConversation,
getSubscribedThreads,
renameThread as renameThreadApi,
scheduleMessage as scheduleMessageApi,
setThreadNotificationLevel as setThreadNotificationLevelApi,
summarizeChat,
} from '../services/messagesService.ts'
import { parseMentions, parseSpecialSymbols } from '../utils/textParse.ts'
import { useActorStore } from './actor.ts'
const FOLLOWED_THREADS_FETCH_LIMIT = 100
const pendingFetchSingleThreadRequests = new Set<number>()
@ -47,6 +57,10 @@ export const useChatExtrasStore = defineStore('chatExtras', () => {
const tasksCount = ref(0)
const tasksDoneCount = ref(0)
const chatSummary = ref<Record<string, Record<number, ChatTask>>>({})
const scheduledMessages = ref<Record<string, Record<string, ScheduledMessage>>>({})
const actorStore = useActorStore()
const vuexStore = useStore()
/**
* Returns known thread information from the store
@ -150,6 +164,29 @@ export const useChatExtrasStore = defineStore('chatExtras', () => {
|| t('spreed', 'Error occurred during a summary generation')
}
/**
* Returns list of scheduled messages (sorted by sendAt, prepared for chat)
*
* @param token - conversation token
*/
function getScheduledMessagesList(token: string) {
return Object.values(scheduledMessages.value[token] ?? {})
.sort((a, b) => a.sendAt - b.sendAt)
.map((message) => parseScheduledToChatMessage(token, message))
}
/**
* Returns scheduled messages for given conversation (sorted by sendAt timestamp)
*
* @param token - conversation token
* @param messageId
*/
function getScheduledMessage(token: string, messageId: string): BigIntChatMessage | undefined {
if (scheduledMessages.value[token]?.[messageId]) {
return parseScheduledToChatMessage(token, scheduledMessages.value[token][messageId])
}
}
/**
* Add a thread to the store for given conversation
*
@ -609,6 +646,125 @@ export const useChatExtrasStore = defineStore('chatExtras', () => {
}
}
/**
* Converts ScheduledMessage to BigIntChatMessage format (to render in chat)
*
* @param token - conversation token
* @param message - scheduled message object
*/
function parseScheduledToChatMessage(token: string, message: ScheduledMessage): BigIntChatMessage {
return {
token,
id: message.id,
actorId: message.actorId,
actorType: message.actorType,
actorDisplayName: actorStore.displayName,
message: message.message,
messageType: message.messageType,
referenceId: '',
systemMessage: '',
isReplyable: false,
markdown: true,
messageParameters: {},
parent: message.parent,
reactions: {},
timestamp: message.sendAt,
expirationTimestamp: 0,
threadId: message.threadId,
threadTitle: message.threadTitle,
isThread: !!message.threadId,
silent: message.silent,
}
}
/**
* Fetch scheduled messages for given conversation
*
* @param token - conversation token
*/
async function fetchScheduledMessages(token: string) {
try {
const response = await getScheduledMessagesApi(token)
if (!scheduledMessages.value[token]) {
scheduledMessages.value[token] = {}
}
// Patch scheduled messages into ChatMessage format to be able to show in the chat
response.data.ocs.data.forEach((message) => {
scheduledMessages.value[token][message.id] = message
})
} catch (e) {
console.error('Error while fetching scheduled messages:', e)
}
}
/**
* Schedule a message to be posted with given payload
*
* @param token - conversation token
* @param payload - action payload
*/
async function scheduleMessage(token: string, payload: scheduleMessageParams) {
try {
const response = await scheduleMessageApi({ token, ...payload })
if (!scheduledMessages.value[token]) {
scheduledMessages.value[token] = {}
}
scheduledMessages.value[token][response.data.ocs.data.id] = response.data.ocs.data
await vuexStore.dispatch('setConversationProperties', {
token,
properties: {
hasScheduledMessages: true,
},
})
} catch (e) {
console.error('Error while scheduling message:', e)
}
}
/**
* Edit already scheduled message with given payload
*
* @param token - conversation token
* @param messageId - id of message to edit
* @param payload - action payload
*/
async function editScheduledMessage(token: string, messageId: string, payload: editScheduledMessageParams) {
try {
const response = await editScheduledMessageApi({ token, messageId, ...payload })
scheduledMessages.value[token][messageId] = response.data.ocs.data
} catch (e) {
console.error('Error while editing scheduled message:', e)
}
}
/**
* Delete already scheduled message
*
* @param token - conversation token
* @param messageId - id of message to delete
*/
async function deleteScheduledMessage(token: string, messageId: string) {
try {
await deleteScheduledMessageApi(token, messageId)
delete scheduledMessages.value[token][messageId]
// Check if there are any scheduled messages left
if (Object.keys(scheduledMessages.value[token] ?? {}).length === 0) {
await vuexStore.dispatch('setConversationProperties', {
token,
properties: {
hasScheduledMessages: false,
},
})
}
} catch (e) {
console.error('Error while deleting scheduled message:', e)
}
}
return {
threads,
followedThreads,
@ -622,6 +778,7 @@ export const useChatExtrasStore = defineStore('chatExtras', () => {
tasksCount,
tasksDoneCount,
chatSummary,
scheduledMessages,
followedThreadsList,
@ -634,6 +791,8 @@ export const useChatExtrasStore = defineStore('chatExtras', () => {
getChatSummaryTaskQueue,
hasChatSummaryTaskRequested,
getChatSummary,
getScheduledMessagesList,
getScheduledMessage,
addThread,
fetchSingleThread,
@ -662,5 +821,9 @@ export const useChatExtrasStore = defineStore('chatExtras', () => {
requestChatSummary,
storeChatSummary,
dismissChatSummary,
fetchScheduledMessages,
scheduleMessage,
editScheduledMessage,
deleteScheduledMessage,
}
})

View file

@ -39,6 +39,11 @@ export type TokenMap<T> = Record<string, T>
export type IdMap<T> = Record<number | string, T>
export type TokenIdMap<T> = TokenMap<IdMap<T>>
type BigInt<T, K extends PropertyKey = 'id'>
= K extends keyof T
? Omit<T, K> & Record<K, string>
: T & Record<K, string>
type SpreedCapabilities = components['schemas']['Capabilities']
// From https://github.com/nextcloud/password_policy/blob/master/lib/Capabilities.php
@ -250,11 +255,16 @@ export type File = RichObject<'size' | 'path' | 'link' | 'mimetype' | 'preview-a
height: string
}
export type ChatMessage = components['schemas']['ChatMessageWithParent']
/* This supports Snowflake ID */
export type BigIntChatMessage = BigInt<ChatMessage>
export type ChatTask = SummarizeChatTask & {
fromMessageId: number
summary?: string
}
export type ScheduledMessage = components['schemas']['ScheduledMessage']
export type receiveMessagesParams = operations['chat-receive-messages']['parameters']['query']
export type receiveMessagesResponse = ApiResponse<operations['chat-receive-messages']['responses'][200]['content']['application/json']>
export type getMessageContextParams = operations['chat-get-message-context']['parameters']['query']
@ -276,6 +286,13 @@ export type SummarizeChatTask = operations['chat-summarize-chat']['responses'][2
export type upcomingRemindersResponse = ApiResponse<operations['chat-get-upcoming-reminders']['responses'][200]['content']['application/json']>
export type UpcomingReminder = components['schemas']['ChatReminderUpcoming']
export type getScheduledMessagesResponse = ApiResponse<operations['chat-get-scheduled-messages']['responses'][200]['content']['application/json']>
export type scheduleMessageParams = operations['chat-schedule-message']['requestBody']['content']['application/json']
export type scheduleMessageResponse = ApiResponse<operations['chat-schedule-message']['responses'][201]['content']['application/json']>
export type editScheduledMessageParams = operations['chat-edit-scheduled-message']['requestBody']['content']['application/json']
export type editScheduledMessageResponse = ApiResponse<operations['chat-edit-scheduled-message']['responses'][202]['content']['application/json']>
export type deleteScheduledMessageResponse = ApiResponse<operations['chat-delete-schedule-message']['responses'][200]['content']['application/json']>
// Threads
export type Thread = components['schemas']['Thread']
export type ThreadAttendee = components['schemas']['ThreadAttendee']