UNPKG

softchatjs-core

Version:
1 lines 150 kB
{"version":3,"sources":["../src/events.ts","../src/types.ts","../src/utils.ts","../src/Broadcast.ts","../src/fetch.ts","../src/MessageClient.ts","../src/Connection.ts","../src/error.ts","../src/Conversation.ts","../src/ChatClient.ts","../src/index.ts"],"sourcesContent":["export enum Events {\r\n CONVERSATION_LIST_CHANGED = 'conversation_list_changed',\r\n CONNECTION_CHANGED = 'connection_changed',\r\n NEW_MESSAGE = 'new_message',\r\n NEW_BROADCAST_MESSAGE = 'new_broadcast_message',\r\n EDITED_MESSAGE = 'edited_message',\r\n HAS_STARTED_TYPING = 'started_typing',\r\n HAS_STOPPED_TYPING = 'stopped_typing',\r\n NEW_CONVERSATION = 'new_conversation',\r\n CONVERSATION_LIST_META_CHANGED = 'conversation_list_meta_changed',\r\n BROADCAST_LIST_META_CHANGED = 'broadcast_list_meta_changed',\r\n DELETED_MESSAGE = 'deleted_message',\r\n UPLOAD_FAILED = 'upload_failed',\r\n}\r\n","export enum ClientActions {\r\n INCOMING_MESSAGE = 'incomingMessage',\r\n MESSAGES_READ = 'messagesRead',\r\n USER_IS_TYPING = 'userIsTyping',\r\n MESSAGE_ERROR = 'sendMessageError',\r\n NEW_MESSAGE_REACTION = 'newMessageReaction',\r\n ACK_HEALTH_CHECK = 'acknowledgeHealthCheck',\r\n EDITED_MESSAGE = 'editedMessage',\r\n MESSAGE_DELETED = 'deletedMessage',\r\n MESSAGE_SENT = 'messageSent'\r\n}\r\n\r\n// sent to the server\r\nexport enum ServerActions {\r\n INITIALIZE = 'initialize',\r\n SEND_MESSAGE = 'sendMessage',\r\n CREATE_CONVERSATION = 'createConversation',\r\n SEND_MESSAGE_REPLY = 'sendMessageReply',\r\n USER_TYPING = 'userTyping',\r\n HEALTH_CHECK = 'healthCheck',\r\n SEND_LOCATION = 'sendLocation',\r\n READ_MESSAGES = 'readMessages',\r\n DELETE_MESSAGE = 'deleteMessage',\r\n EDIT_MESSAGE = 'editMessage',\r\n SEND_MESSAGE_REACTION = 'sendMessageReaction',\r\n CONNECTION_CLOSED = 'clearUserSession',\r\n CREATE_BROADCAST_LIST = 'createBroadcastList',\r\n BROADCAST_MESSAGE = 'sendMessageToBroadcastList',\r\n UPDATE_BROADCAST_LIST = 'updateUserBroadcastList',\r\n DELETE_BROADCAST_LIST = 'deleteBroadcastList'\r\n}\r\n\r\ntype Timetamps = {\r\n createdAt: string | Date\r\n updatedAt: string | Date\r\n deletedAt?: string | Date\r\n}\r\n\r\nexport type Prettify<T> = {\r\n [K in keyof T]: T[K]\r\n} & {}\r\n\r\nexport enum AttachmentTypes {\r\n NONE = 'none',\r\n MAP = 'map',\r\n MEDIA = 'media',\r\n STICKER = 'sticker'\r\n}\r\n\r\nexport interface Participant extends User {\r\n meta: UserMeta\r\n}\r\n\r\ntype User = {\r\n uid: string,\r\n connectionId?: string,\r\n projectId?: string,\r\n meta: UserMeta\r\n} & Timetamps\r\n\r\nexport type UserMeta = {\r\n username: string,\r\n uid: string,\r\n firstname?: string,\r\n lastname?: string,\r\n profileUrl?: string,\r\n custom?: Record<string, string>,\r\n color?: string\r\n}\r\n\r\nexport enum MediaType {\r\n VIDEO = 'video',\r\n AUDIO = 'audio',\r\n IMAGE = 'image',\r\n DOCUMENT = 'document',\r\n STICKER = 'sticker'\r\n}\r\n\r\nexport type Media = {\r\n type: MediaType,\r\n ext: string,\r\n mediaId: string,\r\n mediaUrl: string,\r\n mimeType?: string,\r\n meta?: {\r\n aspectRatio?: number,\r\n height?: number,\r\n width?: number,\r\n size?: number,\r\n audioDurationSec?: number,\r\n },\r\n uploading?: boolean\r\n}\r\n\r\nexport type Point = {\r\n lng: number,\r\n lat: number\r\n}\r\n\r\ntype QuotedMessage = Message | null\r\n\r\nexport type Message = {\r\n conversationId: string,\r\n from: string,\r\n to: string,\r\n quotedMessage: QuotedMessage | null,\r\n message: string,\r\n messageState: number,\r\n messageId: string,\r\n attachmentType?: AttachmentTypes,\r\n messageOwner: UserMeta,\r\n replyTo?: string,\r\n sharedLocation?: Point,\r\n sharedImage?: {\r\n url: string,\r\n aspectRatio?: number,\r\n },\r\n attachedMedia: Media[],\r\n token?: string,\r\n quotedMessageId?: string,\r\n reactions: Reaction[],\r\n lastEdited: Date | string | null,\r\n broadcastListId?: string,\r\n isBroadcast?: boolean\r\n} & Timetamps\r\n\r\nexport type ParticipantListInfo = {\r\n id: string;\r\n uid: string,\r\n projectId?: string | null;\r\n connectionId?: string;\r\n participantId: string,\r\n participantDetails: UserMeta\r\n} & Timetamps\r\n\r\nexport type ConversationType = 'private-chat' | 'group-chat' | \"admin-chat\" | \"broadcast-chat\"\r\n\r\nexport type GroupChatMeta = {\r\n groupName: string,\r\n groupIcon?: string,\r\n groupWallpaper?: string,\r\n groupBanner?: string\r\n}\r\n\r\nexport type PrivateChatMeta = {\r\n chatWallpaper?: string,\r\n}\r\n\r\nexport type Conversation = {\r\n participants: string[],\r\n admins: string[],\r\n conversationId: string,\r\n messages: Message[],\r\n conversationType: ConversationType,\r\n participantList: ParticipantListInfo[],\r\n meta: PrivateChatMeta | null,\r\n groupMeta: GroupChatMeta | null\r\n name?: string\r\n} & Timetamps\r\n\r\nexport type UploadContent = {\r\n base64: string,\r\n conversationId: string,\r\n key: string\r\n}\r\n// & Omit<Media, 'mediaUrl'>\r\n\r\nexport type IncomingMessage = {\r\n action: ClientActions.INCOMING_MESSAGE,\r\n message: Message\r\n}\r\n\r\nexport type WsPayLoad<Action, Data> = {\r\n action: Action,\r\n message: Data\r\n}\r\n\r\nexport type ReceivedAction = ServerActions\r\n\r\nexport type ReadMessages = {\r\n uid: string\r\n messageIds: string[],\r\n}\r\n\r\nexport type UserTyping = {\r\n uid: string\r\n}\r\n\r\nexport type InitiateConnection = {\r\n from: string,\r\n to: string,\r\n newConversation: boolean,\r\n userDetails: UserMeta,\r\n recipientMeta: UserMeta,\r\n}\r\n\r\nexport type StringOrNumber = string | number\r\n\r\nexport type Config = {\r\n/**\r\n * - `projectId`: The ID of the project.\r\n */\r\n projectId: string,\r\n /**\r\n * - `subId`: The subscriber ID.\r\n */\r\n subId: string\r\n}\r\n\r\nexport type StartConversation = {\r\n from: string,\r\n recipientMeta: UserMeta\r\n message: string\r\n}\r\n\r\nexport type Reaction = {\r\n emoji: string,\r\n uid: string\r\n}\r\n\r\nexport enum MessageStates {\r\n NONE = 0,\r\n FAILED = 1,\r\n LOADING = 2,\r\n SENT = 3,\r\n DELIVERED = 4,\r\n READ = 5,\r\n}\r\n\r\nexport type WsAccessConfig = {\r\n url: string,\r\n token: string\r\n}\r\n\r\nexport enum Screens {\r\n CHAT = \"chat\",\r\n CONVERSATIONS = \"conversations\",\r\n}\r\n\r\nexport type EditedMessage = { from: string, to: string, conversationId: string, messageId: string, textMessage: string, shouldEdit: boolean, isBroadcast?: boolean }\r\n\r\nexport type DeletedMessage = {\r\n conversationId: string, \r\n messageId: string, \r\n}\r\n\r\nexport type ConversationWithTypingIndicator = {\r\n conversationId: string;\r\n timer: NodeJS.Timeout;\r\n timeActive: Date | string;\r\n};\r\n\r\nexport type MessageReactionPayload = {\r\n action: ServerActions,\r\n message: {\r\n conversationId: string,\r\n messageId: string,\r\n from: string,\r\n to: string,\r\n reactions: Array<Reaction>,\r\n },\r\n}\r\n\r\nexport type ConversationMap = {\r\n [key: string]: Conversation\r\n}\r\n\r\nexport type ConversationListItem = {\r\n conversation: Conversation,\r\n lastMessage: Message | null,\r\n unread: string[]\r\n}\r\n\r\nexport type ConversationListMeta = {\r\n [key: string]: ConversationListItem\r\n}\r\n\r\nexport type BroadcastListMeta = {\r\n [key: string]: Conversation\r\n}\r\n\r\nexport type ChatEventGenerics<T> = T\r\n\r\nexport type ConnectionEvent = {\r\n connecting: boolean, \r\n fetchingConversations: boolean, \r\n isConnected: boolean\r\n}\r\n\r\nexport type SendMessageGenerics<M> = Omit<M, \r\n'from' | \r\n'messageState' |\r\n'messageId' |\r\n'messageOwner' |\r\n'token' |\r\n'lastEdit' |\r\n'lastEdited' |\r\n'deletedAt' |\r\n'createdAt' |\r\n'updatedAt' |\r\n'quotedMessageId'\r\n>\r\n\r\nexport type SendGroupMessageGenerics<M> = Omit<M, \r\n'to' |\r\n'from' | \r\n'messageState' |\r\n'messageId' |\r\n'messageOwner' |\r\n'token' |\r\n'lastEdit' |\r\n'lastEdited' |\r\n'deletedAt' |\r\n'createdAt' |\r\n'updatedAt' |\r\n'replyTo' |\r\n'quotedMessageId' |\r\n'quotedMessage' |\r\n'conversationId' |\r\n'attachedMedia'\r\n>\r\n\r\nexport type Emoji = {\r\n emoji: string,\r\n description: string,\r\n category: string,\r\n aliases: string[],\r\n tags: string[],\r\n unicode_version: string,\r\n ios_version: string\r\n}","import moment from 'moment';\r\n\r\nimport { Conversation, StringOrNumber, Participant, ParticipantListInfo, Message, MessageStates, UserMeta } from \"./types\";\r\n\r\nfunction murmurHash3_x64_64(str: string, seed = 0) {\r\n let h1 = BigInt(seed);\r\n let h2 = BigInt(seed);\r\n const c1 = BigInt(\"0x87c37b91114253d5\");\r\n const c2 = BigInt(\"0x4cf5ad432745937f\");\r\n\r\n let length = str.length;\r\n const remainder = length & 15; // length % 16\r\n const bytes = length - remainder;\r\n\r\n for (let i = 0; i < bytes; i += 16) {\r\n let k1 =\r\n BigInt(str.charCodeAt(i)) |\r\n (BigInt(str.charCodeAt(i + 1)) << BigInt(8)) |\r\n (BigInt(str.charCodeAt(i + 2)) << BigInt(16)) |\r\n (BigInt(str.charCodeAt(i + 3)) << BigInt(24)) |\r\n (BigInt(str.charCodeAt(i + 4)) << BigInt(32)) |\r\n (BigInt(str.charCodeAt(i + 5)) << BigInt(40)) |\r\n (BigInt(str.charCodeAt(i + 6)) << BigInt(48)) |\r\n (BigInt(str.charCodeAt(i + 7)) << BigInt(56));\r\n\r\n let k2 =\r\n BigInt(str.charCodeAt(i + 8)) |\r\n (BigInt(str.charCodeAt(i + 9)) << BigInt(8)) |\r\n (BigInt(str.charCodeAt(i + 10)) << BigInt(16)) |\r\n (BigInt(str.charCodeAt(i + 11)) << BigInt(24)) |\r\n (BigInt(str.charCodeAt(i + 12)) << BigInt(32)) |\r\n (BigInt(str.charCodeAt(i + 13)) << BigInt(40)) |\r\n (BigInt(str.charCodeAt(i + 14)) << BigInt(48)) |\r\n (BigInt(str.charCodeAt(i + 15)) << BigInt(56));\r\n\r\n k1 = k1 * c1;\r\n k1 = (k1 << BigInt(31)) | (k1 >> BigInt(33));\r\n k1 = k1 * c2;\r\n h1 ^= k1;\r\n\r\n h1 = (h1 << BigInt(27)) | (h1 >> BigInt(37));\r\n h1 = h1 + h2;\r\n h1 = h1 * BigInt(5) + BigInt(\"0x52dce729\");\r\n\r\n k2 = k2 * c2;\r\n k2 = (k2 << BigInt(33)) | (k2 >> BigInt(31));\r\n k2 = k2 * c1;\r\n h2 ^= k2;\r\n\r\n h2 = (h2 << BigInt(31)) | (h2 >> BigInt(33));\r\n h2 = h1 + h2;\r\n h2 = h2 * BigInt(5) + BigInt(\"0x38495ab5\");\r\n }\r\n\r\n let k1 = BigInt(0);\r\n let k2 = BigInt(0);\r\n\r\n switch (remainder) {\r\n case 15:\r\n k2 ^= BigInt(str.charCodeAt(bytes + 14)) << BigInt(48);\r\n case 14:\r\n k2 ^= BigInt(str.charCodeAt(bytes + 13)) << BigInt(40);\r\n case 13:\r\n k2 ^= BigInt(str.charCodeAt(bytes + 12)) << BigInt(32);\r\n case 12:\r\n k2 ^= BigInt(str.charCodeAt(bytes + 11)) << BigInt(24);\r\n case 11:\r\n k2 ^= BigInt(str.charCodeAt(bytes + 10)) << BigInt(16);\r\n case 10:\r\n k2 ^= BigInt(str.charCodeAt(bytes + 9)) << BigInt(8);\r\n case 9:\r\n k2 ^= BigInt(str.charCodeAt(bytes + 8));\r\n k2 = k2 * c2;\r\n k2 = (k2 << BigInt(33)) | (k2 >> BigInt(31));\r\n k2 = k2 * c1;\r\n h2 ^= k2;\r\n case 8:\r\n k1 ^= BigInt(str.charCodeAt(bytes + 7)) << BigInt(56);\r\n case 7:\r\n k1 ^= BigInt(str.charCodeAt(bytes + 6)) << BigInt(48);\r\n case 6:\r\n k1 ^= BigInt(str.charCodeAt(bytes + 5)) << BigInt(40);\r\n case 5:\r\n k1 ^= BigInt(str.charCodeAt(bytes + 4)) << BigInt(32);\r\n case 4:\r\n k1 ^= BigInt(str.charCodeAt(bytes + 3)) << BigInt(24);\r\n case 3:\r\n k1 ^= BigInt(str.charCodeAt(bytes + 2)) << BigInt(16);\r\n case 2:\r\n k1 ^= BigInt(str.charCodeAt(bytes + 1)) << BigInt(8);\r\n case 1:\r\n k1 ^= BigInt(str.charCodeAt(bytes));\r\n k1 = k1 * c1;\r\n k1 = (k1 << BigInt(31)) | (k1 >> BigInt(33));\r\n k1 = k1 * c2;\r\n h1 ^= k1;\r\n }\r\n\r\n h1 ^= BigInt(length);\r\n h2 ^= BigInt(length);\r\n\r\n h1 += h2;\r\n h2 += h1;\r\n\r\n h1 ^= h1 >> BigInt(33);\r\n h1 = h1 * BigInt(\"0xff51afd7ed558ccd\");\r\n h1 ^= h1 >> BigInt(33);\r\n h1 = h1 * BigInt(\"0xc4ceb9fe1a85ec53\");\r\n h1 ^= h1 >> BigInt(33);\r\n\r\n h2 ^= h2 >> BigInt(33);\r\n h2 = h2 * BigInt(\"0xff51afd7ed558ccd\");\r\n h2 ^= h2 >> BigInt(33);\r\n h2 = h2 * BigInt(\"0xc4ceb9fe1a85ec53\");\r\n h2 ^= h2 >> BigInt(33);\r\n\r\n h1 += h2;\r\n h2 += h1;\r\n\r\n // Combine h1 and h2 to return a 64-bit hash\r\n return (h1 & BigInt(\"0xFFFFFFFFFFFFFFFF\")).toString(16);\r\n}\r\n\r\n// Updated generateConversationId using MurmurHash3 x64 (64-bit)\r\nexport function generateConversationId(str1: string, str2: string, projectId: string) {\r\n // Sort the strings alphabetically to ensure consistency\r\n const sortedStrings = [str1, str2].sort();\r\n // Concatenate the sorted strings with a delimiter\r\n const combinedString = sortedStrings.join(\"_\");\r\n // Generate a 64-bit hash of the combined string using MurmurHash3 x64\r\n const hash = murmurHash3_x64_64(`${projectId}:${combinedString}`);\r\n // Return the hash as the unique ID\r\n return hash;\r\n}\r\n\r\nexport const generateId = () => {\r\n let uuid = '';\r\n const characters = 'abcdef0123456789';\r\n for (let i = 0; i < 32; i++) {\r\n const randomNumber = Math.floor(Math.random() * characters.length);\r\n const character = characters.charAt(randomNumber);\r\n if (i === 8 || i === 12 || i === 16 || i === 20) {\r\n uuid += '-';\r\n }\r\n uuid += character;\r\n }\r\n return uuid;\r\n}\r\n\r\n\r\nexport const getUserInfoWithId = (userId: string, participantList: ParticipantListInfo[]): {\r\n presentUser: UserMeta | undefined,\r\n receivingUser: UserMeta | undefined,\r\n} => {\r\n let presentUser = participantList.find(participant => participant.participantId === userId);\r\n let otherParticipants = participantList.filter(participant => participant.participantId !== userId)\r\n return { presentUser: presentUser?.participantDetails, receivingUser: otherParticipants[0]?.participantDetails };\r\n};\r\n\r\nexport const truncate = (str: string, len: number) => {\r\n return str.length > len ? str.substring(0, len)+'...' : str;\r\n}\r\n\r\nexport const getConversationTitle = (userId: string, converstaion: Conversation) => {\r\n if(converstaion.conversationType === 'private-chat'){\r\n const userInfos = getUserInfoWithId(userId, converstaion.participantList);\r\n\r\n const firstname = userInfos.receivingUser?.firstname\r\n const username = userInfos.receivingUser?.username\r\n return firstname? firstname : username\r\n }\r\n if(converstaion.conversationType === 'group-chat') {\r\n return converstaion.groupMeta?.groupName || 'no-groupname'\r\n }\r\n}\r\n\r\nexport const getUsernameInitials = (username: string) =>{\r\n return username.substring(0, 1)\r\n}\r\n\r\nexport function formatMessageTime(time: Date | string) {\r\n return moment(new Date(time)).format(\"hh:mm a\");\r\n}\r\n\r\nexport function formatConversationTime(time: Date | string) {\r\n const now = moment();\r\n const then = moment(time);\r\n const duration = moment.duration(now.diff(then));\r\n\r\n // Get the largest unit\r\n const years = Math.floor(duration.asYears());\r\n if (years > 0) return years + 'yr';\r\n\r\n const months = Math.floor(duration.asMonths());\r\n if (months > 0) return months + 'mo';\r\n\r\n const weeks = Math.floor(duration.asWeeks());\r\n if (weeks > 0) return weeks + 'w';\r\n\r\n const days = Math.floor(duration.asDays());\r\n if (days > 0) return days + 'd';\r\n\r\n const hours = Math.floor(duration.asHours());\r\n if (hours > 0) return hours + 'h';\r\n\r\n const minutes = Math.floor(duration.asMinutes());\r\n if (minutes > 0) return minutes + 'm';\r\n\r\n // If duration is less than 1 minute\r\n return 'Just now';\r\n}\r\n\r\nexport const generateFillerTimestamps = () => {\r\n return {\r\n createdAt: new Date(),\r\n updatedAt: new Date(),\r\n }\r\n}\r\n\r\nexport const getUnreadMessageIds = (conversation: Conversation, userId: string) => {\r\n var ids: string[] = []\r\n conversation.messages.map(m => {\r\n if (m.messageState === MessageStates.SENT && m.from !== userId) {\r\n ids.push(m.messageId)\r\n }\r\n })\r\n return ids\r\n}\r\n\r\nexport const getQuotedMessage = (messageId: string, messages: Message[]) => {\r\n const message = messages.find(msg => msg.messageId === messageId)\r\n return message\r\n}\r\n","import Connection from \"./Connection\";\r\nimport { Errors } from \"./error\";\r\nimport { Events } from \"./events\";\r\nimport {\r\n AttachmentTypes,\r\n BroadcastListMeta,\r\n Conversation,\r\n ConversationListItem,\r\n ConversationType,\r\n GroupChatMeta,\r\n Message,\r\n MessageStates,\r\n Participant,\r\n Prettify,\r\n SendGroupMessageGenerics,\r\n SendMessageGenerics,\r\n ServerActions,\r\n UserMeta,\r\n} from \"./types\";\r\nimport {\r\n generateConversationId,\r\n generateFillerTimestamps,\r\n generateId,\r\n} from \"./utils\";\r\n\r\nlet userMetaSample = {\r\n id: \"\",\r\n username: \"\",\r\n email: \"\",\r\n firstname: \"\",\r\n lastname: \"\",\r\n profileImgUrl: \"\",\r\n phone: \"\",\r\n profileBannerUrl: \"\",\r\n custom: {},\r\n};\r\nexport default class BroadcastList {\r\n private static conversation: BroadcastList | null = null;\r\n private connection: Connection;\r\n private participants: UserMeta[];\r\n\r\n constructor(connection: Connection, participants: UserMeta[]) {\r\n this.connection = connection;\r\n this.participants = participants;\r\n }\r\n\r\n static getInstance(\r\n connection: Connection,\r\n participants: UserMeta[]\r\n ): BroadcastList {\r\n if (BroadcastList.conversation) {\r\n return BroadcastList.conversation;\r\n }\r\n BroadcastList.conversation = new BroadcastList(connection, participants);\r\n return BroadcastList.conversation;\r\n }\r\n\r\n private generateConversation(\r\n name: string,\r\n conversationId: string\r\n ): Conversation & { name: string } {\r\n const timeStamps = generateFillerTimestamps();\r\n const participantIds = this.participants.map((p) => p.uid);\r\n const senderObject = {\r\n id: generateId(),\r\n uid: this.connection.userMeta.uid,\r\n participantId: this.connection.userMeta.uid,\r\n participantDetails: {\r\n ...this.connection.userMeta,\r\n ...timeStamps,\r\n },\r\n ...timeStamps,\r\n };\r\n\r\n const updatedParticipantList = this.participants.map((participant) => ({\r\n id: generateId(),\r\n uid: participant.uid,\r\n participantId: participant.uid,\r\n participantDetails: {\r\n ...participant,\r\n ...timeStamps,\r\n },\r\n ...timeStamps,\r\n }));\r\n\r\n const participantList = [senderObject, ...updatedParticipantList];\r\n\r\n return {\r\n name,\r\n participants: [this.connection.userMeta.uid, ...participantIds],\r\n admins: [this.connection.userMeta.uid],\r\n conversationId,\r\n messages: [],\r\n conversationType: \"broadcast-chat\",\r\n participantList,\r\n meta: null,\r\n groupMeta: null,\r\n ...timeStamps,\r\n };\r\n }\r\n\r\n create(name: string = `${this.participants.length} Recipients`) {\r\n try {\r\n if (!this.connection) {\r\n throw new Error(\"Inialize uesr before calling method\");\r\n }\r\n const conversationId = generateId();\r\n const newConveration = this.generateConversation(name, conversationId);\r\n const socketMessage = {\r\n action: ServerActions.CREATE_BROADCAST_LIST,\r\n message: {\r\n conversationId,\r\n name: name,\r\n participants: this.participants,\r\n token: this.connection.wsAccessConfig.token,\r\n user: this.connection.userMeta,\r\n },\r\n };\r\n this.connection.socket.send(JSON.stringify(socketMessage));\r\n this.connection.broadcastListMeta[conversationId] = {\r\n conversation: newConveration,\r\n lastMessage: null,\r\n unread: [],\r\n };\r\n this.reset();\r\n this.connection.emit(Events.BROADCAST_LIST_META_CHANGED, {\r\n broadcastListMeta: this.connection.broadcastListMeta,\r\n });\r\n return {\r\n [conversationId]: {\r\n conversation: newConveration,\r\n lastMessage: null,\r\n unread: [],\r\n },\r\n };\r\n } catch (error) {\r\n if(error instanceof Error){\r\n console.error(error.message);\r\n }\r\n }\r\n }\r\n\r\n reset() {\r\n BroadcastList.conversation = null;\r\n }\r\n}\r\n","import axios, { AxiosResponse } from \"axios\";\r\nimport { MediaType, Prettify, UploadContent } from \"./types\";\r\n\r\nlet API = \"https://api.softchatjs.com\";\r\n\r\nenum ENDPOINTS {\r\n CONVERSATIONS = \"/conversations\",\r\n CONVERSATION = \"/conversation\",\r\n MESSAGES = \"/messages\",\r\n UPLOAD = \"/upload\",\r\n UPLOAD_ATTACHMENT = \"/upload-attachment\",\r\n CREATE_SESSION = \"/auth/session\",\r\n EMOJIS = \"/gifs/trending\",\r\n GET_PRESIGNED_URL = \"/presigned-url\",\r\n BROADCAST_LIST = \"/broadcastlist\",\r\n BROADCAST_LISTS = \"/broadcastlists\",\r\n}\r\n\r\ntype Payload = {\r\n endpoint: string;\r\n body: Object;\r\n method: \"GET\" | \"POST\" | \"PUT\";\r\n token?: string;\r\n headers?: Record<string, string>;\r\n};\r\n\r\nconst chatApi = async <R>(payload: Payload): Promise<APIResponse<R>> => {\r\n try {\r\n const res: AxiosResponse<R> = await axios({\r\n url: payload.endpoint,\r\n method: payload.method,\r\n headers: {\r\n \"Cache-Control\": \"no-cache\",\r\n accessToken: payload.token || \"\",\r\n \"Content-Type\": \"application/json\",\r\n ...payload.headers,\r\n },\r\n data: payload.method === \"POST\" ? payload.body : undefined,\r\n responseType: \"json\",\r\n timeout: 30000,\r\n });\r\n const response = res.data;\r\n if (typeof response === \"object\" && response && \"success\" in response) {\r\n return response as unknown as APIResponse<R>;\r\n }\r\n return { ...response, success: false } as unknown as APIResponse<R>;\r\n } catch (error) {\r\n console.log(error, \"fetch error\");\r\n if (axios.isAxiosError(error) && error.response) {\r\n throw new Error(`HTTP error! Status: ${error.response.status}`);\r\n }\r\n throw new Error(\"An unknown error occurred.\");\r\n }\r\n};\r\n\r\ntype APIResponse<R> = {\r\n success: boolean;\r\n data: R;\r\n message: string;\r\n};\r\n\r\nexport async function CREATE_SESSION<Response>({\r\n userId,\r\n subId,\r\n projectId,\r\n}: {\r\n userId: string;\r\n subId: string;\r\n projectId: string;\r\n}): Promise<APIResponse<Response>> {\r\n return await chatApi<Response>({\r\n endpoint: `${API}${ENDPOINTS.CREATE_SESSION}`,\r\n body: { userId, projectId, subId },\r\n method: \"POST\",\r\n // headers: { \"x-api-key\": apiKey },\r\n });\r\n}\r\n\r\nexport async function GET_CONVERSATIONS<Response>(\r\n token: string | undefined,\r\n): Promise<APIResponse<Response>> {\r\n return await chatApi<Response>({\r\n endpoint: `${API}${ENDPOINTS.CONVERSATIONS}`,\r\n body: {},\r\n method: \"GET\",\r\n token,\r\n });\r\n}\r\n\r\nexport async function GET_CONVERSATION<Response>(\r\n token: string | undefined, conversationId: string\r\n): Promise<APIResponse<Response>> {\r\n return await chatApi<Response>({\r\n endpoint: `${API}${ENDPOINTS.CONVERSATION}/${conversationId}`,\r\n body: {},\r\n method: \"GET\",\r\n token,\r\n });\r\n}\r\n\r\nexport async function GET_BROADCASTLISTS<Response>(\r\n token: string | undefined\r\n): Promise<APIResponse<Response>> {\r\n return await chatApi<Response>({\r\n endpoint: `${API}${ENDPOINTS.BROADCAST_LISTS}`,\r\n body: {},\r\n method: \"GET\",\r\n token,\r\n });\r\n}\r\n\r\nexport async function GET_MESSAGES<Response>(\r\n token: string | undefined,\r\n conversationId: string,\r\n page?: number\r\n): Promise<APIResponse<Response>> {\r\n return await chatApi<Response>({\r\n endpoint: `${API}${ENDPOINTS.MESSAGES}/${conversationId}${page ? \"?page=\" + page : \"\"}`,\r\n body: {},\r\n method: \"GET\",\r\n token,\r\n });\r\n}\r\n\r\nexport async function GET_BROADCAST_LIST_MESSAGES<Response>(\r\n token: string | undefined,\r\n broadcastListId: string,\r\n page?: number\r\n): Promise<APIResponse<Response>> {\r\n return await chatApi<Response>({\r\n endpoint: `${API}${ENDPOINTS.BROADCAST_LIST}/messages/${broadcastListId}${page ? \"?page=\" + page : \"\"}`,\r\n body: {},\r\n method: \"GET\",\r\n token,\r\n });\r\n}\r\n\r\nexport async function GET_EMOJIS<Response>(\r\n token: string | undefined\r\n): Promise<APIResponse<Response>> {\r\n return await chatApi<Response>({\r\n endpoint: `${API}${ENDPOINTS.EMOJIS}`,\r\n body: {},\r\n method: \"GET\",\r\n token,\r\n });\r\n}\r\n\r\nexport async function UPLOAD_MEDIA<Response>(\r\n token: string,\r\n data: Prettify<UploadContent>\r\n): Promise<APIResponse<Response>> {\r\n return await chatApi<Response>({\r\n endpoint: `${API}${ENDPOINTS.UPLOAD}`,\r\n body: data,\r\n method: \"POST\",\r\n token,\r\n });\r\n}\r\n\r\nexport async function GET_PRESIGNED_URL<Response>(\r\n token: string,\r\n data: Prettify<UploadContent & { mediaType: string, uid: string, ext: string }>\r\n): Promise<APIResponse<Response>> {\r\n return await chatApi<Response>({\r\n endpoint: `${API}${ENDPOINTS.GET_PRESIGNED_URL}`,\r\n body: data,\r\n method: \"POST\",\r\n token,\r\n });\r\n}\r\n\r\nexport async function UPLOAD_ATTACHMENT<Response>(\r\n token: string,\r\n data: any\r\n): Promise<APIResponse<Response>> {\r\n return await chatApi<Response>({\r\n endpoint: `${API}${ENDPOINTS.UPLOAD_ATTACHMENT}`,\r\n body: data,\r\n method: \"POST\",\r\n token,\r\n headers: { \"Content-Type\": \"multipart/form-data\" },\r\n });\r\n}\r\n","import Connection from \"./Connection\";\r\nimport {\r\n ClientActions,\r\n Conversation,\r\n ConversationListItem,\r\n ConversationType,\r\n ConversationWithTypingIndicator,\r\n DeletedMessage,\r\n EditedMessage,\r\n MediaType,\r\n Message,\r\n MessageStates,\r\n Prettify,\r\n Reaction,\r\n ReadMessages,\r\n Screens,\r\n SendMessageGenerics,\r\n ServerActions,\r\n UserMeta,\r\n WsPayLoad,\r\n} from \"./types\";\r\nimport {\r\n generateConversationId,\r\n generateFillerTimestamps,\r\n generateId,\r\n} from \"./utils\";\r\nimport { Events } from \"./events\";\r\nimport {\r\n GET_BROADCAST_LIST_MESSAGES,\r\n GET_CONVERSATION,\r\n GET_EMOJIS,\r\n GET_MESSAGES,\r\n GET_PRESIGNED_URL,\r\n UPLOAD_ATTACHMENT,\r\n UPLOAD_MEDIA,\r\n} from \"./fetch\";\r\nimport { Emoticon } from \"./emoticon.type\";\r\nimport moment from \"moment\";\r\nimport { Readable } from \"stream\";\r\n\r\nlet CLEAR_UNREAD_TIMEOUT = 1000;\r\n\r\nexport default class MessageClient {\r\n private static message_client: MessageClient;\r\n private connection: Connection;\r\n // private currentConversationId: string;\r\n private screen: Screens;\r\n private idleTimers: { [key: string]: NodeJS.Timeout | undefined };\r\n\r\n constructor(connection: Connection, conversationId: string) {\r\n this.connection = connection;\r\n this.idleTimers = {};\r\n // this.connection.activeConversationId = conversationId;\r\n this.screen = Screens.CONVERSATIONS;\r\n }\r\n\r\n static getInstace(connection: Connection, conversationId: string) {\r\n if (MessageClient.message_client) {\r\n if (conversationId) {\r\n MessageClient.message_client.connection.activeConversationId =\r\n conversationId;\r\n }\r\n return MessageClient.message_client;\r\n } else {\r\n MessageClient.message_client = new MessageClient(\r\n connection,\r\n conversationId\r\n );\r\n return MessageClient.message_client;\r\n }\r\n }\r\n\r\n private getPublicMethods() {\r\n return {\r\n getMessages: this.getMessages.bind(this),\r\n sendMessage: this.sendMessage.bind(this),\r\n editMessage: this.editMessage.bind(this),\r\n sendTypingNotification: this.sendTypingNotification.bind(this),\r\n reactToMessage: this.reactToMessage.bind(this),\r\n uploadAttachment: this.uploadAttachment.bind(this),\r\n };\r\n }\r\n\r\n private getConversationType(conversationId?: string) {\r\n // const conversationMeta = this.connection.conversationListMeta[conversationId? conversationId : this.connection.activeConversationId];\r\n try {\r\n const conversationMeta =\r\n this.connection.conversationListMeta[\r\n conversationId ? conversationId : this.connection.activeConversationId\r\n ];\r\n return conversationMeta.conversation.conversationType;\r\n } catch (error) {\r\n return \"private-chat\";\r\n }\r\n }\r\n\r\n // removes the last item in the list and adds a new one to the end keeping the original length\r\n private rotateAndInsertMessageList = (\r\n messageList: Array<Message>,\r\n message: Message\r\n ) => {\r\n var list = [...messageList];\r\n if (messageList.length >= 25) {\r\n var list = [...messageList];\r\n list.unshift();\r\n list.push(message);\r\n return list;\r\n }\r\n list.push(message);\r\n return list;\r\n };\r\n\r\n private _createMessage(newMessage: Message) {\r\n try {\r\n if (newMessage) {\r\n const socketMessage = {\r\n action: ServerActions.SEND_MESSAGE,\r\n message: {\r\n messageId: newMessage.messageId,\r\n from: this.connection.userMeta.uid,\r\n to: newMessage.to,\r\n conversationType: this.getConversationType(),\r\n message: { ...newMessage, messageState: MessageStates.SENT },\r\n user: this.connection.userMeta,\r\n token: this.connection.wsAccessConfig.token,\r\n },\r\n };\r\n this.connection.emit(Events.NEW_MESSAGE, {\r\n message: {\r\n ...newMessage,\r\n reactions: [],\r\n messageState: MessageStates.LOADING,\r\n },\r\n });\r\n if (\r\n this.connection.socket &&\r\n this.connection.socket?.readyState === WebSocket.OPEN\r\n ) {\r\n this.connection.socket.send(JSON.stringify(socketMessage));\r\n this.connection.emit(Events.EDITED_MESSAGE, {\r\n message: {\r\n ...newMessage,\r\n reactions: [],\r\n messageState: MessageStates.SENT,\r\n },\r\n });\r\n var conversationMeta =\r\n this.connection.conversationListMeta[newMessage.conversationId];\r\n\r\n var updatedMessages = this.rotateAndInsertMessageList(\r\n conversationMeta.conversation.messages,\r\n {\r\n ...newMessage,\r\n reactions: [],\r\n messageState: MessageStates.SENT,\r\n }\r\n );\r\n const unread = conversationMeta.unread;\r\n this.connection.conversationListMeta[newMessage.conversationId] = {\r\n conversation: {\r\n ...conversationMeta.conversation,\r\n messages: updatedMessages,\r\n },\r\n lastMessage: {\r\n ...newMessage,\r\n },\r\n unread: unread,\r\n };\r\n this.connection.emit(Events.CONVERSATION_LIST_META_CHANGED, {\r\n conversationListMeta: this.connection.conversationListMeta,\r\n });\r\n } else {\r\n this.connection.emit(Events.EDITED_MESSAGE, {\r\n message: {\r\n ...newMessage,\r\n reactions: [],\r\n messageState: MessageStates.FAILED,\r\n },\r\n });\r\n this.connection.conversationListMeta[newMessage.conversationId] = {\r\n conversation:\r\n this.connection.conversationListMeta[newMessage.conversationId]\r\n .conversation,\r\n lastMessage: {\r\n ...newMessage,\r\n messageState: MessageStates.FAILED,\r\n },\r\n unread: [],\r\n };\r\n this.connection.emit(Events.CONVERSATION_LIST_META_CHANGED, {\r\n conversationListMeta: this.connection.conversationListMeta,\r\n });\r\n }\r\n }\r\n } catch (error) {\r\n if (error instanceof Error) {\r\n console.error(error);\r\n this.connection.emit(Events.DELETED_MESSAGE, {\r\n message: {\r\n ...newMessage,\r\n reactions: [],\r\n messageState: MessageStates.FAILED,\r\n },\r\n });\r\n }\r\n }\r\n }\r\n\r\n private editConversationListMetaMessage({ isBroadcast, updatedMessage } : { isBroadcast: boolean, updatedMessage: any }) {\r\n var conversationMeta = isBroadcast? this.connection.broadcastListMeta[updatedMessage.conversationId] : this.connection.conversationListMeta[updatedMessage.conversationId];\r\n var messageId = updatedMessage.messageId;\r\n if (conversationMeta) {\r\n // const socketMessage = {\r\n // action: ServerActions.EDIT_MESSAGE,\r\n // message: {\r\n // ...updatedMessage,\r\n // token: this.connection.wsAccessConfig.token,\r\n // },\r\n // };\r\n // this.connection.socket.send(JSON.stringify(socketMessage));\r\n var prevMessage = conversationMeta.conversation.messages.find(\r\n (m) => m.messageId === messageId\r\n );\r\n if (prevMessage) {\r\n var editedMessage = {\r\n ...prevMessage,\r\n message: updatedMessage.textMessage,\r\n lastEdited: new Date(),\r\n };\r\n var updatedMessageList = conversationMeta.conversation.messages.map(\r\n (m) => {\r\n if (m.messageId === messageId) {\r\n return editedMessage;\r\n }\r\n return m;\r\n }\r\n );\r\n \r\n if(isBroadcast){\r\n this.connection.broadcastListMeta[updatedMessage.conversationId] = {\r\n conversation: {\r\n ...conversationMeta.conversation,\r\n messages: updatedMessageList,\r\n },\r\n lastMessage: null,\r\n unread: [],\r\n };\r\n this.connection.emit(Events.BROADCAST_LIST_META_CHANGED, {\r\n broadcastListMeta: this.connection.broadcastListMeta,\r\n });\r\n }else{\r\n this.connection.conversationListMeta[updatedMessage.conversationId] = {\r\n conversation: {\r\n ...conversationMeta.conversation,\r\n messages: updatedMessageList,\r\n },\r\n lastMessage: { ...editedMessage },\r\n unread: conversationMeta.unread,\r\n };\r\n this.connection.emit(Events.CONVERSATION_LIST_META_CHANGED, {\r\n conversationListMeta: this.connection.conversationListMeta,\r\n });\r\n }\r\n }\r\n }\r\n }\r\n\r\n private _editMessage(message: EditedMessage) {\r\n try {\r\n if (message) {\r\n let updatedMessage = {\r\n ...message,\r\n message: message.textMessage,\r\n lastEdited: new Date(),\r\n };\r\n this.connection.emit(Events.EDITED_MESSAGE, {\r\n message: updatedMessage,\r\n });\r\n this.editConversationListMetaMessage({\r\n isBroadcast: false, \r\n updatedMessage \r\n });\r\n\r\n // update both lists !!might need improvement, maybe go back to using a single list.\r\n this.editConversationListMetaMessage({\r\n isBroadcast: true, \r\n updatedMessage \r\n });\r\n \r\n if(this.connection.socket){\r\n const socketMessage = {\r\n action: ServerActions.EDIT_MESSAGE,\r\n message: {\r\n ...updatedMessage,\r\n token: this.connection.wsAccessConfig.token,\r\n isBroadcast: message.isBroadcast,\r\n user: this.connection.userMeta\r\n },\r\n };\r\n this.connection.socket.send(JSON.stringify(socketMessage));\r\n }\r\n // if(this.connection.socket){\r\n // const socketMessage = {\r\n // action: ServerActions.EDIT_MESSAGE,\r\n // message: {\r\n // ...updatedMessage,\r\n // token: this.connection.wsAccessConfig.token,\r\n // },\r\n // };\r\n // console.log(socketMessage)\r\n // this.connection.socket.send(JSON.stringify(socketMessage));\r\n // }\r\n // if (conversationMeta && this.connection.socket) {\r\n // const socketMessage = {\r\n // action: ServerActions.EDIT_MESSAGE,\r\n // message: {\r\n // ...updatedMessage,\r\n // token: this.connection.wsAccessConfig.token,\r\n // },\r\n // };\r\n // this.connection.socket.send(JSON.stringify(socketMessage));\r\n // var prevMessage = conversationMeta.conversation.messages.find(\r\n // (m) => m.messageId === message.messageId\r\n // );\r\n // if (prevMessage) {\r\n // var editedMessage = {\r\n // ...prevMessage,\r\n // message: message.textMessage,\r\n // lastEdited: new Date(),\r\n // };\r\n // var updatedMessageList = conversationMeta.conversation.messages.map(\r\n // (m) => {\r\n // if (m.messageId === message.messageId) {\r\n // return editedMessage;\r\n // }\r\n // return m;\r\n // }\r\n // );\r\n // this.connection.conversationListMeta[message.conversationId] = {\r\n // conversation: {\r\n // ...conversationMeta.conversation,\r\n // messages: updatedMessageList,\r\n // },\r\n // lastMessage: { ...editedMessage },\r\n // unread: conversationMeta.unread,\r\n // };\r\n // this.connection.emit(Events.CONVERSATION_LIST_META_CHANGED, {\r\n // conversationListMeta: this.connection.conversationListMeta,\r\n // });\r\n // // update message copy on broadcast chat screen\r\n // this.connection.emit(Events.BROADCAST_LIST_META_CHANGED, {\r\n // broadcastListMeta: this.connection.broadcastListMeta,\r\n // });\r\n // }\r\n // }\r\n }\r\n } catch (error) {\r\n // maybe an error listener instead\r\n console.error(error);\r\n }\r\n }\r\n\r\n _updateMessageReactions(\r\n conversationId: string,\r\n messageId: string,\r\n reactions: Reaction[],\r\n config?: { ws: boolean; to: string }\r\n ) {\r\n try {\r\n this.connection.emit(Events.EDITED_MESSAGE, {\r\n message: { messageId, reactions },\r\n });\r\n // should also send to the lastMessage in conversations meta.\r\n if (config?.ws) {\r\n const reactionPayload = {\r\n action: ServerActions.SEND_MESSAGE_REACTION,\r\n message: {\r\n conversationId,\r\n messageId: messageId,\r\n from: this.connection.userMeta.uid,\r\n to: config.to,\r\n reactions,\r\n token: this.connection.wsAccessConfig.token,\r\n user: this.connection.userMeta\r\n },\r\n };\r\n this.connection.socket.send(JSON.stringify(reactionPayload));\r\n }\r\n\r\n var conversationMeta =\r\n this.connection.conversationListMeta[conversationId];\r\n var prevLastMessage = conversationMeta?.lastMessage;\r\n\r\n if (prevLastMessage && prevLastMessage.messageId === messageId) {\r\n var updatedMessage = {\r\n ...prevLastMessage,\r\n reactions,\r\n };\r\n var updatedMessages = this.rotateAndInsertMessageList(\r\n conversationMeta.conversation.messages,\r\n updatedMessage\r\n );\r\n\r\n var updatedConversationListMeta = {\r\n conversation: {\r\n ...conversationMeta.conversation,\r\n messages: updatedMessages,\r\n },\r\n unread: conversationMeta.unread,\r\n lastMessage: {\r\n ...updatedMessage,\r\n },\r\n };\r\n this.connection.conversationListMeta[conversationId] =\r\n updatedConversationListMeta;\r\n this.connection.emit(Events.CONVERSATION_LIST_META_CHANGED, {\r\n conversationListMeta: this.connection.conversationListMeta,\r\n });\r\n }\r\n } catch (error) {\r\n if (error instanceof Error) {\r\n }\r\n }\r\n }\r\n\r\n private storeEditedMessage(data: EditedMessage) {\r\n const conversation = this.connection.conversationMap[data.conversationId];\r\n // const conversation = this.connection.conversationMap[data.conversationId];\r\n if (conversation) {\r\n var message = conversation.messages.find(\r\n (message) => message.messageId === data.messageId\r\n );\r\n if (message) {\r\n const updatedMessage = {\r\n ...message,\r\n message: data.textMessage,\r\n lastEdited: new Date(),\r\n };\r\n this.connection.emit(Events.EDITED_MESSAGE, {\r\n message: updatedMessage,\r\n });\r\n }\r\n }\r\n }\r\n\r\n /**\r\n *\r\n * @param conversationId\r\n * @summary Updates the list of conversations with typing indicator flags\r\n */\r\n private showTypingIndicator = (\r\n conversationId: string,\r\n action: \"START\" | \"STOP\"\r\n ) => {\r\n if (action === \"START\") {\r\n this.connection.emit(Events.HAS_STARTED_TYPING, {\r\n conversationId,\r\n });\r\n } else {\r\n this._clearActiveTypingIndicator(conversationId);\r\n this.idleTimers[conversationId] = setTimeout(() => {\r\n this.connection.emit(Events.HAS_STOPPED_TYPING, {\r\n conversationId,\r\n });\r\n }, 4000);\r\n }\r\n };\r\n // sent to message recipient\r\n private _sendTypingNotification(uid: string) {\r\n if (this.connection.socket) {\r\n this.connection.socket.send(\r\n JSON.stringify({\r\n action: ServerActions.USER_TYPING,\r\n message: {\r\n uid,\r\n conversationId: this.connection.activeConversationId,\r\n action: \"START\",\r\n conversationType: this.getConversationType(\r\n this.connection.activeConversationId\r\n ),\r\n user: this.connection.userMeta,\r\n token: this.connection.wsAccessConfig.token,\r\n },\r\n })\r\n );\r\n }\r\n }\r\n\r\n private _sendStoppedTypingNotification(uid: string) {\r\n if (this.connection.socket) {\r\n this.connection.socket.send(\r\n JSON.stringify({\r\n action: ServerActions.USER_TYPING,\r\n message: {\r\n uid,\r\n conversationId: this.connection.activeConversationId,\r\n action: \"STOP\",\r\n conversationType: this.getConversationType(\r\n this.connection.activeConversationId\r\n ),\r\n user: this.connection.userMeta,\r\n token: this.connection.wsAccessConfig.token,\r\n },\r\n })\r\n );\r\n }\r\n }\r\n\r\n private addMessageToConversation(newMessage: Message, screen: string) {\r\n try {\r\n const conversation =\r\n this.connection.conversationMap[newMessage.conversationId];\r\n if (conversation) {\r\n const updatedMessages = [\r\n ...conversation.messages,\r\n {\r\n ...newMessage,\r\n messageState:\r\n screen === Screens.CHAT ? MessageStates.READ : MessageStates.SENT,\r\n },\r\n ];\r\n\r\n this.connection.conversationMap[newMessage.conversationId] = {\r\n ...conversation,\r\n messages: updatedMessages,\r\n };\r\n this.connection.emit(Events.NEW_MESSAGE, {\r\n message: {\r\n ...newMessage,\r\n messageState:\r\n screen === Screens.CHAT ? MessageStates.READ : MessageStates.SENT,\r\n },\r\n });\r\n\r\n const unread = [\r\n ...this.connection.conversationListMeta[newMessage.conversationId]\r\n .unread,\r\n ];\r\n\r\n if (\r\n newMessage.conversationId !== this.connection.activeConversationId\r\n ) {\r\n unread.push(newMessage.messageId);\r\n }\r\n\r\n const updatedConversationListMeta = {\r\n conversation: {\r\n ...this.connection.conversationListMeta[newMessage.conversationId]\r\n .conversation,\r\n messages: updatedMessages,\r\n },\r\n lastMessage: {\r\n ...newMessage,\r\n },\r\n unread: unread,\r\n };\r\n this.connection.conversationListMeta[newMessage.conversationId] =\r\n updatedConversationListMeta;\r\n this.connection.emit(Events.CONVERSATION_LIST_META_CHANGED, {\r\n conversationListMeta: this.connection.conversationListMeta,\r\n });\r\n }\r\n } catch (error) {\r\n console.error(error);\r\n }\r\n }\r\n\r\n private sendReadNotification(data: ReadMessages) {\r\n if (this.connection.socket) {\r\n this.connection.socket.send(\r\n JSON.stringify({\r\n action: ServerActions.READ_MESSAGES,\r\n message: { \r\n ...data,\r\n user: this.connection.userMeta,\r\n token: this.connection.wsAccessConfig.token },\r\n })\r\n );\r\n }\r\n }\r\n\r\n private _clearActiveTypingIndicator(conversationId: string, emit?: boolean) {\r\n clearTimeout(this.idleTimers[conversationId]);\r\n delete this.idleTimers[conversationId];\r\n if (emit === true) {\r\n this.connection.emit(Events.HAS_STOPPED_TYPING, {\r\n conversationId,\r\n });\r\n }\r\n }\r\n\r\n private _updateConversationListMetaMessages() {}\r\n\r\n readMessages(conversationId: string, data: ReadMessages) {\r\n if (this.connection.socket) {\r\n const socketMessage = {\r\n action: ServerActions.READ_MESSAGES,\r\n message: {\r\n ...data,\r\n user: this.connection.userMeta,\r\n token: this.connection.wsAccessConfig.token,\r\n },\r\n };\r\n \r\n this.connection.socket.send(JSON.stringify(socketMessage));\r\n \r\n // Create an updated conversation list meta immutably\r\n const conversationMeta = this.connection.conversationListMeta[conversationId];\r\n if (conversationMeta) {\r\n const updatedConversationListMeta = {\r\n ...this.connection.conversationListMeta,\r\n [conversationId]: {\r\n ...conversationMeta,\r\n unread: [], // Clears unread without direct mutation\r\n },\r\n };\r\n \r\n this.connection.conversationListMeta = updatedConversationListMeta;\r\n \r\n this.connection.emit(Events.CONVERSATION_LIST_META_CHANGED, {\r\n conversationListMeta: updatedConversationListMeta,\r\n });\r\n }\r\n }\r\n }\r\n \r\n\r\n //this is to clear notifications for user who hasn't opened chat\r\n clearUserUnreadNotifications(conversationId: string, ids: string[]) {\r\n var i = 0;\r\n var len = ids.length;\r\n\r\n while (i < len) {\r\n this.connection.emit(Events.EDITED_MESSAGE, {\r\n message: { messageId: ids[i], messageState: MessageStates.READ },\r\n });\r\n i++;\r\n }\r\n }\r\n\r\n private wsOnError(error: CloseEvent) {\r\n this.connection.emit(Events.CONNECTION_CHANGED, {\r\n isConnected: false,\r\n connecting: false,\r\n fetchingConversations: false,\r\n });\r\n }\r\n\r\n private deleteMessageFromConversationMeta(\r\n conversationId: string,\r\n messageId: string\r\n ) {\r\n try {\r\n const conversationMeta =\r\n this.connection.conversationListMeta[conversationId];\r\n\r\n // check if the message being deleted is the last message\r\n var isLastMessage = messageId === conversationMeta.lastMessage?.messageId;\r\n\r\n if (conversationMeta) {\r\n const filteredMessages = conversationMeta.conversation.messages.filter(\r\n (m) => m.messageId !== messageId\r\n );\r\n\r\n var newLastMessage = filteredMessages[filteredMessages.length - 1];\r\n\r\n var updatedConversationListMeta = {\r\n ...conversationMeta,\r\n conversation: {\r\n ...conversationMeta.conversation,\r\n messages: filteredMessages,\r\n },\r\n };\r\n\r\n if (isLastMessage) {\r\n updatedConversationListMeta.lastMessage = newLastMessage\r\n ? newLastMessage\r\n : null;\r\n }\r\n\r\n this.connection.conversationListMeta[conversationId] =\r\n updatedConversationListMeta;\r\n\r\n this.connection.emit(Events.CONVERSATION_LIST_META_CHANGED, {\r\n conversationListMeta: this.connection.conversationListMeta,\r\n });\r\n } else {\r\n throw new Error(`Conversation with ID ${conversationId} not found.`);\r\n }\r\n } catch (error) {\r\n if (error instanceof Error) {\r\n console.error(error.message);\r\n }\r\n }\r\n }\r\n\r\n reactToMessage({\r\n conversationId,\r\n messageId,\r\n reactions,\r\n to,\r\n }: {\r