import json, strutils, sequtils, tables, chronicles, times, sugar, algorithm import statusgo_backend/chat as status_chat import statusgo_backend/contacts as status_contacts import statusgo_backend/chatCommands as status_chat_commands import types/[message, status_update, activity_center_notification, sticker, removed_message, profile] import types/chat as chat_type import utils as status_utils import stickers import ../eventemitter import contacts import chat/[chat, utils] import ens, accounts include utils/json_utils logScope: topics = "chat-model" const backToFirstChat* = "__goBackToFirstChat" const ZERO_ADDRESS* = "0x0000000000000000000000000000000000000000" proc buildFilter*(chat: Chat):JsonNode = if chat.chatType == ChatType.PrivateGroupChat: return newJNull() result = %* { "ChatID": chat.id, "OneToOne": chat.chatType == ChatType.OneToOne } type ChatUpdateArgs* = ref object of Args chats*: seq[Chat] messages*: seq[Message] pinnedMessages*: seq[Message] emojiReactions*: seq[Reaction] communities*: seq[Community] communityMembershipRequests*: seq[CommunityMembershipRequest] activityCenterNotifications*: seq[ActivityCenterNotification] statusUpdates*: seq[StatusUpdate] deletedMessages*: seq[RemovedMessage] ChatIdArg* = ref object of Args chatId*: string ChannelArgs* = ref object of Args chat*: Chat ChatArgs* = ref object of Args chats*: seq[Chat] CommunityActiveChangedArgs* = ref object of Args active*: bool MsgsLoadedArgs* = ref object of Args chatId*: string messages*: seq[Message] statusUpdates*: seq[StatusUpdate] ActivityCenterNotificationsArgs* = ref object of Args activityCenterNotifications*: seq[ActivityCenterNotification] ReactionsLoadedArgs* = ref object of Args reactions*: seq[Reaction] MessageArgs* = ref object of Args id*: string channel*: string MessageSendingSuccess* = ref object of Args chat*: Chat message*: Message MarkAsReadNotificationProperties* = ref object of Args communityId*: string channelId*: string notificationTypes*: seq[ActivityCenterNotificationType] MarkAsUnreadNotificationProperties* = ref object of Args communityId*: string channelId*: string notificationTypes*: seq[ActivityCenterNotificationType] type ChatModel* = ref object publicKey*: string events*: EventEmitter communitiesToFetch*: seq[string] mailserverReady*: bool channels*: Table[string, Chat] msgCursor: Table[string, string] pinnedMsgCursor: Table[string, string] activityCenterCursor*: string emojiCursor: Table[string, string] lastMessageTimestamps*: Table[string, int64] proc newChatModel*(events: EventEmitter): ChatModel = result = ChatModel() result.events = events result.mailserverReady = false result.communitiesToFetch = @[] result.channels = initTable[string, Chat]() result.msgCursor = initTable[string, string]() result.pinnedMsgCursor = initTable[string, string]() result.emojiCursor = initTable[string, string]() result.lastMessageTimestamps = initTable[string, int64]() proc getContacts*(self: ChatModel): Table[string, Profile] = let (index, usedCache) = status_contacts.getContactsIndex() if not usedCache: let (contacts, _) = status_contacts.getContacts() self.events.emit("contactUpdate", ContactUpdateArgs(contacts: contacts)) return index proc update*(self: ChatModel, chats: seq[Chat], messages: seq[Message], emojiReactions: seq[Reaction], communities: seq[Community], communityMembershipRequests: seq[CommunityMembershipRequest], pinnedMessages: seq[Message], activityCenterNotifications: seq[ActivityCenterNotification], statusUpdates: seq[StatusUpdate], deletedMessages: seq[RemovedMessage]) = for chat in chats: self.channels[chat.id] = chat for message in messages: let chatId = message.chatId let ts = times.convert(Milliseconds, Seconds, message.whisperTimestamp.parseInt()) if not self.lastMessageTimestamps.hasKey(chatId): self.lastMessageTimestamps[chatId] = ts else: if self.lastMessageTimestamps[chatId] > ts: self.lastMessageTimestamps[chatId] = ts self.events.emit("chatUpdate", ChatUpdateArgs(messages: messages,chats: chats, emojiReactions: emojiReactions, communities: communities, communityMembershipRequests: communityMembershipRequests, pinnedMessages: pinnedMessages, activityCenterNotifications: activityCenterNotifications, statusUpdates: statusUpdates, deletedMessages: deletedMessages)) proc parseChatResponse(self: ChatModel, response: string): (seq[Chat], seq[Message]) = var parsedResponse = parseJson(response) var chats: seq[Chat] = @[] var messages: seq[Message] = @[] if parsedResponse{"result"}{"messages"} != nil: for jsonMsg in parsedResponse["result"]["messages"]: messages.add(jsonMsg.toMessage()) if parsedResponse{"result"}{"chats"} != nil: for jsonChat in parsedResponse["result"]["chats"]: let chat = jsonChat.toChat self.channels[chat.id] = chat chats.add(chat) result = (chats, messages) proc emitUpdate(self: ChatModel, response: string) = var (chats, messages) = self.parseChatResponse(response) self.events.emit("chatUpdate", ChatUpdateArgs(messages: messages, chats: chats)) proc removeFiltersByChatId(self: ChatModel, chatId: string, filters: JsonNode) proc removeChatFilters(self: ChatModel, chatId: string) = # TODO: this code should be handled by status-go / stimbus instead of the client # Clients should not have to care about filters. For more info about filters: # https://github.com/status-im/specs/blob/master/docs/stable/3-whisper-usage.md#keys-management let filters = parseJson(status_chat.loadFilters(@[]))["result"] case self.channels[chatId].chatType of ChatType.Public: for filter in filters: if filter["chatId"].getStr == chatId: status_chat.removeFilters(chatId, filter["filterId"].getStr) of ChatType.OneToOne, ChatType.Profile: # Check if user does not belong to any active chat group var inGroup = false for channel in self.channels.values: if channel.isActive and channel.id != chatId and channel.chatType == ChatType.PrivateGroupChat: inGroup = true break if not inGroup: self.removeFiltersByChatId(chatId, filters) of ChatType.PrivateGroupChat: for member in self.channels[chatId].members: # Check that any of the members are not in other active group chats, or that you don’t have a one-to-one open. var hasConversation = false for channel in self.channels.values: if (channel.isActive and channel.chatType == ChatType.OneToOne and channel.id == member.id) or (channel.isActive and channel.id != chatId and channel.chatType == ChatType.PrivateGroupChat and channel.isMember(member.id)): hasConversation = true break if not hasConversation: self.removeFiltersByChatId(member.id, filters) else: error "Unknown chat type removed", chatId proc removeFiltersByChatId(self: ChatModel, chatId: string, filters: JsonNode) = var partitionedTopic = "" for filter in filters: # Contact code filter should be removed if filter["identity"].getStr == chatId and filter["chatId"].getStr.endsWith("-contact-code"): status_chat.removeFilters(chatId, filter["filterId"].getStr) # Remove partitioned topic if no other user in an active group chat or one-to-one is from the # same partitioned topic if filter["identity"].getStr == chatId and filter["chatId"].getStr.startsWith("contact-discovery-"): partitionedTopic = filter["topic"].getStr var samePartitionedTopic = false for f in filters.filterIt(it["topic"].getStr == partitionedTopic and it["filterId"].getStr != filter["filterId"].getStr): let fIdentity = f["identity"].getStr; if self.channels.hasKey(fIdentity) and self.channels[fIdentity].isActive: samePartitionedTopic = true break if not samePartitionedTopic: status_chat.removeFilters(chatId, filter["filterId"].getStr) proc hasChannel*(self: ChatModel, chatId: string): bool = self.channels.hasKey(chatId) proc getActiveChannel*(self: ChatModel): string = if (self.channels.len == 0): "" else: toSeq(self.channels.values)[self.channels.len - 1].id proc emitTopicAndJoin(self: ChatModel, chat: Chat) = let filterResult = status_chat.loadFilters(@[buildFilter(chat)]) self.events.emit("channelJoined", ChannelArgs(chat: chat)) proc join*(self: ChatModel, chatId: string, chatType: ChatType, ensName: string = "", pubKey: string = "") = if self.hasChannel(chatId): return var chat = newChat(chatId, ChatType(chatType)) self.channels[chat.id] = chat status_chat.saveChat(chatId, chatType, color=chat.color, ensName=ensName, profile=pubKey) self.emitTopicAndJoin(chat) proc createOneToOneChat*(self: ChatModel, publicKey: string, ensName: string = "") = if self.hasChannel(publicKey): # We want to show the chat to the user and for that we activate the chat status_chat.saveChat(publicKey, ChatType.OneToOne, color=self.channels[publicKey].color, ensName=ensName) self.emitTopicAndJoin(self.channels[publicKey]) return var chat = newChat(publicKey, ChatType.OneToOne) if ensName != "": chat.name = ensName chat.ensName = ensName self.channels[chat.id] = chat discard status_chat.createOneToOneChat(publicKey) self.emitTopicAndJoin(chat) proc createPublicChat*(self: ChatModel, chatId: string) = if self.hasChannel(chatId): return var chat = newChat(chatId, ChatType.Public) self.channels[chat.id] = chat discard status_chat.createPublicChat(chatId) self.emitTopicAndJoin(chat) proc requestMissingCommunityInfos*(self: ChatModel) = if (self.communitiesToFetch.len == 0): return for communityId in self.communitiesToFetch: status_chat.requestCommunityInfo(communityId) proc sortChats(x, y: chat_type.Chat): int = var t1 = x.lastMessage.whisperTimestamp var t2 = y.lastMessage.whisperTimestamp if t1 <= $x.joined: t1 = $x.joined if t2 <= $y.joined: t2 = $y.joined if t1 > t2: 1 elif t1 == t2: 0 else: -1 proc init*(self: ChatModel, pubKey: string) = self.publicKey = pubKey var (contacts, _) = status_contacts.getContacts() contacts = contacts.filter(c => c.added) var chatList = status_chat.loadChats() chatList.sort(sortChats) let profileUpdatesChatIds = chatList.filter(c => c.chatType == ChatType.Profile).map(c => c.id) if chatList.filter(c => c.chatType == ChatType.Timeline).len == 0: var timelineChannel = newChat(status_utils.getTimelineChatId(), ChatType.Timeline) self.join(timelineChannel.id, timelineChannel.chatType) chatList.add(timelineChannel) let timelineChatId = status_utils.getTimelineChatId(pubKey) if not profileUpdatesChatIds.contains(timelineChatId): var profileUpdateChannel = newChat(timelineChatId, ChatType.Profile) status_chat.saveChat(profileUpdateChannel.id, profileUpdateChannel.chatType, profile=pubKey) chatList.add(profileUpdateChannel) # For profile updates and timeline, we have to make sure that for # each added contact, a chat has been saved for the currently logged-in # user. Users that will use a version of Status with timeline support for the # first time, won't have any of those otherwise. if profileUpdatesChatIds.filter(id => id != timelineChatId).len != contacts.len: for contact in contacts: if not profileUpdatesChatIds.contains(status_utils.getTimelineChatId(contact.address)): let profileUpdatesChannel = newChat(status_utils.getTimelineChatId(contact.address), ChatType.Profile) status_chat.saveChat(profileUpdatesChannel.id, profileUpdatesChannel.chatType, ensName=contact.ensName, profile=contact.address) chatList.add(profileUpdatesChannel) var filters:seq[JsonNode] = @[] for chat in chatList: if self.hasChannel(chat.id): continue filters.add buildFilter(chat) self.channels[chat.id] = chat self.events.emit("channelLoaded", ChannelArgs(chat: chat)) if filters.len == 0: return let filterResult = status_chat.loadFilters(filters) self.events.emit("chatsLoaded", ChatArgs(chats: chatList)) self.events.once("mailserverAvailable") do(a: Args): self.mailserverReady = true self.requestMissingCommunityInfos() proc statusUpdates*(self: ChatModel) = let statusUpdates = status_chat.statusUpdates() self.events.emit("messagesLoaded", MsgsLoadedArgs(statusUpdates: statusUpdates)) proc leave*(self: ChatModel, chatId: string) = self.removeChatFilters(chatId) if self.channels[chatId].chatType == ChatType.PrivateGroupChat: let leaveGroupResponse = status_chat.leaveGroupChat(chatId) self.emitUpdate(leaveGroupResponse) discard status_chat.deactivateChat(self.channels[chatId]) self.channels.del(chatId) discard status_chat.clearChatHistory(chatId) self.events.emit("channelLeft", ChatIdArg(chatId: chatId)) proc clearHistory*(self: ChatModel, chatId: string) = discard status_chat.clearChatHistory(chatId) let chat = self.channels[chatId] self.events.emit("chatHistoryCleared", ChannelArgs(chat: chat)) proc setActiveChannel*(self: ChatModel, chatId: string) = self.events.emit("activeChannelChanged", ChatIdArg(chatId: chatId)) proc processMessageUpdateAfterSend(self: ChatModel, response: string): (seq[Chat], seq[Message]) = result = self.parseChatResponse(response) var (chats, messages) = result if chats.len == 0 and messages.len == 0: self.events.emit("messageSendingFailed", Args()) return # This fixes issue#3490 var msg = messages[0] for m in messages: if(m.responseTo.len > 0): msg = m break self.events.emit("messageSendingSuccess", MessageSendingSuccess(message: msg, chat: chats[0])) proc sendMessage*(self: ChatModel, chatId: string, msg: string, replyTo: string = "", contentType: int = ContentType.Message.int, communityId: string = "") = var response = status_chat.sendChatMessage(chatId, msg, replyTo, contentType, communityId) discard self.processMessageUpdateAfterSend(response) proc editMessage*(self: ChatModel, messageId: string, msg: string) = var response = status_chat.editMessage(messageId, msg) discard self.processMessageUpdateAfterSend(response) proc sendAudio*(self: ChatModel, chatId: string, audioBase64: string, durationMs: uint64) = var response = status_chat.sendAudioMessage(chatId, audioBase64, durationMs) discard self.processMessageUpdateAfterSend(response) proc sendImage*(self: ChatModel, chatId: string, image: string) = var response = status_chat.sendImageMessage(chatId, image) discard self.processMessageUpdateAfterSend(response) proc sendImages*(self: ChatModel, chatId: string, images: var seq[string]) = var response = status_chat.sendImageMessages(chatId, images) discard self.processMessageUpdateAfterSend(response) proc deleteMessageAndSend*(self: ChatModel, messageId: string) = discard status_chat.deleteMessageAndSend(messageId) proc sendSticker*(self: ChatModel, chatId: string, replyTo: string, sticker: Sticker) = var response = status_chat.sendStickerMessage(chatId, replyTo, sticker) self.events.emit("stickerSent", StickerArgs(sticker: sticker, save: true)) var (chats, messages) = self.parseChatResponse(response) self.events.emit("chatUpdate", ChatUpdateArgs(messages: messages, chats: chats)) self.events.emit("sendingMessage", MessageArgs(id: messages[0].id, channel: messages[0].chatId)) proc addEmojiReaction*(self: ChatModel, chatId: string, messageId: string, emojiId: int) = let reactions = status_chat.addEmojiReaction(chatId, messageId, emojiId) self.events.emit("reactionsLoaded", ReactionsLoadedArgs(reactions: reactions)) proc removeEmojiReaction*(self: ChatModel, emojiReactionId: string) = let reactions = status_chat.removeEmojiReaction(emojiReactionId) self.events.emit("reactionsLoaded", ReactionsLoadedArgs(reactions: reactions)) proc onMarkMessagesRead(self: ChatModel, response: string, chatId: string): JsonNode = result = parseJson(response) if self.channels.hasKey(chatId): self.channels[chatId].unviewedMessagesCount = 0 self.channels[chatId].mentionsCount = 0 self.events.emit("channelUpdate", ChatUpdateArgs(messages: @[], chats: @[self.channels[chatId]])) proc onAsyncMarkMessagesRead*(self: ChatModel, response: string) = let parsedResponse = parseJson(response) discard self.onMarkMessagesRead(parsedResponse{"response"}.getStr, parsedResponse{"chatId"}.getStr) proc markAllChannelMessagesRead*(self: ChatModel, chatId: string): JsonNode = var response = status_chat.markAllRead(chatId) return self.onMarkMessagesRead(response, chatId) proc markMessagesSeen*(self: ChatModel, chatId: string, messageIds: seq[string]): JsonNode = var response = status_chat.markMessagesSeen(chatId, messageIds) return self.onMarkMessagesRead(response, chatId) proc confirmJoiningGroup*(self: ChatModel, chatId: string) = var response = status_chat.confirmJoiningGroup(chatId) self.emitUpdate(response) proc renameGroup*(self: ChatModel, chatId: string, newName: string) = var response = status_chat.renameGroup(chatId, newName) self.emitUpdate(response) proc getUserName*(self: ChatModel, id: string, defaultUserName: string):string = let contacts = self.getContacts() if(contacts.hasKey(id)): return userNameOrAlias(contacts[id]) else: return defaultUserName proc processGroupChatCreation*(self: ChatModel, result: string) = var response = parseJson(result) var (chats, messages) = formatChatUpdate(response) let chat = chats[0] self.channels[chat.id] = chat self.events.emit("chatUpdate", ChatUpdateArgs(messages: messages, chats: chats)) self.events.emit("activeChannelChanged", ChatIdArg(chatId: chat.id)) proc createGroup*(self: ChatModel, groupName: string, pubKeys: seq[string]) = var result = status_chat.createGroup(groupName, pubKeys) self.processGroupChatCreation(result) proc createGroupChatFromInvitation*(self: ChatModel, groupName: string, chatID: string, adminPK: string) = var result = status_chat.createGroupChatFromInvitation(groupName, chatID, adminPK) self.processGroupChatCreation(result) proc addGroupMembers*(self: ChatModel, chatId: string, pubKeys: seq[string]) = var response = status_chat.addGroupMembers(chatId, pubKeys) self.emitUpdate(response) proc kickGroupMember*(self: ChatModel, chatId: string, pubKey: string) = var response = status_chat.kickGroupMember(chatId, pubKey) self.emitUpdate(response) proc makeAdmin*(self: ChatModel, chatId: string, pubKey: string) = var response = status_chat.makeAdmin(chatId, pubKey) self.emitUpdate(response) proc resendMessage*(self: ChatModel, messageId: string) = discard status_chat.reSendChatMessage(messageId) proc muteChat*(self: ChatModel, chat: Chat) = discard status_chat.muteChat(chat.id) self.events.emit("chatUpdate", ChatUpdateArgs(messages: @[], chats: @[chat])) proc unmuteChat*(self: ChatModel, chat: Chat) = discard status_chat.unmuteChat(chat.id) self.events.emit("chatUpdate", ChatUpdateArgs(messages: @[], chats: @[chat])) proc processUpdateForTransaction*(self: ChatModel, messageId: string, response: string) = var (chats, messages) = self.processMessageUpdateAfterSend(response) self.events.emit("messageDeleted", MessageArgs(id: messageId, channel: chats[0].id)) proc acceptRequestAddressForTransaction*(self: ChatModel, messageId: string, address: string) = let response = status_chat_commands.acceptRequestAddressForTransaction(messageId, address) self.processUpdateForTransaction(messageId, response) proc declineRequestAddressForTransaction*(self: ChatModel, messageId: string) = let response = status_chat_commands.declineRequestAddressForTransaction(messageId) self.processUpdateForTransaction(messageId, response) proc declineRequestTransaction*(self: ChatModel, messageId: string) = let response = status_chat_commands.declineRequestTransaction(messageId) self.processUpdateForTransaction(messageId, response) proc requestAddressForTransaction*(self: ChatModel, chatId: string, fromAddress: string, amount: string, tokenAddress: string) = let address = if (tokenAddress == ZERO_ADDRESS): "" else: tokenAddress let response = status_chat_commands.requestAddressForTransaction(chatId, fromAddress, amount, address) discard self.processMessageUpdateAfterSend(response) proc acceptRequestTransaction*(self: ChatModel, transactionHash: string, messageId: string, signature: string) = let response = status_chat_commands.acceptRequestTransaction(transactionHash, messageId, signature) discard self.processMessageUpdateAfterSend(response) proc requestTransaction*(self: ChatModel, chatId: string, fromAddress: string, amount: string, tokenAddress: string) = let address = if (tokenAddress == ZERO_ADDRESS): "" else: tokenAddress let response = status_chat_commands.requestTransaction(chatId, fromAddress, amount, address) discard self.processMessageUpdateAfterSend(response) proc getAllComunities*(self: ChatModel): seq[Community] = result = status_chat.getAllComunities() proc getJoinedComunities*(self: ChatModel): seq[Community] = result = status_chat.getJoinedComunities() proc createCommunity*(self: ChatModel, name: string, description: string, access: int, ensOnly: bool, color: string, imageUrl: string, aX: int, aY: int, bX: int, bY: int): Community = result = status_chat.createCommunity(name, description, access, ensOnly, color, imageUrl, aX, aY, bX, bY) proc editCommunity*(self: ChatModel, id: string, name: string, description: string, access: int, ensOnly: bool, color: string, imageUrl: string, aX: int, aY: int, bX: int, bY: int): Community = result = status_chat.editCommunity(id, name, description, access, ensOnly, color, imageUrl, aX, aY, bX, bY) proc createCommunityChannel*(self: ChatModel, communityId: string, name: string, description: string): Chat = result = status_chat.createCommunityChannel(communityId, name, description) proc editCommunityChannel*(self: ChatModel, communityId: string, channelId: string, name: string, description: string, categoryId: string, position: int): Chat = result = status_chat.editCommunityChannel(communityId, channelId, name, description, categoryId, position) proc deleteCommunityChat*(self: ChatModel, communityId: string, channelId: string) = status_chat.deleteCommunityChat(communityId, channelId) proc reorderCommunityCategories*(self: ChatModel, communityId: string, categoryId: string, position: int) = status_chat.reorderCommunityCategories(communityId, categoryId, position) proc createCommunityCategory*(self: ChatModel, communityId: string, name: string, channels: seq[string]): CommunityCategory = result = status_chat.createCommunityCategory(communityId, name, channels) proc editCommunityCategory*(self: ChatModel, communityId: string, categoryId: string, name: string, channels: seq[string]) = status_chat.editCommunityCategory(communityId, categoryId, name, channels) proc deleteCommunityCategory*(self: ChatModel, communityId: string, categoryId: string) = status_chat.deleteCommunityCategory(communityId, categoryId) proc reorderCommunityChannel*(self: ChatModel, communityId: string, categoryId: string, chatId: string, position: int) = status_chat.reorderCommunityChat(communityId, categoryId, chatId, position) proc joinCommunity*(self: ChatModel, communityId: string) = status_chat.joinCommunity(communityId) proc requestCommunityInfo*(self: ChatModel, communityId: string) = if (not self.mailserverReady): self.communitiesToFetch.add(communityId) self.communitiesToFetch = self.communitiesToFetch.deduplicate() return status_chat.requestCommunityInfo(communityId) proc leaveCommunity*(self: ChatModel, communityId: string) = status_chat.leaveCommunity(communityId) proc inviteUserToCommunity*(self: ChatModel, communityId: string, pubKey: string) = let response = status_chat.inviteUsersToCommunity(communityId, @[pubKey]) discard self.processMessageUpdateAfterSend(response) proc inviteUsersToCommunity*(self: ChatModel, communityId: string, pubKeys: seq[string]) = let response = status_chat.inviteUsersToCommunity(communityId, pubKeys) discard self.processMessageUpdateAfterSend(response) proc removeUserFromCommunity*(self: ChatModel, communityId: string, pubKey: string) = status_chat.removeUserFromCommunity(communityId, pubKey) proc banUserFromCommunity*(self: ChatModel, pubKey: string, communityId: string): string = return status_chat.banUserFromCommunity(pubKey, communityId) proc exportCommunity*(self: ChatModel, communityId: string): string = result = status_chat.exportCommunity(communityId) proc importCommunity*(self: ChatModel, communityKey: string): string = result = status_chat.importCommunity(communityKey) proc requestToJoinCommunity*(self: ChatModel, communityKey: string, ensName: string): seq[CommunityMembershipRequest] = status_chat.requestToJoinCommunity(communityKey, ensName) proc acceptRequestToJoinCommunity*(self: ChatModel, requestId: string) = status_chat.acceptRequestToJoinCommunity(requestId) proc declineRequestToJoinCommunity*(self: ChatModel, requestId: string) = status_chat.declineRequestToJoinCommunity(requestId) proc pendingRequestsToJoinForCommunity*(self: ChatModel, communityKey: string): seq[CommunityMembershipRequest] = result = status_chat.pendingRequestsToJoinForCommunity(communityKey) proc setCommunityMuted*(self: ChatModel, communityId: string, muted: bool) = status_chat.setCommunityMuted(communityId, muted) proc myPendingRequestsToJoin*(self: ChatModel): seq[CommunityMembershipRequest] = result = status_chat.myPendingRequestsToJoin() proc setPinMessage*(self: ChatModel, messageId: string, chatId: string, pinned: bool) = status_chat.setPinMessage(messageId, chatId, pinned) proc activityCenterNotifications*(self: ChatModel, initialLoad: bool = true) = # Notifications were already loaded, since cursor will # be nil/empty if there are no more notifs if(not initialLoad and self.activityCenterCursor == ""): return let activityCenterNotificationsTuple = status_chat.activityCenterNotification(self.activityCenterCursor) self.activityCenterCursor = activityCenterNotificationsTuple[0]; self.events.emit("activityCenterNotificationsLoaded", ActivityCenterNotificationsArgs(activityCenterNotifications: activityCenterNotificationsTuple[1])) proc activityCenterNotifications*(self: ChatModel, cursor: string = "", activityCenterNotifications: seq[ActivityCenterNotification]) = self.activityCenterCursor = cursor self.events.emit("activityCenterNotificationsLoaded", ActivityCenterNotificationsArgs(activityCenterNotifications: activityCenterNotifications)) proc markAllActivityCenterNotificationsRead*(self: ChatModel): string = try: status_chat.markAllActivityCenterNotificationsRead() except Exception as e: error "Error marking all as read", msg = e.msg result = e.msg # This proc should accept ActivityCenterNotificationType in order to clear all notifications # per type, that's why we have this part here. If we add all types to notificationsType that # means that we need to clear all notifications for all types. var types : seq[ActivityCenterNotificationType] for t in ActivityCenterNotificationType: types.add(t) self.events.emit("markNotificationsAsRead", MarkAsReadNotificationProperties(notificationTypes: types)) proc markActivityCenterNotificationRead*(self: ChatModel, notificationId: string, markAsReadProps: MarkAsReadNotificationProperties): string = try: status_chat.markActivityCenterNotificationsRead(@[notificationId]) except Exception as e: error "Error marking as read", msg = e.msg result = e.msg self.events.emit("markNotificationsAsRead", markAsReadProps) proc markActivityCenterNotificationUnread*(self: ChatModel, notificationId: string, markAsUnreadProps: MarkAsUnreadNotificationProperties): string = try: status_chat.markActivityCenterNotificationsUnread(@[notificationId]) except Exception as e: error "Error marking as unread", msg = e.msg result = e.msg self.events.emit("markNotificationsAsUnread", markAsUnreadProps) proc acceptActivityCenterNotifications*(self: ChatModel, ids: seq[string]): string = try: let response = status_chat.acceptActivityCenterNotifications(ids) let (chats, messages) = self.parseChatResponse(response) self.events.emit("chatUpdate", ChatUpdateArgs(messages: messages, chats: chats)) except Exception as e: error "Error marking as accepted", msg = e.msg result = e.msg proc dismissActivityCenterNotifications*(self: ChatModel, ids: seq[string]): string = try: discard status_chat.dismissActivityCenterNotifications(ids) except Exception as e: error "Error marking as dismissed", msg = e.msg result = e.msg proc unreadActivityCenterNotificationsCount*(self: ChatModel): int = status_chat.unreadActivityCenterNotificationsCount() proc getLinkPreviewData*(link: string, success: var bool): JsonNode = result = status_chat.getLinkPreviewData(link, success) proc getCommunityIdForChat*(self: ChatModel, chatId: string): string = if (not self.hasChannel(chatId)): return "" return self.channels[chatId].communityId proc onAsyncSearchMessages*(self: ChatModel, response: string) = let responseObj = response.parseJson if (responseObj.kind != JObject): info "search messages response is not an json object" return var chatId: string discard responseObj.getProp("chatId", chatId) var messagesObj: JsonNode if (not responseObj.getProp("messages", messagesObj)): info "search messages response doesn't contain messages property" return var messagesArray: JsonNode if (not messagesObj.getProp("messages", messagesArray)): info "search messages response doesn't contain messages array" return var messages: seq[Message] = @[] if (messagesArray.kind == JArray): for jsonMsg in messagesArray: messages.add(jsonMsg.toMessage()) self.events.emit("searchMessagesLoaded", MsgsLoadedArgs(chatId: chatId, messages: messages)) proc onLoadMoreMessagesForChannel*(self: ChatModel, response: string) = let responseObj = response.parseJson if (responseObj.kind != JObject): info "load more messages response is not an json object" # notify view self.events.emit("messagesLoaded", MsgsLoadedArgs()) self.events.emit("reactionsLoaded", ReactionsLoadedArgs()) self.events.emit("pinnedMessagesLoaded", MsgsLoadedArgs()) return var chatId: string discard responseObj.getProp("chatId", chatId) # handling chat messages var chatMessagesObj: JsonNode var chatCursor: string discard responseObj.getProp("messages", chatMessagesObj) discard responseObj.getProp("messagesCursor", chatCursor) self.msgCursor[chatId] = chatCursor var messages: seq[Message] = @[] if (chatMessagesObj.kind == JArray): for jsonMsg in chatMessagesObj: messages.add(jsonMsg.toMessage()) if messages.len > 0: let lastMsgIndex = messages.len - 1 let ts = times.convert(Milliseconds, Seconds, messages[lastMsgIndex].whisperTimestamp.parseInt()) self.lastMessageTimestamps[chatId] = ts # handling reactions var reactionsObj: JsonNode var reactionsCursor: string discard responseObj.getProp("reactions", reactionsObj) discard responseObj.getProp("reactionsCursor", reactionsCursor) self.emojiCursor[chatId] = reactionsCursor; var reactions: seq[Reaction] = @[] if (reactionsObj.kind == JArray): for jsonMsg in reactionsObj: reactions.add(jsonMsg.toReaction) # handling pinned messages var pinnedMsgObj: JsonNode var pinnedMsgCursor: string discard responseObj.getProp("pinnedMessages", pinnedMsgObj) discard responseObj.getProp("pinnedMessagesCursor", pinnedMsgCursor) self.pinnedMsgCursor[chatId] = pinnedMsgCursor var pinnedMessages: seq[Message] = @[] if (pinnedMsgObj.kind == JArray): for jsonMsg in pinnedMsgObj: var msgObj: JsonNode if(jsonMsg.getProp("message", msgObj)): var msg: Message msg = msgObj.toMessage() discard jsonMsg.getProp("pinnedBy", msg.pinnedBy) pinnedMessages.add(msg) # notify view self.events.emit("messagesLoaded", MsgsLoadedArgs(chatId: chatId, messages: messages)) self.events.emit("reactionsLoaded", ReactionsLoadedArgs(reactions: reactions)) self.events.emit("pinnedMessagesLoaded", MsgsLoadedArgs(chatId: chatId, messages: pinnedMessages)) proc userNameOrAlias*(self: ChatModel, pubKey: string, prettyForm: bool = false): string = ## Returns ens name or alias, in case if prettyForm is true and ens name ## ends with ".stateofus.eth" that part will be removed. var alias: string let contacts = self.getContacts() if contacts.hasKey(pubKey): alias = ens.userNameOrAlias(contacts[pubKey]) else: alias = generateAlias(pubKey) if (prettyForm and alias.endsWith(".stateofus.eth")): alias = alias[0 .. ^15] return alias proc chatName*(self: ChatModel, chatItem: Chat): string = if (not chatItem.chatType.isOneToOne): return chatItem.name let contacts = self.getContacts() if (contacts.hasKey(chatItem.id) and contacts[chatItem.id].hasNickname()): return contacts[chatItem.id].localNickname if chatItem.ensName != "": return "@" & userName(chatItem.ensName).userName(true) return self.userNameOrAlias(chatItem.id) proc isMessageCursorSet*(self: ChatModel, channelId: string): bool = self.msgCursor.hasKey(channelId) proc getCurrentMessageCursor*(self: ChatModel, channelId: string): string = if(not self.msgCursor.hasKey(channelId)): self.msgCursor[channelId] = "" return self.msgCursor[channelId] proc isEmojiCursorSet*(self: ChatModel, channelId: string): bool = self.emojiCursor.hasKey(channelId) proc getCurrentEmojiCursor*(self: ChatModel, channelId: string): string = if(not self.emojiCursor.hasKey(channelId)): self.emojiCursor[channelId] = "" return self.emojiCursor[channelId] proc isPinnedMessageCursorSet*(self: ChatModel, channelId: string): bool = self.pinnedMsgCursor.hasKey(channelId) proc getCurrentPinnedMessageCursor*(self: ChatModel, channelId: string): string = if(not self.pinnedMsgCursor.hasKey(channelId)): self.pinnedMsgCursor[channelId] = "" return self.pinnedMsgCursor[channelId]