Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
2 changes: 1 addition & 1 deletion src/components/MessagesList/MessagesList.vue
Original file line number Diff line number Diff line change
Expand Up @@ -670,7 +670,7 @@ export default {
return
}

if (!this.$store.getters.getFirstKnownMessageId(this.token)) {
if (!this.chatStore.chatBlocks[this.token]) {
// This can happen if the browser is fast enough to close the sidebar
// when switching from a one-to-one to a group conversation.
console.debug('Ignoring handleScroll as the messages history is empty')
Expand Down
5 changes: 4 additions & 1 deletion src/composables/useDocumentTitle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { useRoute, useRouter } from 'vue-router'
import { useStore } from 'vuex'
import { EventBus } from '../services/EventBus.ts'
import { useActorStore } from '../stores/actor.ts'
import { useChatStore } from '../stores/chat.ts'
import { hasCall, hasUnreadMentions } from '../utils/conversation.ts'
import { useDocumentVisibility } from './useDocumentVisibility.ts'

Expand All @@ -31,6 +32,8 @@ export function useDocumentTitle() {
const route = useRoute()
const isDocumentVisible = useDocumentVisibility()

const chatStore = useChatStore()

const defaultPageTitle = ref<string>(getDefaultPageTitle())
const showAsterisk = ref(false)
const savedLastMessageMap = ref<LastMessageMap>({})
Expand Down Expand Up @@ -122,7 +125,7 @@ export function useDocumentTitle() {
} else {
// @ts-expect-error: Property 'id' does not exist on type ChatProxyMessage
const lastMessageId = lastMessage.id ?? 0
const lastKnownMessageId = store.getters.getLastKnownMessageId(token) ?? 0
const lastKnownMessageId = Math.max(...chatStore.chatBlocks[token][0]) ?? 0
acc[token].lastMessageId = Math.max(lastMessageId, lastKnownMessageId)
}
acc[token].unreadMessages = unreadMessages
Expand Down
4 changes: 3 additions & 1 deletion src/composables/useGetMessages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { START_LOCATION, useRoute, useRouter } from 'vue-router'
import { useStore } from 'vuex'
import { CHAT, MESSAGE } from '../constants.ts'
import { EventBus } from '../services/EventBus.ts'
import { useChatStore } from '../stores/chat.ts'
import { debugTimer } from '../utils/debugTimer.ts'
import { useGetThreadId } from './useGetThreadId.ts'
import { useGetToken } from './useGetToken.ts'
Expand Down Expand Up @@ -55,6 +56,7 @@ export function useGetMessagesProvider() {
const store = useStore()
const router = useRouter()
const route = useRoute()
const chatStore = useChatStore()

const currentToken = useGetToken()
const threadId = useGetThreadId()
Expand Down Expand Up @@ -205,7 +207,7 @@ export function useGetMessagesProvider() {

store.dispatch('setVisualLastReadMessageId', { token, id: conversation.value!.lastReadMessage })

if (!store.getters.getFirstKnownMessageId(token)) {
if (!chatStore.chatBlocks[token]) {
try {
// Start from message hash or unread marker
let startingMessageId = focusMessageId !== null ? focusMessageId : conversation.value!.lastReadMessage
Expand Down
4 changes: 4 additions & 0 deletions src/store/messagesStore.js
Original file line number Diff line number Diff line change
Expand Up @@ -835,6 +835,8 @@ const actions = {
*/
purgeMessagesStore(context, token) {
context.commit('purgeMessagesStore', token)
const chatStore = useChatStore()
chatStore.purgeChatStore(token)
},

/**
Expand All @@ -847,6 +849,8 @@ const actions = {
*/
clearMessagesHistory(context, { token, id }) {
context.commit('clearMessagesHistory', { token, id })
const chatStore = useChatStore()
chatStore.clearMessagesHistory(token, id)
},

/**
Expand Down
100 changes: 100 additions & 0 deletions src/stores/__tests__/chat.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,22 @@ describe('chatStore', () => {
jest.clearAllMocks()
})

describe('check for existence', () => {
it('returns false if chat blocks are not yet available', () => {
// Assert
expect(chatStore.hasMessage(TOKEN, { messageId: mockMessages[109].id })).toBeFalsy()
})

it('returns boolean whether message is known by the store', () => {
// Act
chatStore.processChatBlocks(TOKEN, chatBlockA)

// Assert
expect(chatStore.hasMessage(TOKEN, { messageId: mockMessages[109].id })).toBeTruthy()
expect(chatStore.hasMessage(TOKEN, { messageId: mockMessages[101].id })).toBeFalsy()
})
})

describe('get a list of messages', () => {
it('returns an array if both messages and blocks present', () => {
// Arrange
Expand All @@ -84,6 +100,44 @@ describe('chatStore', () => {
})
})

describe('get first and last known messages', () => {
it('returns given message id if chat blocks are not yet available', () => {
// Assert
expect(chatStore.getLastKnownId(TOKEN, { messageId: mockMessages[109].id })).toBe(mockMessages[109].id)
expect(chatStore.getFirstKnownId(TOKEN, { messageId: mockMessages[109].id })).toBe(mockMessages[109].id)
})

it('returns first / last known id of first block if no message id was given', () => {
// Act
chatStore.processChatBlocks(TOKEN, chatBlockA)
chatStore.processChatBlocks(TOKEN, chatBlockE)

// Assert
expect(chatStore.getLastKnownId(TOKEN)).toBe(chatBlockA[0].id)
expect(chatStore.getFirstKnownId(TOKEN)).toBe(chatBlockE[2].id)
})

it('returns first / last known id of first block if no message id was given', () => {
// Act
chatStore.processChatBlocks(TOKEN, chatBlockA)
chatStore.processChatBlocks(TOKEN, chatBlockC)

// Assert
expect(chatStore.getLastKnownId(TOKEN, { messageId: chatBlockB[0].id })).toBe(chatBlockA[0].id)
expect(chatStore.getFirstKnownId(TOKEN, { messageId: chatBlockB[0].id })).toBe(chatBlockA[1].id)
})

it('returns first / last known id of containing block if message id was given', () => {
// Act
chatStore.processChatBlocks(TOKEN, chatBlockA)
chatStore.processChatBlocks(TOKEN, chatBlockB)

// Assert
expect(chatStore.getLastKnownId(TOKEN, { messageId: chatBlockB[0].id })).toBe(chatBlockB[0].id)
expect(chatStore.getFirstKnownId(TOKEN, { messageId: chatBlockB[0].id })).toBe(chatBlockB[1].id)
})
})

describe('process messages chunks', () => {
it('creates a new block, if not created yet', () => {
// Act
Expand Down Expand Up @@ -250,4 +304,50 @@ describe('chatStore', () => {
expect(chatStore.chatBlocks[TOKEN]).toBeUndefined()
})
})

describe('cleanup messages', () => {
it('does nothing, if no blocks are created yet', () => {
// Act
chatStore.clearMessagesHistory(TOKEN, chatBlockA[0].id)

// Assert
expect(chatStore.chatBlocks[TOKEN]).toBeUndefined()
})

it('does nothing, if no blocks are behind id to delete', () => {
// Arrange
chatStore.processChatBlocks(TOKEN, chatBlockA)
chatStore.processChatBlocks(TOKEN, chatBlockB)

// Act
chatStore.clearMessagesHistory(TOKEN, chatBlockC[0].id)

// Assert
expect(chatStore.chatBlocks[TOKEN]).toEqual([outputSet(chatBlockA), outputSet(chatBlockB)])
})

it('purges a store, if all blocks are behind id to delete', () => {
// Arrange
chatStore.processChatBlocks(TOKEN, chatBlockB)
chatStore.processChatBlocks(TOKEN, chatBlockC)

// Act
chatStore.clearMessagesHistory(TOKEN, chatBlockA[0].id)

// Assert
expect(chatStore.chatBlocks[TOKEN]).toBeUndefined()
})

it('cleans up messages behind id to delete', () => {
// Arrange
chatStore.processChatBlocks(TOKEN, chatBlockB)
chatStore.processChatBlocks(TOKEN, chatBlockC)

// Act
chatStore.clearMessagesHistory(TOKEN, chatBlockB[0].id)

// Assert
expect(chatStore.chatBlocks[TOKEN]).toEqual([outputSet([chatBlockB[0]])])
})
})
})
144 changes: 139 additions & 5 deletions src/stores/chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,13 @@ import { defineStore } from 'pinia'
import { reactive } from 'vue'
import { useStore } from 'vuex'

type GetMessagesListOptions = {
/** if given, look for Set that has it */
messageId?: number
/** if given, look for thread Set */
threadId?: number
}

type ProcessChatBlocksOptions = {
/** if given, look for Set that has it */
mergeBy?: number
Expand Down Expand Up @@ -46,8 +53,19 @@ export const useChatStore = defineStore('chat', () => {
return []
}

// FIXME temporary show all messages from al blocks - no behaviour change
return Array.from(chatBlocks[token].flatMap((set) => Array.from(set)))
// FIXME temporary show all messages for given thread from all chat blocks - no behaviour change
const contextBlock = chatBlocks[token].reduce<Set<number>>((acc, set) => {
set.forEach((id) => acc.add(id))
return acc
}, new Set())
return prepareMessagesList(token, contextBlock)
}

/**
* Returns list of messages from given set
*/
function prepareMessagesList(token: string, block: Set<number>): ChatMessage[] {
return Array.from(block).sort((a, b) => a - b)
.sort((a, b) => a - b)
.reduce<ChatMessage[]>((acc, id) => {
const message = store.state.messagesStore.messages[token][id]
Expand All @@ -59,13 +77,89 @@ export const useChatStore = defineStore('chat', () => {
}, [])
}

/**
* Returns whether message is known in any of blocks (then it exists in store)
*/
function hasMessage(
token: string,
{ messageId = 0, threadId = 0 }: GetMessagesListOptions = { messageId: 0, threadId: 0 },
): boolean {
if (!chatBlocks[token]) {
return false
}

if (threadId) {
// FIXME temporary check all messages for given thread from all chat blocks
return chatBlocks[token].findIndex((set) => set.has(messageId)) !== -1
}

return chatBlocks[token].findIndex((set) => set.has(messageId)) !== -1
}

/**
* Returns first known message id, belonging to current context. Defaults to given messageId
*/
function getFirstKnownId(
token: string,
{ messageId = 0, threadId = 0 }: GetMessagesListOptions = { messageId: 0, threadId: 0 },
): number {
if (!chatBlocks[token]) {
return messageId
}

if (threadId) {
// If topmost message of thread is in the store, return its id
if (hasMessage(token, { messageId: threadId, threadId })) {
return threadId
}
// FIXME temporary check all messages for given thread from all chat blocks
return Math.min(...prepareMessagesList(token, new Set(Array.from(chatBlocks[token].flatMap((set) => Array.from(set)))))
.filter((message) => {
return message.threadId === threadId
}).map((message) => message.id))
}

if (messageId <= 0) {
return Math.min(...chatBlocks[token][0])
}

const contextBlock = chatBlocks[token].find((set) => set.has(messageId))
return contextBlock ? Math.min(...contextBlock) : Math.min(...chatBlocks[token][0])
}

/**
* Returns last known message id, belonging to current context. Defaults to given messageId
*/
function getLastKnownId(
token: string,
{ messageId = 0, threadId = 0 }: GetMessagesListOptions = { messageId: 0, threadId: 0 },
): number {
if (!chatBlocks[token]) {
return messageId
}

if (threadId) {
// FIXME temporary check all messages for given thread from all chat blocks
return Math.max(...prepareMessagesList(token, new Set(Array.from(chatBlocks[token].flatMap((set) => Array.from(set)))))
.filter((message) => {
return message.threadId === threadId
}).map((message) => message.id))
}

if (messageId <= 0) {
return Math.max(...chatBlocks[token][0])
}

const contextBlock = chatBlocks[token].find((set) => set.has(messageId))
return contextBlock ? Math.max(...contextBlock) : Math.max(...chatBlocks[token][0])
}

/**
* Populate chat blocks from given arrays of messages
* If blocks already exist, try to extend them
*/
function processChatBlocks(token: string, messages: ChatMessage[], options?: ProcessChatBlocksOptions): void {
const newMessageIds = messages.map((message) => message.id)
const newMessageIdsSet = new Set(newMessageIds)
const newMessageIdsSet = new Set(messages.map((message) => message.id))

if (!chatBlocks[token]) {
// If no blocks exist, create a new one with the first message. First in array will be considered main block
Expand Down Expand Up @@ -161,16 +255,56 @@ export const useChatStore = defineStore('chat', () => {
}, [])

if (chatBlocks[token].length === 0) {
delete chatBlocks[token]
purgeChatStore(token)
}
}

/**
* Clears the messages entry from the store for the given token starting from defined id
*/
function clearMessagesHistory(token: string, idToDelete: number) {
if (!chatBlocks[token]) {
return
}

const deleteIndex = chatBlocks[token].findIndex((block) => Math.max(...block) < idToDelete)
if (deleteIndex === -1) {
// Not found, nothing to delete
return
} else if (deleteIndex === 0) {
// If first block is to be deleted, remove all blocks
purgeChatStore(token)
return
} else {
// Remove all blocks with max id less than given id
chatBlocks[token] = chatBlocks[token].slice(0, deleteIndex)
const lastBlock = chatBlocks[token].at(-1)!
for (const id of lastBlock) {
if (id < idToDelete) {
lastBlock.delete(id)
}
}
}
}

/**
* Clears the store for the given token
*/
function purgeChatStore(token: string) {
delete chatBlocks[token]
}

return {
chatBlocks,

getMessagesList,
hasMessage,
getFirstKnownId,
getLastKnownId,
processChatBlocks,
addMessageToChatBlocks,
removeMessagesFromChatBlocks,
clearMessagesHistory,
purgeChatStore,
}
})