diff --git a/src/components/Dashboard/TalkDashboard.vue b/src/components/Dashboard/TalkDashboard.vue index 991b31c1638..c9f1bed87a6 100644 --- a/src/components/Dashboard/TalkDashboard.vue +++ b/src/components/Dashboard/TalkDashboard.vue @@ -35,6 +35,7 @@ import { EventBus } from '../../services/EventBus.ts' import { useActorStore } from '../../stores/actor.ts' import { useDashboardStore } from '../../stores/dashboard.ts' import { hasUnreadMentions } from '../../utils/conversation.ts' +import { convertToUnix } from '../../utils/formattedTime.ts' import { copyConversationLinkToClipboard } from '../../utils/handleUrl.ts' const supportsUpcomingReminders = hasTalkFeature('local', 'upcoming-reminders') @@ -141,7 +142,7 @@ async function startMeeting() { roomName: conversationName.value || t('spreed', 'Meeting'), roomType: CONVERSATION.TYPE.PUBLIC, objectType: CONVERSATION.OBJECT_TYPE.INSTANT_MEETING, - objectId: Math.floor(Date.now() / 1000).toString(), + objectId: convertToUnix(new Date()).toString(), }) await copyConversationLinkToClipboard(conversation.token) await router.push({ diff --git a/src/components/LeftSidebar/ConversationsList/Conversation.spec.js b/src/components/LeftSidebar/ConversationsList/Conversation.spec.js index 2d6acb26bcb..8643f313779 100644 --- a/src/components/LeftSidebar/ConversationsList/Conversation.spec.js +++ b/src/components/LeftSidebar/ConversationsList/Conversation.spec.js @@ -17,6 +17,7 @@ import { ATTENDEE, CONVERSATION, PARTICIPANT } from '../../../constants.ts' import { leaveConversation } from '../../../services/participantsService.js' import storeConfig from '../../../store/storeConfig.js' import { findNcActionButton } from '../../../test-helpers.js' +import { convertToUnix } from '../../../utils/formattedTime.ts' vi.mock('../../../services/participantsService', () => ({ leaveConversation: vi.fn(), diff --git a/src/components/MessagesList/MessagesGroup/Message/MessageButtonsBar/MessageButtonsBar.spec.js b/src/components/MessagesList/MessagesGroup/Message/MessageButtonsBar/MessageButtonsBar.spec.js index 16421680896..a5f9d2754f5 100644 --- a/src/components/MessagesList/MessagesGroup/Message/MessageButtonsBar/MessageButtonsBar.spec.js +++ b/src/components/MessagesList/MessagesGroup/Message/MessageButtonsBar/MessageButtonsBar.spec.js @@ -18,6 +18,7 @@ import { useActorStore } from '../../../../../stores/actor.ts' import { useIntegrationsStore } from '../../../../../stores/integrations.js' import { useTokenStore } from '../../../../../stores/token.ts' import { findNcActionButton, findNcButton } from '../../../../../test-helpers.js' +import { convertToUnix } from '../../../../../utils/formattedTime.ts' describe('MessageButtonsBar.vue', () => { const TOKEN = 'XXTOKENXX' @@ -65,7 +66,7 @@ describe('MessageButtonsBar.vue', () => { messageParameters: {}, id: 123, isReplyable: true, - timestamp: new Date('2020-05-07 09:23:00').getTime() / 1000, + timestamp: convertToUnix(new Date('2020-05-07 09:23:00')), token: TOKEN, systemMessage: '', messageType: MESSAGE.TYPE.COMMENT, diff --git a/src/components/MessagesList/MessagesGroup/Message/MessageItem.spec.js b/src/components/MessagesList/MessagesGroup/Message/MessageItem.spec.js index d5594eee3ca..3abc244c3cc 100644 --- a/src/components/MessagesList/MessagesGroup/Message/MessageItem.spec.js +++ b/src/components/MessagesList/MessagesGroup/Message/MessageItem.spec.js @@ -30,6 +30,7 @@ import { EventBus } from '../../../../services/EventBus.ts' import storeConfig from '../../../../store/storeConfig.js' import { useActorStore } from '../../../../stores/actor.ts' import { useTokenStore } from '../../../../stores/token.ts' +import { convertToUnix } from '../../../../utils/formattedTime.ts' let store @@ -92,7 +93,7 @@ describe('MessageItem.vue', () => { messageParameters: {}, id: 123, isReplyable: true, - timestamp: new Date('2020-05-07 09:23:00').getTime() / 1000, + timestamp: convertToUnix(new Date('2020-05-07 09:23:00')), token: TOKEN, systemMessage: '', messageType: MESSAGE.TYPE.COMMENT, @@ -483,7 +484,7 @@ describe('MessageItem.vue', () => { }) test('does not render actions for temporary messages', async () => { - messageProps.message.timestamp = 0 + messageProps.message.id = 'temp-123' const wrapper = mountMessage(messageProps) @@ -594,7 +595,7 @@ describe('MessageItem.vue', () => { }) test('displays the message already with a spinner while sending it', () => { - messageProps.message.timestamp = 0 + messageProps.message.id = 'temp-123' const wrapper = mountMessage(messageProps) const message = wrapper.findComponent(NcRichText) expect(message.text()).toBe('test message') diff --git a/src/components/MessagesList/MessagesGroup/Message/MessageItem.vue b/src/components/MessagesList/MessagesGroup/Message/MessageItem.vue index 5744c8a6cfe..9a83669a0c2 100644 --- a/src/components/MessagesList/MessagesGroup/Message/MessageItem.vue +++ b/src/components/MessagesList/MessagesGroup/Message/MessageItem.vue @@ -115,6 +115,7 @@ import { EventBus } from '../../../../services/EventBus.ts' import { useActorStore } from '../../../../stores/actor.ts' import { useChatExtrasStore } from '../../../../stores/chatExtras.ts' import { getItemTypeFromMessage } from '../../../../utils/getItemTypeFromMessage.ts' +import { isTemporaryId } from '../../../../utils/message.ts' const LocationCard = defineAsyncComponent(() => import('./MessagePart/LocationCard.vue')) @@ -187,7 +188,7 @@ export default { }, isTemporary() { - return !this.isScheduledMessage && this.message.timestamp === 0 + return !this.isScheduledMessage && isTemporaryId(this.message.id) }, isDeletedMessage() { @@ -332,7 +333,7 @@ export default { } return this.message.id === this.message.threadId - || this.message.id.toString().startsWith('temp-') + || isTemporaryId(this.message.id) || (this.isScheduledMessage && this.message.threadId === -1) }, diff --git a/src/components/MessagesList/MessagesGroup/Message/MessagePart/MessageBody.vue b/src/components/MessagesList/MessagesGroup/Message/MessagePart/MessageBody.vue index 7c43bb50c96..b32e91a03bf 100644 --- a/src/components/MessagesList/MessagesGroup/Message/MessagePart/MessageBody.vue +++ b/src/components/MessagesList/MessagesGroup/Message/MessagePart/MessageBody.vue @@ -213,6 +213,7 @@ import { useChatExtrasStore } from '../../../../../stores/chatExtras.ts' import { usePollsStore } from '../../../../../stores/polls.ts' import { useUploadStore } from '../../../../../stores/upload.ts' import { formatDateTime } from '../../../../../utils/formattedTime.ts' +import { isTemporaryId } from '../../../../../utils/message.ts' import { parseMentions, parseSpecialSymbols } from '../../../../../utils/textParse.ts' // Regular expression to check for Unicode emojis in message text @@ -410,7 +411,7 @@ export default { }, isTemporary() { - return !this.isScheduledMessage && this.message.timestamp === 0 + return !this.isScheduledMessage && isTemporaryId(this.message.id) }, isScheduledSendingFailure() { diff --git a/src/components/MessagesList/MessagesList.spec.js b/src/components/MessagesList/MessagesList.spec.js index d7d8c0f7f63..50ce239b24f 100644 --- a/src/components/MessagesList/MessagesList.spec.js +++ b/src/components/MessagesList/MessagesList.spec.js @@ -18,6 +18,7 @@ import router from '../../__mocks__/router.js' import { ATTENDEE, MESSAGE } from '../../constants.ts' import storeConfig from '../../store/storeConfig.js' import { useChatStore } from '../../stores/chat.ts' +import { convertToUnix } from '../../utils/formattedTime.ts' vi.mock('vuex', async () => { const vuex = await vi.importActual('vuex') @@ -48,7 +49,7 @@ vi.mock('../../composables/useGetMessages.ts', async () => ({ })), })) -const fakeTimestamp = (value) => new Date(value).getTime() / 1000 +const fakeTimestamp = (value) => convertToUnix(new Date(value)) describe('MessagesList.vue', () => { const TOKEN = 'XXTOKENXX' diff --git a/src/components/MessagesList/MessagesList.vue b/src/components/MessagesList/MessagesList.vue index aff5c6bd602..35ac8e520bc 100644 --- a/src/components/MessagesList/MessagesList.vue +++ b/src/components/MessagesList/MessagesList.vue @@ -462,7 +462,7 @@ export default { if (!this.messagesShouldBeGrouped(message, lastMessage)) { groupId = message.id if (message.timestamp === 0) { - // This is a temporary message, the timestamp is today + // This is a scheduled message that failed to send, the timestamp is today dateTimestamp = this.currentDay } else { dateTimestamp = convertToUnix(new Date(message.timestamp * 1000).setHours(0, 0, 0, 0)) @@ -625,9 +625,6 @@ export default { * @return {object} Date object */ getDateOfMessage(message) { - if (message.id.toString().startsWith('temp-')) { - return new Date() - } return new Date(message.timestamp * 1000) }, diff --git a/src/composables/__tests__/useMessageInfo.spec.js b/src/composables/__tests__/useMessageInfo.spec.js index 9d1c6d89a2d..11802e0a343 100644 --- a/src/composables/__tests__/useMessageInfo.spec.js +++ b/src/composables/__tests__/useMessageInfo.spec.js @@ -10,6 +10,7 @@ import { useStore } from 'vuex' import { ATTENDEE, CONVERSATION, MESSAGE } from '../../constants.ts' import { useActorStore } from '../../stores/actor.ts' import { useGuestNameStore } from '../../stores/guestName.ts' +import { convertToUnix } from '../../utils/formattedTime.ts' import { useConversationInfo } from '../useConversationInfo.ts' import { useMessageInfo } from '../useMessageInfo.ts' @@ -49,7 +50,7 @@ describe('message actions', () => { messageParameters: {}, id: 123, isReplyable: true, - timestamp: new Date('2024-05-01 16:15:00').getTime() / 1000, + timestamp: convertToUnix(new Date('2024-05-01 16:15:00')), token: TOKEN, systemMessage: '', messageType: MESSAGE.TYPE.COMMENT, @@ -78,7 +79,7 @@ describe('message actions', () => { test('message is not deleteable when it is older than 6 hours and unlimited capability is disabled', () => { // Arrange - message.value.timestamp = new Date('2024-05-01 7:20:00').getTime() / 1000 + message.value.timestamp = convertToUnix(new Date('2024-05-01 7:20:00')) // Act const result = useMessageInfo(message) // Assert @@ -173,7 +174,7 @@ describe('message actions', () => { test('can edit own message in note to self', () => { // Arrange - message.value.timestamp = new Date('2024-04-28 7:20:00').getTime() / 1000 + message.value.timestamp = convertToUnix(new Date('2024-04-28 7:20:00')) conversationProps.type = CONVERSATION.TYPE.NOTE_TO_SELF // Act const result = useMessageInfo(message) @@ -368,11 +369,11 @@ describe('message actions', () => { messageParameters: {}, id: 123, isReplyable: true, - lastEditTimestamp: new Date('2024-05-01 16:30:00').getTime() / 1000, + lastEditTimestamp: convertToUnix(new Date('2024-05-01 16:30:00')), lastEditActorId: 'user-id-1', lastEditActorType: ATTENDEE.ACTOR_TYPE.USERS, lastEditActorDisplayName: 'user-display-name-1', - timestamp: new Date('2024-05-01 16:15:00').getTime() / 1000, + timestamp: convertToUnix(new Date('2024-05-01 16:15:00')), token: TOKEN, systemMessage: '', messageType: MESSAGE.TYPE.COMMENT, diff --git a/src/store/conversationsStore.spec.js b/src/store/conversationsStore.spec.js index 3063fa2c2f4..5df827158e1 100644 --- a/src/store/conversationsStore.spec.js +++ b/src/store/conversationsStore.spec.js @@ -39,6 +39,7 @@ import { setConversationUnread, updateLastReadMessage } from '../services/messag import { useActorStore } from '../stores/actor.ts' import { useTalkHashStore } from '../stores/talkHash.js' import { generateOCSErrorResponse, generateOCSResponse } from '../test-helpers.js' +import { convertToUnix } from '../utils/formattedTime.ts' import storeConfig from './storeConfig.js' vi.mock('../services/conversationsService', () => ({ @@ -1021,7 +1022,7 @@ describe('conversationsStore', () => { vi.useRealTimers() const changedConversation = store.getters.conversation(testToken) - expect(changedConversation.lastActivity).toBe(mockDate.getTime() / 1000) + expect(changedConversation.lastActivity).toBe(convertToUnix(mockDate)) }) }) diff --git a/src/store/messagesStore.js b/src/store/messagesStore.js index 267fde85e7d..b56320bfba5 100644 --- a/src/store/messagesStore.js +++ b/src/store/messagesStore.js @@ -37,7 +37,7 @@ import { useSharedItemsStore } from '../stores/sharedItems.ts' import CancelableRequest from '../utils/CancelableRequest.ts' import { debugTimer } from '../utils/debugTimer.ts' import { convertToUnix } from '../utils/formattedTime.ts' -import { isHiddenSystemMessage } from '../utils/message.ts' +import { isHiddenSystemMessage, isTemporaryId } from '../utils/message.ts' /** * Returns whether the given message contains a mention to self, directly @@ -166,7 +166,7 @@ const getters = { return Object.values(state.messages[token]).filter((message) => { return message.referenceId === referenceId - && ('' + message.id).startsWith('temp-') + && isTemporaryId(message.id) }) }, @@ -188,7 +188,7 @@ const getters = { return getters.messagesList(token).find((message) => { return message.id >= readMessageId - && !String(message.id).startsWith('temp-') + && !isTemporaryId(message.id) && !isHiddenSystemMessage(message) })?.id }, @@ -201,7 +201,7 @@ const getters = { return getters.messagesList(token).findLast((message) => { return message.id < readMessageId && isMessageVisible(message.id) - && !String(message.id).startsWith('temp-') + && !isTemporaryId(message.id) && !isHiddenSystemMessage(message) })?.id }, diff --git a/src/store/participantsStore.spec.js b/src/store/participantsStore.spec.js index 61f99c62d24..a22594515c6 100644 --- a/src/store/participantsStore.spec.js +++ b/src/store/participantsStore.spec.js @@ -36,6 +36,7 @@ import { useGuestNameStore } from '../stores/guestName.ts' import { useSessionStore } from '../stores/session.ts' import { useTokenStore } from '../stores/token.ts' import { generateOCSErrorResponse, generateOCSResponse } from '../test-helpers.js' +import { convertToUnix } from '../utils/formattedTime.ts' import participantsStore from './participantsStore.js' import storeConfig from './storeConfig.js' @@ -1002,7 +1003,7 @@ describe('participantsStore', () => { */ function prepareTestJoinWithMaxPingAge(lastPingAge, inCall) { const mockDate = new Date('2020-01-01 20:00:00') - participantData.lastPing = mockDate.getTime() / 1000 - lastPingAge + participantData.lastPing = convertToUnix(mockDate) - lastPingAge participantData.inCall = inCall vi.useFakeTimers().setSystemTime(mockDate) diff --git a/src/stores/__tests__/callView.spec.js b/src/stores/__tests__/callView.spec.js index 0b38232bc68..2e7a7f7cd76 100644 --- a/src/stores/__tests__/callView.spec.js +++ b/src/stores/__tests__/callView.spec.js @@ -8,6 +8,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { CONVERSATION } from '../../constants.ts' import BrowserStorage from '../../services/BrowserStorage.js' import vuexStore from '../../store/index.js' +import { convertToUnix } from '../../utils/formattedTime.ts' import { useCallViewStore } from '../callView.ts' vi.mock('../../services/BrowserStorage.js', () => ({ @@ -267,19 +268,19 @@ describe('callViewStore', () => { }) it('sets timeout if timestamp is lesser than 10 seconds', () => { - callViewStore.setCallHasJustEnded(Date.now() / 1000 - 3) + callViewStore.setCallHasJustEnded(convertToUnix(Date.now()) - 3) expect(callViewStore.callHasJustEnded).toBeTruthy() }) it('does not set timeout if timestamp is bigger than 10 seconds', () => { - callViewStore.setCallHasJustEnded(Date.now() / 1000 - 15) + callViewStore.setCallHasJustEnded(convertToUnix(Date.now()) - 15) expect(callViewStore.callHasJustEnded).toBeFalsy() }) it('resets callHasJustEnded after passed time', () => { // Arrange vi.useFakeTimers() - callViewStore.setCallHasJustEnded(Date.now() / 1000 - 2) + callViewStore.setCallHasJustEnded(convertToUnix(Date.now()) - 2) expect(callViewStore.callHasJustEnded).toBeTruthy() // Skip 4 seconds vi.advanceTimersByTime(4000) diff --git a/src/stores/chat.ts b/src/stores/chat.ts index 4c2e6430159..537a301e627 100644 --- a/src/stores/chat.ts +++ b/src/stores/chat.ts @@ -13,7 +13,7 @@ import type { import { defineStore } from 'pinia' import { reactive } from 'vue' import { useStore } from 'vuex' -import { isHiddenSystemMessage } from '../utils/message.ts' +import { isHiddenSystemMessage, isTemporaryId } from '../utils/message.ts' import { useChatExtrasStore } from './chatExtras.ts' type GetMessagesListOptions = { @@ -57,7 +57,7 @@ function checkIfBelongsToContext(message: ChatMessage, threadId?: number): boole // In thread context, only thread messages with given threadId are allowed ? threadId === message.threadId // In main context, only non-thread messages, topmost thread messages and temporary messages are allowed - : (!message.isThread || message.id === message.threadId || message.id.toString().startsWith('temp-')) + : (!message.isThread || message.id === message.threadId || isTemporaryId(message.id)) } /** @@ -123,7 +123,21 @@ export const useChatStore = defineStore('chat', () => { * @param threadId */ function prepareMessagesList(token: string, block: Set, threadId?: number): ChatMessage[] { - return Array.from(block).sort((a, b) => a - b) + return Array.from(block) + .sort((a, b) => { + const aIsTemporary = isTemporaryId(a) + const bIsTemporary = isTemporaryId(b) + if (aIsTemporary !== bIsTemporary) { + // Temporary messages always sorted to the end + return aIsTemporary ? 1 : -1 + } else if (aIsTemporary) { + // Temporary messages are not sorted between themselves + return 0 + } else { + // Numeric id messages have incremental order + return a - b + } + }) .reduce((acc, id) => { const message = store.state.messagesStore.messages[token][id] // Check for exceptions (message should not be added to the displayed list): diff --git a/src/utils/__tests__/prepareTemporaryMessage.spec.js b/src/utils/__tests__/prepareTemporaryMessage.spec.js index fe41327d73a..53dc9dc7abd 100644 --- a/src/utils/__tests__/prepareTemporaryMessage.spec.js +++ b/src/utils/__tests__/prepareTemporaryMessage.spec.js @@ -5,6 +5,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { ATTENDEE, MESSAGE } from '../../constants.ts' +import { convertToUnix } from '../formattedTime.ts' import { prepareTemporaryMessage } from '../prepareTemporaryMessage.ts' describe('prepareTemporaryMessage', () => { @@ -40,7 +41,7 @@ describe('prepareTemporaryMessage', () => { reactions: {}, referenceId: expect.stringMatching(/^[a-zA-Z0-9]{64}$/), systemMessage: '', - timestamp: 0, + timestamp: convertToUnix(new Date('2020-01-01T20:00:00')), token: TOKEN, silent: false, threadId: undefined, diff --git a/src/utils/message.ts b/src/utils/message.ts index e9e96869b25..06e230ea140 100644 --- a/src/utils/message.ts +++ b/src/utils/message.ts @@ -185,6 +185,15 @@ export function isFileShareMessage(message: ChatMessage): boolean { return Object.keys(message.messageParameters ?? {}).some((key) => key.startsWith('file')) } +/** + * Returns whether the given id belongs to a temporary message + * + * @param id message id + */ +export function isTemporaryId(id: ChatMessage['id'] | string): boolean { + return id.toString().startsWith('temp-') +} + /** * Returns whether the given system message should be hidden in the UI * diff --git a/src/utils/prepareTemporaryMessage.ts b/src/utils/prepareTemporaryMessage.ts index 14bff3f1a59..4c5b26543bc 100644 --- a/src/utils/prepareTemporaryMessage.ts +++ b/src/utils/prepareTemporaryMessage.ts @@ -9,6 +9,7 @@ import Hex from 'crypto-js/enc-hex.js' import SHA256 from 'crypto-js/sha256.js' import { MESSAGE } from '../constants.ts' import { hasTalkFeature } from '../services/CapabilitiesManager.ts' +import { convertToUnix } from './formattedTime.ts' export type RawTemporaryMessagePayload = Pick