From 414b39d7e0362b0ee0f102f682a68c664f99274e Mon Sep 17 00:00:00 2001 From: Jonathan Rainville Date: Fri, 11 Jun 2021 13:41:59 -0400 Subject: [PATCH] feat: add mentions to activity center and interactions Fixes #2610 --- src/app/chat/core.nim | 2 + src/app/chat/event_handling.nim | 5 + src/app/chat/signal_handling.nim | 2 +- src/app/chat/view.nim | 59 ++++- .../chat/views/activity_notification_list.nim | 149 +++++++++++ src/app/chat/views/channels_list.nim | 5 + src/app/chat/views/message_format.nim | 25 +- src/app/chat/views/message_item.nim | 174 ++++++++++++ src/app/chat/views/message_list.nim | 21 +- src/status/chat.nim | 54 ++-- src/status/chat/chat.nim | 17 ++ src/status/libstatus/chat.nim | 33 ++- src/status/signals/messages.nim | 27 ++ src/status/signals/types.nim | 1 + .../Chat/ChatColumn/ActivityCenter.qml | 248 ++++++++++++------ .../ChatComponents/ActivityCenterTopBar.qml | 12 +- .../ChatComponents/ActivityChannelBadge.qml | 2 +- ui/app/AppLayouts/Chat/ChatColumn/Message.qml | 5 +- .../MessageComponents/CompactMessage.qml | 13 +- .../MessageComponents/DateGroup.qml | 19 +- .../MessageComponents/MessageMouseArea.qml | 2 +- .../MessageComponents/NormalMessage.qml | 3 + ui/app/AppLayouts/Chat/ChatColumn/TopBar.qml | 78 +++--- 23 files changed, 767 insertions(+), 189 deletions(-) create mode 100644 src/app/chat/views/activity_notification_list.nim create mode 100644 src/app/chat/views/message_item.nim diff --git a/src/app/chat/core.nim b/src/app/chat/core.nim index 394c607ca2..93fb54de9f 100644 --- a/src/app/chat/core.nim +++ b/src/app/chat/core.nim @@ -41,6 +41,8 @@ proc init*(self: ChatController) = self.status.chat.init(pubKey, messagesFromContactsOnly) self.status.stickers.init() self.view.reactions.init() + + self.view.asyncActivityNotificationLoad() let recentStickers = self.status.stickers.getRecentStickers() for sticker in recentStickers: diff --git a/src/app/chat/event_handling.nim b/src/app/chat/event_handling.nim index ce6cfc4424..a150dba706 100644 --- a/src/app/chat/event_handling.nim +++ b/src/app/chat/event_handling.nim @@ -22,6 +22,9 @@ proc handleChatEvents(self: ChatController) = self.status.events.on("pinnedMessagesLoaded") do(e:Args): self.view.pushPinnedMessages(MsgsLoadedArgs(e).messages) + self.status.events.on("activityCenterNotificationsLoaded") do(e:Args): + self.view.pushActivityCenterNotifications(ActivityCenterNotificationsArgs(e).activityCenterNotifications) + self.status.events.on("contactUpdate") do(e: Args): var evArgs = ContactUpdateArgs(e) self.view.updateUsernames(evArgs.contacts) @@ -56,6 +59,8 @@ proc handleChatEvents(self: ChatController) = self.view.communities.addMembershipRequests(evArgs.communityMembershipRequests) if (evArgs.pinnedMessages.len > 0): self.view.addPinnedMessages(evArgs.pinnedMessages) + if (evArgs.activityCenterNotifications.len > 0): + self.view.addActivityCenterNotification(evArgs.activityCenterNotifications) self.status.events.on("channelUpdate") do(e: Args): var evArgs = ChatUpdateArgs(e) diff --git a/src/app/chat/signal_handling.nim b/src/app/chat/signal_handling.nim index 9d4c7cbee6..5e120ce717 100644 --- a/src/app/chat/signal_handling.nim +++ b/src/app/chat/signal_handling.nim @@ -4,7 +4,7 @@ import proc handleSignals(self: ChatController) = self.status.events.on(SignalType.Message.event) do(e:Args): var data = MessageSignal(e) - self.status.chat.update(data.chats, data.messages, data.emojiReactions, data.communities, data.membershipRequests, data.pinnedMessages) + self.status.chat.update(data.chats, data.messages, data.emojiReactions, data.communities, data.membershipRequests, data.pinnedMessages, data.activityCenterNotification) self.status.events.on(SignalType.DiscoverySummary.event) do(e:Args): ## Handle mailserver peers being added and removed diff --git a/src/app/chat/view.nim b/src/app/chat/view.nim index b73c19f564..5200295a20 100644 --- a/src/app/chat/view.nim +++ b/src/app/chat/view.nim @@ -13,7 +13,7 @@ import ../../status/ens as status_ens import ../../status/chat/[chat, message] import ../../status/profile/profile import web3/[conversions, ethtypes] -import views/[channels_list, message_list, chat_item, suggestions_list, reactions, stickers, groups, transactions, communities, community_list, community_item] +import views/[channels_list, message_list, chat_item, suggestions_list, reactions, stickers, groups, transactions, communities, community_list, community_item, activity_notification_list] import ../utils/image_utils import ../../status/tasks/[qt, task_runner_impl] import ../../status/tasks/marathon/mailserver/worker @@ -29,6 +29,7 @@ type GetLinkPreviewDataTaskArg = ref object of QObjectTaskArg link: string uuid: string + AsyncActivityNotificationLoadTaskArg = ref object of QObjectTaskArg AsyncMessageLoadTaskArg = ref object of QObjectTaskArg chatId: string ResolveEnsTaskArg = ref object of QObjectTaskArg @@ -81,6 +82,19 @@ const asyncMessageLoadTask: Task = proc(argEncoded: string) {.gcsafe, nimcall.} } arg.finish(responseJson) +const asyncActivityNotificationLoadTask: Task = proc(argEncoded: string) {.gcsafe, nimcall.} = + let arg = decode[AsyncActivityNotificationLoadTaskArg](argEncoded) + var activityNotifications: JsonNode + var activityNotificationsCallSuccess: bool + let activityNotificationsCallResult = rpcActivityCenterNotifications(newJString(""), 20, activityNotificationsCallSuccess) + if(activityNotificationsCallSuccess): + activityNotifications = activityNotificationsCallResult.parseJson()["result"] + + let responseJson = %*{ + "activityNotifications": activityNotifications + } + arg.finish(responseJson) + proc asyncMessageLoad[T](self: T, slot: string, chatId: string) = let arg = AsyncMessageLoadTaskArg( tptr: cast[ByteAddress](asyncMessageLoadTask), @@ -90,6 +104,14 @@ proc asyncMessageLoad[T](self: T, slot: string, chatId: string) = ) self.status.tasks.threadpool.start(arg) +proc asyncActivityNotificationLoad[T](self: T, slot: string) = + let arg = AsyncActivityNotificationLoadTaskArg( + tptr: cast[ByteAddress](asyncActivityNotificationLoadTask), + vptr: cast[ByteAddress](self.vptr), + slot: slot + ) + self.status.tasks.threadpool.start(arg) + const resolveEnsTask: Task = proc(argEncoded: string) {.gcsafe, nimcall.} = let arg = decode[ResolveEnsTaskArg](argEncoded) @@ -111,6 +133,7 @@ QtObject: status: Status chats*: ChannelsList currentSuggestions*: SuggestionsList + activityNotificationList*: ActivityNotificationList callResult: string messageList*: OrderedTable[string, ChatMessageList] pinnedMessagesList*: OrderedTable[string, ChatMessageList] @@ -137,6 +160,7 @@ QtObject: self.activeChannel.delete self.contextChannel.delete self.currentSuggestions.delete + self.activityNotificationList.delete for msg in self.messageList.values: msg.delete for msg in self.pinnedMessagesList.values: @@ -159,6 +183,7 @@ QtObject: result.activeChannel = newChatItemView(status) result.contextChannel = newChatItemView(status) result.currentSuggestions = newSuggestionsList() + result.activityNotificationList = newActivityNotificationList(status) result.messageList = initOrderedTable[string, ChatMessageList]() result.pinnedMessagesList = initOrderedTable[string, ChatMessageList]() result.reactions = newReactionView(status, result.messageList.addr, result.activeChannel) @@ -438,6 +463,15 @@ QtObject: QtProperty[QVariant] suggestionList: read = getCurrentSuggestions + proc activityNotificationsChanged*(self: ChatsView) {.signal.} + + proc getActivityNotificationList(self: ChatsView): QVariant {.slot.} = + return newQVariant(self.activityNotificationList) + + QtProperty[QVariant] activityNotificationList: + read = getActivityNotificationList + notify = activityNotificationsChanged + proc upsertChannel(self: ChatsView, channel: string) = var chat: Chat = nil if self.status.chat.channels.hasKey(channel): @@ -481,6 +515,15 @@ QtObject: # put the message as pinned in the message list self.messageList[msg.chatId].changeMessagePinned(msg.id, true, msg.pinnedBy) + proc pushActivityCenterNotifications*(self:ChatsView, activityCenterNotifications: seq[ActivityCenterNotification]) = + self.activityNotificationList.setNewData(activityCenterNotifications) + self.activityNotificationsChanged() + + proc addActivityCenterNotification*(self:ChatsView, activityCenterNotifications: seq[ActivityCenterNotification]) = + for activityCenterNotification in activityCenterNotifications: + self.activityNotificationList.addActivityNotificationItemToList(activityCenterNotification) + self.activityNotificationsChanged() + proc pushMessages*(self:ChatsView, messages: var seq[Message]) = for msg in messages.mitems: self.upsertChannel(msg.chatId) @@ -626,6 +669,9 @@ QtObject: proc asyncMessageLoad*(self: ChatsView, chatId: string) {.slot.} = self.asyncMessageLoad("asyncMessageLoaded", chatId) + proc asyncActivityNotificationLoad*(self: ChatsView) {.slot.} = + self.asyncActivityNotificationLoad("asyncActivityNotificationLoaded") + proc asyncMessageLoaded*(self: ChatsView, rpcResponse: string) {.slot.} = let rpcResponseObj = rpcResponse.parseJson @@ -636,7 +682,7 @@ QtObject: let messages = rpcResponseObj{"messages"} if(messages != nil and messages.kind != JNull): - let chatMessages = parseChatMessagesResponse(chatId, messages) + let chatMessages = parseChatMessagesResponse(messages) self.status.chat.chatMessages(chatId, true, chatMessages[0], chatMessages[1]) let rxns = rpcResponseObj{"reactions"} @@ -646,9 +692,16 @@ QtObject: let pinnedMsgs = rpcResponseObj{"pinnedMessages"} if(pinnedMsgs != nil and pinnedMsgs.kind != JNull): - let pinnedMessages = parseChatPinnedMessagesResponse(chatId, pinnedMsgs) + let pinnedMessages = parseChatPinnedMessagesResponse(pinnedMsgs) self.status.chat.pinnedMessagesByChatID(chatId, pinnedMessages[0], pinnedMessages[1]) + proc asyncActivityNotificationLoaded*(self: ChatsView, rpcResponse: string) {.slot.} = + let rpcResponseObj = rpcResponse.parseJson + + if(rpcResponseObj["activityNotifications"].kind != JNull): + let activityNotifications = parseActivityCenterNotifications(rpcResponseObj["activityNotifications"]) + self.status.chat.activityCenterNotifications(activityNotifications[0], activityNotifications[1]) + proc hideLoadingIndicator*(self: ChatsView) {.slot.} = self.loadingMessages = false self.loadingMessagesChanged(false) diff --git a/src/app/chat/views/activity_notification_list.nim b/src/app/chat/views/activity_notification_list.nim new file mode 100644 index 0000000000..0819e10055 --- /dev/null +++ b/src/app/chat/views/activity_notification_list.nim @@ -0,0 +1,149 @@ +import NimQml, Tables, chronicles +import ../../../status/chat/chat +import ../../../status/status +import ../../../status/accounts +import strutils +import message_item + +type ActivityCenterNotificationViewItem* = ref object of ActivityCenterNotification + messageItem*: MessageItem + +type + NotifRoles {.pure.} = enum + Id = UserRole + 1 + ChatId = UserRole + 2 + Name = UserRole + 3 + NotificationType = UserRole + 4 + Message = UserRole + 5 + Timestamp = UserRole + 6 + Read = UserRole + 7 + Dismissed = UserRole + 8 + Accepted = UserRole + 9 + +QtObject: + type + ActivityNotificationList* = ref object of QAbstractListModel + activityCenterNotifications*: seq[ActivityCenterNotificationViewItem] + status: Status + nbUnreadNotifications*: int + + proc setup(self: ActivityNotificationList) = self.QAbstractListModel.setup + + proc delete(self: ActivityNotificationList) = + self.activityCenterNotifications = @[] + self.QAbstractListModel.delete + + proc newActivityNotificationList*(status: Status): ActivityNotificationList = + new(result, delete) + result.activityCenterNotifications = @[] + result.status = status + result.setup() + + proc unreadCountChanged*(self: ActivityNotificationList) {.signal.} + + proc unreadCount*(self: ActivityNotificationList): int {.slot.} = + self.nbUnreadNotifications + + QtProperty[int] unreadCount: + read = unreadCount + notify = unreadCountChanged + + method rowCount*(self: ActivityNotificationList, index: QModelIndex = nil): int = self.activityCenterNotifications.len + + method data(self: ActivityNotificationList, index: QModelIndex, role: int): QVariant = + if not index.isValid: + return + if index.row < 0 or index.row >= self.activityCenterNotifications.len: + return + + let acitivityNotificationItem = self.activityCenterNotifications[index.row] + let communityItemRole = role.NotifRoles + case communityItemRole: + of NotifRoles.Id: result = newQVariant(acitivityNotificationItem.id) + of NotifRoles.ChatId: result = newQVariant(acitivityNotificationItem.chatId) + of NotifRoles.Name: result = newQVariant(acitivityNotificationItem.name) + of NotifRoles.NotificationType: result = newQVariant(acitivityNotificationItem.notificationType.int) + of NotifRoles.Message: result = newQVariant(acitivityNotificationItem.messageItem) + of NotifRoles.Timestamp: result = newQVariant(acitivityNotificationItem.timestamp) + of NotifRoles.Read: result = newQVariant(acitivityNotificationItem.read.bool) + of NotifRoles.Dismissed: result = newQVariant(acitivityNotificationItem.dismissed.bool) + of NotifRoles.Accepted: result = newQVariant(acitivityNotificationItem.accepted.bool) + + proc getNotificationData(self: ActivityNotificationList, index: int, data: string): string {.slot.} = + if index < 0 or index >= self.activityCenterNotifications.len: return ("") + + let notif = self.activityCenterNotifications[index] + case data: + of "id": result = notif.id + of "chatId": result = notif.chatId + of "name": result = notif.name + of "notificationType": result = $(notif.notificationType.int) + of "timestamp": result = $(notif.timestamp) + of "read": result = $(notif.read) + of "dismissed": result = $(notif.dismissed) + of "accepted": result = $(notif.accepted) + else: result = ("") + + method roleNames(self: ActivityNotificationList): Table[int, string] = + { + NotifRoles.Id.int:"id", + NotifRoles.ChatId.int:"chatId", + NotifRoles.Name.int: "name", + NotifRoles.NotificationType.int: "notificationType", + NotifRoles.Message.int: "message", + NotifRoles.Timestamp.int: "timestamp", + NotifRoles.Read.int: "read", + NotifRoles.Dismissed.int: "dismissed", + NotifRoles.Accepted.int: "accepted" + }.toTable + + proc markAllActivityCenterNotificationsRead(self: ActivityNotificationList): string {.slot.} = + let error = self.status.chat.markAllActivityCenterNotificationsRead() + if (error != ""): + return error + + self.nbUnreadNotifications = 0 + self.unreadCountChanged() + + for activityCenterNotification in self.activityCenterNotifications: + activityCenterNotification.read = true + + let topLeft = self.createIndex(0, 0, nil) + let bottomRight = self.createIndex(self.activityCenterNotifications.len - 1, 0, nil) + self.dataChanged(topLeft, bottomRight, @[NotifRoles.Read.int]) + + + proc toActivityCenterNotificationViewItem*(self: ActivityNotificationList, activityCenterNotification: ActivityCenterNotification): ActivityCenterNotificationViewItem = + ActivityCenterNotificationViewItem( + id: activityCenterNotification.id, + chatId: activityCenterNotification.chatId, + name: activityCenterNotification.name, + notificationType: activityCenterNotification.notificationType, + timestamp: activityCenterNotification.timestamp, + read: activityCenterNotification.read, + dismissed: activityCenterNotification.dismissed, + accepted: activityCenterNotification.accepted, + messageItem: newMessageItem(self.status, activityCenterNotification.message) + ) + + proc setNewData*(self: ActivityNotificationList, activityCenterNotifications: seq[ActivityCenterNotification]) = + self.beginResetModel() + self.activityCenterNotifications = @[] + + for activityCenterNotification in activityCenterNotifications: + self.activityCenterNotifications.add(self.toActivityCenterNotificationViewItem(activityCenterNotification)) + + self.endResetModel() + + self.nbUnreadNotifications = self.status.chat.unreadActivityCenterNotificationsCount() + self.unreadCountChanged() + + proc addActivityNotificationItemToList*(self: ActivityNotificationList, activityCenterNotification: ActivityCenterNotification) = + self.beginInsertRows(newQModelIndex(), self.activityCenterNotifications.len, self.activityCenterNotifications.len) + + self.activityCenterNotifications.add(self.toActivityCenterNotificationViewItem(activityCenterNotification)) + + self.endInsertRows() + + if (not activityCenterNotification.read): + self.nbUnreadNotifications = self.nbUnreadNotifications + 1 \ No newline at end of file diff --git a/src/app/chat/views/channels_list.nim b/src/app/chat/views/channels_list.nim index d30c4f56ee..b6e6757623 100644 --- a/src/app/chat/views/channels_list.nim +++ b/src/app/chat/views/channels_list.nim @@ -146,6 +146,11 @@ QtObject: if (channel == nil): return return channel.color + proc getChannelType*(self: ChannelsList, id: string): int {.slot.} = + let channel = self.getChannelById(id) + if (channel == nil): return ChatType.Unknown.int + return channel.chatType.int + proc updateChat*(self: ChannelsList, channel: Chat) = let idx = self.upsertChannel(channel) if idx == -1: return diff --git a/src/app/chat/views/message_format.nim b/src/app/chat/views/message_format.nim index f9afd09fbf..8bbe3994e2 100644 --- a/src/app/chat/views/message_format.nim +++ b/src/app/chat/views/message_format.nim @@ -1,22 +1,29 @@ -import sequtils, re, strutils +import NimQml, Tables, sets, json, sugar, re +import ../../../status/status +import ../../../status/accounts +import ../../../status/chat +import ../../../status/chat/[message,stickers] +import ../../../status/profile/profile +import ../../../status/ens +import strformat, strutils, sequtils let NEW_LINE = re"\n|\r" -proc sectionIdentifier(message: Message): string = +proc sectionIdentifier*(message: Message): string = result = message.fromAuthor # Force section change, because group status messages are sent with the # same fromAuthor, and ends up causing the header to not be shown if message.contentType == ContentType.Group: result = "GroupChatMessage" -proc mention(self: ChatMessageList, pubKey: string): string = - if self.status.chat.contacts.hasKey(pubKey): - return ens.userNameOrAlias(self.status.chat.contacts[pubKey], true) +proc mention*(pubKey: string, contacts: Table[string, Profile]): string = + if contacts.hasKey(pubKey): + return ens.userNameOrAlias(contacts[pubKey], true) generateAlias(pubKey) # See render-inline in status-react/src/status_im/ui/screens/chat/message/message.cljs -proc renderInline(self: ChatMessageList, elem: TextItem): string = +proc renderInline*(elem: TextItem, contacts: Table[string, Profile]): string = let value = escape_html(elem.literal).multiReplace(("\r\n", "
")).multiReplace(("\n", "
")).multiReplace((" ", "  ")) case elem.textType: of "": result = value @@ -25,19 +32,19 @@ proc renderInline(self: ChatMessageList, elem: TextItem): string = of "strong": result = fmt("{value}") of "strong-emph": result = fmt(" {value} ") of "link": result = fmt("{elem.destination}") - of "mention": result = fmt("{self.mention(value)}") + of "mention": result = fmt("{mention(value, contacts)}") of "status-tag": result = fmt("#{value}") of "del": result = fmt("{value}") else: result = fmt(" {value} ") # See render-block in status-react/src/status_im/ui/screens/chat/message/message.cljs -proc renderBlock(self: ChatMessageList, message: Message): string = +proc renderBlock*(message: Message, contacts: Table[string, Profile]): string = for pMsg in message.parsedText: case pMsg.textType: of "paragraph": result = result & "

" for children in pMsg.children: - result = result & self.renderInline(children) + result = result & renderInline(children, contacts) result = result & "

" of "blockquote": var diff --git a/src/app/chat/views/message_item.nim b/src/app/chat/views/message_item.nim new file mode 100644 index 0000000000..99e03833dc --- /dev/null +++ b/src/app/chat/views/message_item.nim @@ -0,0 +1,174 @@ +import NimQml, std/wrapnils, chronicles +import ../../../status/status +import ../../../status/chat/message +import ../../../status/chat/stickers +import message_format + +QtObject: + type MessageItem* = ref object of QObject + messageItem*: Message + status*: Status + + proc setup(self: MessageItem) = + self.QObject.setup + + proc delete*(self: MessageItem) = + self.QObject.delete + + proc newMessageItem*(status: Status, message: Message): MessageItem = + new(result, delete) + result.messageItem = message + result.status = status + result.setup + + proc setMessageItem*(self: MessageItem, messageItem: Message) = + self.messageItem = messageItem + + proc alias*(self: MessageItem): string {.slot.} = result = ?.self.messageItem.alias + QtProperty[string] alias: + read = alias + + proc userName*(self: MessageItem): string {.slot.} = result = ?.self.messageItem.userName + QtProperty[string] userName: + read = userName + + proc message*(self: MessageItem): string {.slot.} = result = renderBlock(self.messageItem, self.status.chat.contacts) + QtProperty[string] message: + read = message + + proc localName*(self: MessageItem): string {.slot.} = result = ?.self.messageItem.localName + QtProperty[string] localName: + read = localName + + proc chatId*(self: MessageItem): string {.slot.} = result = ?.self.messageItem.chatId + QtProperty[string] chatId: + read = chatId + + proc clock*(self: MessageItem): int {.slot.} = result = ?.self.messageItem.clock + QtProperty[int] clock: + read = clock + + proc gapFrom*(self: MessageItem): int {.slot.} = result = ?.self.messageItem.gapFrom + QtProperty[int] gapFrom: + read = gapFrom + + proc gapTo*(self: MessageItem): int {.slot.} = result = ?.self.messageItem.gapTo + QtProperty[int] gapTo: + read = gapTo + + proc contentType*(self: MessageItem): int {.slot.} = result = self.messageItem.contentType.int + QtProperty[int] contentType: + read = contentType + + proc ensName*(self: MessageItem): string {.slot.} = result = ?.self.messageItem.ensName + QtProperty[string] ensName: + read = ensName + + proc fromAuthor*(self: MessageItem): string {.slot.} = result = ?.self.messageItem.fromAuthor + QtProperty[string] fromAuthor: + read = fromAuthor + + proc messageId*(self: MessageItem): string {.slot.} = result = ?.self.messageItem.id + QtProperty[string] messageId: + read = messageId + + proc identicon*(self: MessageItem): string {.slot.} = result = ?.self.messageItem.identicon + QtProperty[string] identicon: + read = identicon + + proc lineCount*(self: MessageItem): int {.slot.} = result = ?.self.messageItem.lineCount + QtProperty[int] lineCount: + read = lineCount + + proc localChatId*(self: MessageItem): string {.slot.} = result = ?.self.messageItem.localChatId + QtProperty[string] localChatId: + read = localChatId + + proc messageType*(self: MessageItem): string {.slot.} = result = ?.self.messageItem.messageType + QtProperty[string] messageType: + read = messageType + + proc replace*(self: MessageItem): string {.slot.} = result = ?.self.messageItem.replace + QtProperty[string] replace: + read = replace + + proc responseTo*(self: MessageItem): string {.slot.} = result = ?.self.messageItem.responseTo + QtProperty[string] responseTo: + read = responseTo + + proc rtl*(self: MessageItem): bool {.slot.} = result = ?.self.messageItem.rtl + QtProperty[bool] rtl: + read = rtl + + proc seen*(self: MessageItem): bool {.slot.} = result = ?.self.messageItem.seen + QtProperty[bool] seen: + read = seen + + proc sticker*(self: MessageItem): string {.slot.} = result = self.messageItem.stickerHash.decodeContentHash() + QtProperty[string] sticker: + read = sticker + + proc sectionIdentifier*(self: MessageItem): string {.slot.} = result = sectionIdentifier(self.messageItem) + QtProperty[string] sectionIdentifier: + read = sectionIdentifier + + proc stickerPackId*(self: MessageItem): int {.slot.} = result = ?.self.messageItem.stickerPackId + QtProperty[int] stickerPackId: + read = stickerPackId + + proc plainText*(self: MessageItem): string {.slot.} = result = ?.self.messageItem.text + QtProperty[string] plainText: + read = plainText + + proc timestamp*(self: MessageItem): string {.slot.} = result = ?.self.messageItem.timestamp + QtProperty[string] timestamp: + read = timestamp + + proc whisperTimestamp*(self: MessageItem): string {.slot.} = result = ?.self.messageItem.whisperTimestamp + QtProperty[string] whisperTimestamp: + read = whisperTimestamp + + proc isCurrentUser*(self: MessageItem): bool {.slot.} = result = ?.self.messageItem.isCurrentUser + QtProperty[bool] isCurrentUser: + read = isCurrentUser + + proc stickerHash*(self: MessageItem): string {.slot.} = result = ?.self.messageItem.stickerHash + QtProperty[string] stickerHash: + read = stickerHash + + proc outgoingStatus*(self: MessageItem): string {.slot.} = result = ?.self.messageItem.outgoingStatus + QtProperty[string] outgoingStatus: + read = outgoingStatus + + proc linkUrls*(self: MessageItem): string {.slot.} = result = ?.self.messageItem.linkUrls + QtProperty[string] linkUrls: + read = linkUrls + + proc image*(self: MessageItem): string {.slot.} = result = ?.self.messageItem.image + QtProperty[string] image: + read = image + + + proc audio*(self: MessageItem): string {.slot.} = result = ?.self.messageItem.audio + QtProperty[string] audio: + read = audio + + proc communityId*(self: MessageItem): string {.slot.} = result = ?.self.messageItem.communityId + QtProperty[string] communityId: + read = communityId + + proc audioDurationMs*(self: MessageItem): int {.slot.} = result = ?.self.messageItem.audioDurationMs + QtProperty[int] audioDurationMs: + read = audioDurationMs + + proc hasMention*(self: MessageItem): bool {.slot.} = result = ?.self.messageItem.hasMention + QtProperty[bool] hasMention: + read = hasMention + + proc isPinned*(self: MessageItem): bool {.slot.} = result = ?.self.messageItem.isPinned + QtProperty[bool] isPinned: + read = isPinned + + proc pinnedBy*(self: MessageItem): string {.slot.} = result = ?.self.messageItem.pinnedBy + QtProperty[string] pinnedBy: + read = pinnedBy \ No newline at end of file diff --git a/src/app/chat/views/message_list.nim b/src/app/chat/views/message_list.nim index 282b8d74a6..28e93d88ad 100644 --- a/src/app/chat/views/message_list.nim +++ b/src/app/chat/views/message_list.nim @@ -1,4 +1,4 @@ -import NimQml, Tables, sets, json, sugar, chronicles +import NimQml, Tables, sets, json, sugar, chronicles, sequtils import ../../../status/status import ../../../status/accounts import ../../../status/chat @@ -6,6 +6,7 @@ import ../../../status/chat/[message,stickers] import ../../../status/profile/profile import ../../../status/ens import strformat, strutils +import message_format type ChatMessageRoles {.pure.} = enum @@ -61,8 +62,6 @@ QtObject: proc setup(self: ChatMessageList) = self.QAbstractListModel.setup - include message_format - proc fetchMoreMessagesButton(self: ChatMessageList): Message = result = Message() result.contentType = ContentType.FetchMoreMessagesButton; @@ -148,7 +147,7 @@ QtObject: let chatMessageRole = role.ChatMessageRoles case chatMessageRole: of ChatMessageRoles.UserName: result = newQVariant(message.userName) - of ChatMessageRoles.Message: result = newQVariant(self.renderBlock(message)) + of ChatMessageRoles.Message: result = newQVariant(renderBlock(message, self.status.chat.contacts)) of ChatMessageRoles.PlainText: result = newQVariant(message.text) of ChatMessageRoles.Timestamp: result = newQVariant(message.timestamp) of ChatMessageRoles.Clock: result = newQVariant($message.clock) @@ -236,13 +235,13 @@ QtObject: let message = self.messages[index] case data: - of "userName": result = (message.userName) - of "publicKey": result = (message.fromAuthor) - of "alias": result = (message.alias) - of "localName": result = (message.localName) - of "ensName": result = (message.ensName) - of "message": result = (self.renderBlock(message)) - of "identicon": result = (message.identicon) + of "userName": result = message.userName + of "publicKey": result = message.fromAuthor + of "alias": result = message.alias + of "localName": result = message.localName + of "ensName": result = message.ensName + of "message": result = (renderBlock(message, self.status.chat.contacts)) + of "identicon": result = message.identicon of "timestamp": result = $(message.timestamp) of "image": result = $(message.image) of "contentType": result = $(message.contentType.int) diff --git a/src/status/chat.nim b/src/status/chat.nim index fca822962e..02d7636bbc 100644 --- a/src/status/chat.nim +++ b/src/status/chat.nim @@ -29,6 +29,7 @@ type emojiReactions*: seq[Reaction] communities*: seq[Community] communityMembershipRequests*: seq[CommunityMembershipRequest] + activityCenterNotifications*: seq[ActivityCenterNotification] ChatIdArg* = ref object of Args chatId*: string @@ -42,10 +43,12 @@ type CommunityActiveChangedArgs* = ref object of Args active*: bool - MsgsLoadedArgs* = ref object of Args messages*: seq[Message] + ActivityCenterNotificationsArgs* = ref object of Args + activityCenterNotifications*: seq[ActivityCenterNotification] + ReactionsLoadedArgs* = ref object of Args reactions*: seq[Reaction] @@ -105,7 +108,7 @@ proc cleanSpamChatGroups(self: ChatModel, chats: seq[Chat], contacts: seq[Profil else: result.add(chat) -proc update*(self: ChatModel, chats: seq[Chat], messages: seq[Message], emojiReactions: seq[Reaction], communities: seq[Community], communityMembershipRequests: seq[CommunityMembershipRequest], pinnedMessages: seq[Message]) = +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]) = var contacts = getAddedContacts() var chatList = chats @@ -126,7 +129,7 @@ proc update*(self: ChatModel, chats: seq[Chat], messages: seq[Message], emojiRea if self.lastMessageTimestamps[chatId] > ts: self.lastMessageTimestamps[chatId] = ts - self.events.emit("chatUpdate", ChatUpdateArgs(messages: messages, chats: chatList, contacts: @[], emojiReactions: emojiReactions, communities: communities, communityMembershipRequests: communityMembershipRequests, pinnedMessages: pinnedMessages)) + self.events.emit("chatUpdate", ChatUpdateArgs(messages: messages,chats: chatList, contacts: @[], emojiReactions: emojiReactions, communities: communities, communityMembershipRequests: communityMembershipRequests, pinnedMessages: pinnedMessages, activityCenterNotifications: activityCenterNotifications)) proc hasChannel*(self: ChatModel, chatId: string): bool = self.channels.hasKey(chatId) @@ -177,21 +180,6 @@ proc requestMissingCommunityInfos*(self: ChatModel) = for communityId in self.communitiesToFetch: status_chat.requestCommunityInfo(communityId) -proc activityCenterNotification*(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 - - status_chat.activityCenterNotification(self.activityCenterCursor) - # self.activityCenterCursor[chatId] = messageTuple[0]; - - # if messageTuple[1].len > 0: - # let lastMsgIndex = messageTuple[1].len - 1 - # let ts = times.convert(Milliseconds, Seconds, messageTuple[1][lastMsgIndex].whisperTimestamp.parseInt()) - # self.lastMessageTimestamps[chatId] = ts - - # self.events.emit("messagesLoaded", MsgsLoadedArgs(messages: messageTuple[1])) - proc init*(self: ChatModel, pubKey: string, messagesFromContactsOnly: bool) = self.publicKey = pubKey self.messagesFromContactsOnly = messagesFromContactsOnly @@ -202,8 +190,6 @@ proc init*(self: ChatModel, pubKey: string, messagesFromContactsOnly: bool) = if (messagesFromContactsOnly): chatList = self.cleanSpamChatGroups(chatList, contacts) - # self.activityCenterNotification() - let profileUpdatesChatIds = chatList.filter(c => c.chatType == ChatType.Profile).map(c => c.id) if chatList.filter(c => c.chatType == ChatType.Timeline).len == 0: @@ -555,3 +541,31 @@ proc pinnedMessagesByChatID*(self: ChatModel, chatId: string, cursor: string = " self.msgCursor[chatId] = cursor self.events.emit("pinnedMessagesLoaded", MsgsLoadedArgs(messages: pinnedMessages)) + +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 + + +proc unreadActivityCenterNotificationsCount*(self: ChatModel): int = + status_chat.unreadActivityCenterNotificationsCount() + \ No newline at end of file diff --git a/src/status/chat/chat.nim b/src/status/chat/chat.nim index 6854e46060..79f5fb62dc 100644 --- a/src/status/chat/chat.nim +++ b/src/status/chat/chat.nim @@ -11,6 +11,12 @@ type ChatType* {.pure.}= enum Timeline = 5 CommunityChat = 6 +type ActivityCenterNotificationType* {.pure.}= enum + Unknown = 0, + NewOneToOne = 1, + NewPrivateGroupChat = 2, + Mention = 3 + proc isOneToOne*(self: ChatType): bool = self == ChatType.OneToOne proc isTimeline*(self: ChatType): bool = self == ChatType.Timeline @@ -122,6 +128,17 @@ type Community* = object membershipRequests*: seq[CommunityMembershipRequest] communityColor*: string +type ActivityCenterNotification* = ref object of RootObj + id*: string # ID is the id of the chat, for public chats it is the name e.g. status, for one-to-one is the hex encoded public key and for group chats is a random uuid appended with the hex encoded pk of the creator of the chat + chatId*: string + name*: string + notificationType*: ActivityCenterNotificationType + message*: Message + timestamp*: int64 + read*: bool + dismissed*: bool + accepted*: bool + proc `$`*(self: Chat): string = result = fmt"Chat(id:{self.id}, name:{self.name}, active:{self.isActive}, type:{self.chatType})" diff --git a/src/status/libstatus/chat.nim b/src/status/libstatus/chat.nim index 5366525891..446b6ea317 100644 --- a/src/status/libstatus/chat.nim +++ b/src/status/libstatus/chat.nim @@ -73,7 +73,7 @@ proc loadChats*(): seq[Chat] = result.add(chat) result.sort(sortChats) -proc parseChatMessagesResponse*(chatId: string, rpcResult: JsonNode): (string, seq[Message]) = +proc parseChatMessagesResponse*(rpcResult: JsonNode): (string, seq[Message]) = var messages: seq[Message] = @[] let messagesObj = rpcResult{"messages"} if(messagesObj != nil and messagesObj.kind != JNull): @@ -82,6 +82,15 @@ proc parseChatMessagesResponse*(chatId: string, rpcResult: JsonNode): (string, s messages.add(jsonMsg.toMessage(pk)) return (rpcResult{"cursor"}.getStr, messages) +proc parseActivityCenterNotifications*(rpcResult: JsonNode): (string, seq[ActivityCenterNotification]) = + let pk = status_settings.getSetting[string](Setting.PublicKey, "0x0") + var notifs: seq[ActivityCenterNotification] = @[] + var msg: Message + if rpcResult{"notifications"}.kind != JNull: + for jsonMsg in rpcResult["notifications"]: + notifs.add(jsonMsg.toActivityCenterNotification(pk)) + return (rpcResult{"cursor"}.getStr, notifs) + proc rpcChatMessages*(chatId: string, cursorVal: JsonNode, limit: int, success: var bool): string = success = true try: @@ -101,7 +110,7 @@ proc chatMessages*(chatId: string, cursor: string = ""): (string, seq[Message]) var success: bool let callResult = rpcChatMessages(chatId, cursorVal, 20, success) if success: - result = parseChatMessagesResponse(chatId, callResult.parseJson()["result"]) + result = parseChatMessagesResponse(callResult.parseJson()["result"]) proc parseReactionsResponse*(chatId: string, rpcResult: JsonNode): (string, seq[Reaction]) = var reactions: seq[Reaction] = @[] @@ -517,7 +526,7 @@ proc banUserFromCommunity*(pubKey: string, communityId: string): string = }]) -proc parseChatPinnedMessagesResponse*(chatId: string, rpcResult: JsonNode): (string, seq[Message]) = +proc parseChatPinnedMessagesResponse*(rpcResult: JsonNode): (string, seq[Message]) = var messages: seq[Message] = @[] let messagesObj = rpcResult{"pinnedMessages"} if(messagesObj != nil and messagesObj.kind != JNull): @@ -549,7 +558,7 @@ proc pinnedMessagesByChatID*(chatId: string, cursor: string): (string, seq[Messa var success: bool let callResult = rpcPinnedChatMessages(chatId, cursorVal, 20, success) if success: - result = parseChatPinnedMessagesResponse(chatId, callResult.parseJson()["result"]) + result = parseChatPinnedMessagesResponse(callResult.parseJson()["result"]) proc setPinMessage*(messageId: string, chatId: string, pinned: bool) = discard callPrivateRPC("sendPinMessage".prefix, %*[{ @@ -566,7 +575,7 @@ proc rpcActivityCenterNotifications*(cursorVal: JsonNode, limit: int, success: v success = false result = e.msg -proc activityCenterNotification*(cursor: string = "") = +proc activityCenterNotification*(cursor: string = ""): (string, seq[ActivityCenterNotification]) = var cursorVal: JsonNode if cursor == "": @@ -576,6 +585,14 @@ proc activityCenterNotification*(cursor: string = "") = var success: bool let callResult = rpcActivityCenterNotifications(cursorVal, 20, success) - debug "Activity center", callResult - # if success: - # result = parseChatMessagesResponse(chatId, callResult.parseJson()["result"]) + if success: + result = parseActivityCenterNotifications(callResult.parseJson()["result"]) + +proc markAllActivityCenterNotificationsRead*() = + discard callPrivateRPC("markAllActivityCenterNotificationsRead".prefix, %*[]) + +proc unreadActivityCenterNotificationsCount*(): int = + let rpcResult = callPrivateRPC("unreadActivityCenterNotificationsCount".prefix, %*[]).parseJson + + if rpcResult{"result"}.kind != JNull: + return rpcResult["result"].getInt diff --git a/src/status/signals/messages.nim b/src/status/signals/messages.nim index ef72bddc24..c80ea9bd32 100644 --- a/src/status/signals/messages.nim +++ b/src/status/signals/messages.nim @@ -23,6 +23,8 @@ proc toCommunity*(jsonCommunity: JsonNode): Community proc toCommunityMembershipRequest*(jsonCommunityMembershipRequest: JsonNode): CommunityMembershipRequest +proc toActivityCenterNotification*(jsonNotification: JsonNode, pk: string): ActivityCenterNotification + proc fromEvent*(event: JsonNode): Signal = var signal:MessageSignal = MessageSignal() signal.messages = @[] @@ -66,6 +68,10 @@ proc fromEvent*(event: JsonNode): Signal = for jsonCommunity in event["event"]["requestsToJoinCommunity"]: signal.membershipRequests.add(jsonCommunity.toCommunityMembershipRequest) + if event["event"]{"activityCenterNotifications"} != nil: + for jsonNotification in event["event"]["activityCenterNotifications"]: + signal.activityCenterNotification.add(jsonNotification.toActivityCenterNotification(pk)) + if event["event"]{"pinMessages"} != nil: for jsonPinnedMessage in event["event"]["pinMessages"]: var contentType: ContentType @@ -365,3 +371,24 @@ proc toReaction*(jsonReaction: JsonNode): Reaction = emojiId: jsonReaction{"emojiId"}.getInt, retracted: jsonReaction{"retracted"}.getBool ) + +proc toActivityCenterNotification*(jsonNotification: JsonNode, pk: string): ActivityCenterNotification = + var activityCenterNotificationType: ActivityCenterNotificationType + try: + activityCenterNotificationType = ActivityCenterNotificationType(jsonNotification{"type"}.getInt) + except: + warn "Unknown notification type received", type = jsonNotification{"type"}.getInt + activityCenterNotificationType = ActivityCenterNotificationType.Unknown + result = ActivityCenterNotification( + id: jsonNotification{"id"}.getStr, + chatId: jsonNotification{"chatId"}.getStr, + name: jsonNotification{"name"}.getStr, + notificationType: activityCenterNotificationType, + timestamp: jsonNotification{"timestamp"}.getInt, + read: jsonNotification{"read"}.getBool, + dismissed: jsonNotification{"dismissed"}.getBool, + accepted: jsonNotification{"accepted"}.getBool + ) + + if jsonNotification.contains("message") and jsonNotification{"message"}.kind != JNull: + result.message = jsonNotification{"message"}.toMessage(pk) diff --git a/src/status/signals/types.nim b/src/status/signals/types.nim index 053bebbbaa..bb23ca33b0 100644 --- a/src/status/signals/types.nim +++ b/src/status/signals/types.nim @@ -36,6 +36,7 @@ type MessageSignal* = ref object of Signal emojiReactions*: seq[Reaction] communities*: seq[Community] membershipRequests*: seq[CommunityMembershipRequest] + activityCenterNotification*: seq[ActivityCenterNotification] type CommunitySignal* = ref object of Signal community*: Community diff --git a/ui/app/AppLayouts/Chat/ChatColumn/ActivityCenter.qml b/ui/app/AppLayouts/Chat/ChatColumn/ActivityCenter.qml index c985a38e55..1414c41cec 100644 --- a/ui/app/AppLayouts/Chat/ChatColumn/ActivityCenter.qml +++ b/ui/app/AppLayouts/Chat/ChatColumn/ActivityCenter.qml @@ -1,10 +1,12 @@ import QtQuick 2.13 import QtQuick.Controls 2.13 +import QtQml.Models 2.13 import "../../../../shared" import "../../../../shared/status" import "../../../../imports" import "./ChatComponents" import "../components" +import "./MessageComponents" Popup { enum Filter { @@ -43,104 +45,182 @@ Popup { id: activityCenterTopBar } - Column { - id: notificationsContainer + ScrollView { + id: scrollView anchors.top: activityCenterTopBar.bottom anchors.topMargin: 13 + anchors.bottom: parent.bottom + anchors.bottomMargin: Style.current.smallPadding width: parent.width + clip: true - property Component profilePopupComponent: ProfilePopup { - id: profilePopup - onClosed: destroy() - } + ScrollBar.horizontal.policy: ScrollBar.AlwaysOff - // TODO remove this once it is handled by the activity center - Repeater { - id: contactList - model: profileModel.contacts.contactRequests - - delegate: ContactRequest { - visible: activityCenter.currentFilter === ActivityCenter.Filter.All || activityCenter.currentFilter === ActivityCenter.Filter.ContactRequests - name: Utils.removeStatusEns(model.name) - address: model.address - localNickname: model.localNickname - identicon: model.thumbnailImage || model.identicon - // TODO set to transparent bg if the notif is read - color: Utils.setColorAlpha(Style.current.blue, 0.1) - radius: 0 - profileClick: function (showFooter, userName, fromAuthor, identicon, textParam, nickName) { - var popup = profilePopupComponent.createObject(contactList); - popup.openPopup(showFooter, userName, fromAuthor, identicon, textParam, nickName); - } - onBlockContactActionTriggered: { - blockContactConfirmationDialog.contactName = name - blockContactConfirmationDialog.contactAddress = address - blockContactConfirmationDialog.open() - } - } - } - - StyledText { - text: "Today" - anchors.left: parent.left - anchors.leftMargin: Style.current.padding - font.pixelSize: 15 - bottomPadding: 4 - topPadding: Style.current.halfPadding - color: Style.current.secondaryText - } - - Rectangle { - visible: activityCenter.currentFilter === ActivityCenter.Filter.All || activityCenter.currentFilter === ActivityCenter.Filter.Mentions + Column { + id: notificationsContainer width: parent.width - height: visible ? childrenRect.height + Style.current.smallPadding : 0 - color: Utils.setColorAlpha(Style.current.blue, 0.1) + spacing: 0 - Message { - id: placeholderMessage - anchors.right: undefined - messageId: "placeholderMessage" - userName: "@vitalik" - identicon: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADIAAAAyCAYAAAAeP4ixAAAAb0lEQVR4Ae3UQQqAIBRF0Wj9ba9Bq6l5JBQqfn/ngDMH3YS3AAB/tO3H+XRG3b9bR/+gVoREI2RapVXpfd5+X5oXERKNkHS+rk3tOpWkeREh0QiZVu91ql2zNC8iJBoh0yqtSqt1slpCghICANDPBc0ESPh0bHkHAAAAAElFTkSuQmCC" - message: "@roger great question my dude" - contentType: Constants.messageType - placeholderMessage: true + property Component profilePopupComponent: ProfilePopup { + id: profilePopup + onClosed: destroy() } - StatusIconButton { - id: markReadBtn - icon.name: "double-check" - iconColor: Style.current.primary - icon.width: 24 - icon.height: 24 - width: 32 - height: 32 - onClicked: console.log('TODO mark read') - anchors.right: parent.right - anchors.rightMargin: 12 - anchors.verticalCenter: placeholderMessage.verticalCenter - z: 52 + // TODO remove this once it is handled by the activity center + Repeater { + id: contactList + model: profileModel.contacts.contactRequests - StatusToolTip { - visible: markReadBtn.hovered - text: qsTr("Mark as Read") - orientation: "left" - x: - width - Style.current.padding - y: markReadBtn.height / 2 - height / 2 + 4 + delegate: ContactRequest { + visible: activityCenter.currentFilter === ActivityCenter.Filter.All || activityCenter.currentFilter === ActivityCenter.Filter.ContactRequests + name: Utils.removeStatusEns(model.name) + address: model.address + localNickname: model.localNickname + identicon: model.thumbnailImage || model.identicon + // TODO set to transparent bg if the notif is read + color: Utils.setColorAlpha(Style.current.blue, 0.1) + radius: 0 + profileClick: function (showFooter, userName, fromAuthor, identicon, textParam, nickName) { + var popup = profilePopupComponent.createObject(contactList); + popup.openPopup(showFooter, userName, fromAuthor, identicon, textParam, nickName); + } + onBlockContactActionTriggered: { + blockContactConfirmationDialog.contactName = name + blockContactConfirmationDialog.contactAddress = address + blockContactConfirmationDialog.open() + } } } - ActivityChannelBadge { - name: "status-desktop-ui" - chatType: Constants.chatTypePublic - chatId: "status-desktop-ui" - anchors.top: markReadBtn.bottom - anchors.topMargin: Style.current.halfPadding - anchors.left: parent.left - anchors.leftMargin: 61 // TODO find a way to align with the text of the message + Repeater { + model: notifDelegateList } - } - // TODO add reply placeholder and chaeck if we can do the bubble under + DelegateModelGeneralized { + id: notifDelegateList + lessThan: [ + function(left, right) { return left.timestamp > right.timestamp } + ] + + model: chatsModel.activityNotificationList + + delegate: Item { + id: notificationDelegate + width: parent.width + height: notifLoader.active ? childrenRect.height : 0 + + property int idx: DelegateModel.itemsIndex + + + Loader { + id: notifLoader + anchors.top: parent.top + active: !!sourceComponent + width: parent.width + sourceComponent: { + switch (model.notificationType) { + // TODO add to constants (mention) + case 3: return messageNotificationComponent + default: return null + } + } + } + + Component { + id: messageNotificationComponent + + Rectangle { + visible: activityCenter.currentFilter === ActivityCenter.Filter.All || activityCenter.currentFilter === ActivityCenter.Filter.Mentions + width: parent.width + height: childrenRect.height + Style.current.smallPadding + color: model.read ? Style.current.transparent : Utils.setColorAlpha(Style.current.blue, 0.1) + + Message { + id: notificationMessage + anchors.right: undefined + fromAuthor: model.message.fromAuthor + chatId: model.message.chatId + userName: model.message.userName + alias: model.message.alias + localName: model.message.localName + message: model.message.message + plainText: model.message.plainText + identicon: model.message.identicon + isCurrentUser: model.message.isCurrentUser + timestamp: model.message.timestamp + sticker: model.message.sticker + contentType: model.message.contentType + outgoingStatus: model.message.outgoingStatus + responseTo: model.message.responseTo + imageClick: imagePopup.openPopup.bind(imagePopup) + messageId: model.message.messageId + linkUrls: model.message.linkUrls + communityId: model.message.communityId + hasMention: model.message.hasMention + stickerPackId: model.message.stickerPackId + pinnedBy: model.message.pinnedBy + pinnedMessage: model.message.isPinned + activityCenterMessage: true + prevMessageIndex: { + if (notificationDelegate.idx === 0) { + return 0 + } + + // This is used in order to have access to the previous message and determine the timestamp + // we can't rely on the index because the sequence of messages is not ordered on the nim side + if (notificationDelegate.idx < notifDelegateList.items.count - 1) { + return notifDelegateList.items.get(notificationDelegate.idx - 1).model.index + } + return -1; + } + prevMsgTimestamp: notificationDelegate.idx === 0 ? "" : chatsModel.activityNotificationList.getNotificationData(prevMessageIndex, "timestamp") + } + + // TODO add this back when single MarkAsRead is available + // StatusIconButton { + // id: markReadBtn + // icon.name: "double-check" + // iconColor: Style.current.primary + // icon.width: 24 + // icon.height: 24 + // width: 32 + // height: 32 + // onClicked: console.log('TODO mark read') + // anchors.right: parent.right + // anchors.rightMargin: 12 + // anchors.verticalCenter: notificationMessage.verticalCenter + // z: 52 + + // StatusToolTip { + // visible: markReadBtn.hovered + // text: qsTr("Mark as Read") + // orientation: "left" + // x: - width - Style.current.padding + // y: markReadBtn.height / 2 - height / 2 + 4 + // } + // } + + ActivityChannelBadge { + name: model.name + chatId: model.chatId + anchors.top: notificationMessage.bottom + anchors.left: parent.left + anchors.leftMargin: 61 // TODO find a way to align with the text of the message + } + } + } + } + } + + // StyledText { + // text: "Today" + // anchors.left: parent.left + // anchors.leftMargin: Style.current.padding + // font.pixelSize: 15 + // bottomPadding: 4 + // topPadding: Style.current.halfPadding + // color: Style.current.secondaryText + // } + } } } diff --git a/ui/app/AppLayouts/Chat/ChatColumn/ChatComponents/ActivityCenterTopBar.qml b/ui/app/AppLayouts/Chat/ChatColumn/ChatComponents/ActivityCenterTopBar.qml index 08d8d3f6a4..80f9f4ea92 100644 --- a/ui/app/AppLayouts/Chat/ChatColumn/ChatComponents/ActivityCenterTopBar.qml +++ b/ui/app/AppLayouts/Chat/ChatColumn/ChatComponents/ActivityCenterTopBar.qml @@ -71,7 +71,9 @@ Item { icon.height: 24 width: 32 height: 32 - onClicked: console.log('TODO mark all as read') + onClicked: { + errorText.text = chatsModel.activityNotificationList.markAllActivityCenterNotificationsRead() + } StatusToolTip { visible: markAllReadBtn.hovered @@ -109,4 +111,12 @@ Item { } } } + + StyledText { + id: errorText + visible: !!text + anchors.top: filterButtons.bottom + anchors.topMargin: Style.current.smallPadding + color: Style.current.danger + } } diff --git a/ui/app/AppLayouts/Chat/ChatColumn/ChatComponents/ActivityChannelBadge.qml b/ui/app/AppLayouts/Chat/ChatColumn/ChatComponents/ActivityChannelBadge.qml index 935ed4b271..7db2e9f03f 100644 --- a/ui/app/AppLayouts/Chat/ChatColumn/ChatComponents/ActivityChannelBadge.qml +++ b/ui/app/AppLayouts/Chat/ChatColumn/ChatComponents/ActivityChannelBadge.qml @@ -8,7 +8,7 @@ Rectangle { property string chatId: "" property string name: "channelName" property string identicon - property int chatType: Constants.chatTypePublic + property int chatType: chatsModel.chats.getChannelType(chatId) property int realChatType: { if (chatType === Constants.chatTypeCommunity) { // TODO add a check for private community chats once it is created diff --git a/ui/app/AppLayouts/Chat/ChatColumn/Message.qml b/ui/app/AppLayouts/Chat/ChatColumn/Message.qml index b340135b2b..77433e37c1 100644 --- a/ui/app/AppLayouts/Chat/ChatColumn/Message.qml +++ b/ui/app/AppLayouts/Chat/ChatColumn/Message.qml @@ -29,6 +29,7 @@ Item { property bool hasMention: false property string linkUrls: "" property bool placeholderMessage: false + property bool activityCenterMessage: false property bool pinnedMessage: false property string pinnedBy property bool forceHoverHandler: false // Used to force the HoverHandler to be active (useful for messages in popups) @@ -176,7 +177,7 @@ Item { } function clickMessage(isProfileClick, isSticker = false, isImage = false, image = null, emojiOnly = false, hideEmojiPicker = false) { - if (placeholderMessage) { + if (placeholderMessage || activityCenterMessage) { return } @@ -300,7 +301,7 @@ Item { height: 12 } } - + StyledText { id: fetchMoreButton font.weight: Font.Medium diff --git a/ui/app/AppLayouts/Chat/ChatColumn/MessageComponents/CompactMessage.qml b/ui/app/AppLayouts/Chat/ChatColumn/MessageComponents/CompactMessage.qml index bc0b0a45b5..0e7520fcac 100644 --- a/ui/app/AppLayouts/Chat/ChatColumn/MessageComponents/CompactMessage.qml +++ b/ui/app/AppLayouts/Chat/ChatColumn/MessageComponents/CompactMessage.qml @@ -23,7 +23,7 @@ Item { + (dateGroupLbl.visible ? dateGroupLbl.height + dateGroupLbl.anchors.topMargin : 0) MouseArea { - enabled: !placeholderMessage + enabled: !placeholderMessage && !activityCenterMessage anchors.fill: messageContainer acceptedButtons: Qt.RightButton onClicked: messageMouseArea.clicked(mouse) @@ -53,6 +53,9 @@ Item { DateGroup { id: dateGroupLbl + previousMessageIndex: prevMessageIndex + previousMessageTimestamp: prevMsgTimestamp + messageTimestamp: timestamp } Rectangle { @@ -60,7 +63,7 @@ Item { id: messageContainer anchors.top: dateGroupLbl.visible ? dateGroupLbl.bottom : parent.top - anchors.topMargin: dateGroupLbl.visible ? Style.current.padding : 0 + anchors.topMargin: dateGroupLbl.visible ? (activityCenterMessage ? 4 : Style.current.padding) : 0 height: childrenRect.height + (chatName.visible || emojiReactionLoader.active ? Style.current.halfPadding : 0) + (chatName.visible && emojiReactionLoader.active ? Style.current.padding : 0) @@ -71,6 +74,10 @@ Item { width: parent.width color: { + if (placeholderMessage || activityCenterMessage) { + return Style.current.transparent + } + if (pinnedMessage) { return root.isHovered || isMessageActive ? Style.current.pinnedMessageBackgroundHovered : Style.current.pinnedMessageBackground } @@ -297,7 +304,7 @@ Item { } Loader { - active: hasMention || pinnedMessage + active: !activityCenterMessage && (hasMention || pinnedMessage) height: messageContainer.height anchors.left: messageContainer.left anchors.top: messageContainer.top diff --git a/ui/app/AppLayouts/Chat/ChatColumn/MessageComponents/DateGroup.qml b/ui/app/AppLayouts/Chat/ChatColumn/MessageComponents/DateGroup.qml index 7388c9ddbf..f6909c9a98 100644 --- a/ui/app/AppLayouts/Chat/ChatColumn/MessageComponents/DateGroup.qml +++ b/ui/app/AppLayouts/Chat/ChatColumn/MessageComponents/DateGroup.qml @@ -3,20 +3,29 @@ import "../../../../../shared" import "../../../../../imports" StyledText { + property int previousMessageIndex: -1 + property string previousMessageTimestamp + property string messageTimestamp + id: dateGroupLbl font.pixelSize: 13 color: Style.current.secondaryText horizontalAlignment: Text.AlignHCenter - anchors.horizontalCenter: parent.horizontalCenter + anchors.horizontalCenter: activityCenterMessage ? undefined : parent.horizontalCenter + anchors.top: parent.top + anchors.topMargin: visible ? (activityCenterMessage ? Style.current.halfPadding : 20) : 0 + anchors.left: parent.left + anchors.leftMargin: activityCenterMessage ? Style.current.padding : 0 + text: { - if (prevMessageIndex == -1) return ""; // identifier + if (previousMessageIndex === -1) return ""; // identifier let now = new Date() let yesterday = new Date() yesterday.setDate(now.getDate()-1) - var currentMsgDate = new Date(parseInt(timestamp, 10)); - var prevMsgDate = prevMsgTimestamp === "" ? new Date(0) : new Date(parseInt(prevMsgTimestamp, 10)); + var currentMsgDate = new Date(parseInt(messageTimestamp, 10)); + var prevMsgDate = previousMessageTimestamp === "" ? new Date(0) : new Date(parseInt(previousMessageTimestamp, 10)); if (currentMsgDate.getDay() === prevMsgDate.getDay()) { return "" @@ -59,6 +68,4 @@ StyledText { } } visible: text !== "" - anchors.top: parent.top - anchors.topMargin: this.visible ? 20 : 0 } diff --git a/ui/app/AppLayouts/Chat/ChatColumn/MessageComponents/MessageMouseArea.qml b/ui/app/AppLayouts/Chat/ChatColumn/MessageComponents/MessageMouseArea.qml index 8a2bdf7bed..ac29d1b6bf 100644 --- a/ui/app/AppLayouts/Chat/ChatColumn/MessageComponents/MessageMouseArea.qml +++ b/ui/app/AppLayouts/Chat/ChatColumn/MessageComponents/MessageMouseArea.qml @@ -3,7 +3,7 @@ import "../../../../../shared" import "../../../../../imports" MouseArea { - enabled: !placeholderMessage + enabled: !placeholderMessage && !activityCenterMessage cursorShape: chatText.hoveredLink ? Qt.PointingHandCursor : undefined acceptedButtons: Qt.RightButton | Qt.LeftButton z: 50 diff --git a/ui/app/AppLayouts/Chat/ChatColumn/MessageComponents/NormalMessage.qml b/ui/app/AppLayouts/Chat/ChatColumn/MessageComponents/NormalMessage.qml index cc972c6f1b..a53e4ee884 100644 --- a/ui/app/AppLayouts/Chat/ChatColumn/MessageComponents/NormalMessage.qml +++ b/ui/app/AppLayouts/Chat/ChatColumn/MessageComponents/NormalMessage.qml @@ -18,6 +18,9 @@ Item { DateGroup { id: dateGroupLbl + previousMessageIndex: prevMessageIndex + previousMessageTimestamp: prevMsgTimestamp + messageTimestamp: timestamp } UserImage { diff --git a/ui/app/AppLayouts/Chat/ChatColumn/TopBar.qml b/ui/app/AppLayouts/Chat/ChatColumn/TopBar.qml index 71611c24a7..c1745c9b92 100644 --- a/ui/app/AppLayouts/Chat/ChatColumn/TopBar.qml +++ b/ui/app/AppLayouts/Chat/ChatColumn/TopBar.qml @@ -135,50 +135,50 @@ Item { } } -// Rectangle { -// id: separator -// width: 1 -// height: 24 -// color: Style.current.separator -// anchors.verticalCenter: parent.verticalCenter -// } + Rectangle { + id: separator + width: 1 + height: 24 + color: Style.current.separator + anchors.verticalCenter: parent.verticalCenter + } -// StatusIconButton { -// id: activityCenterBtn -// icon.name: "bell" -// iconColor: Style.current.contextMenuButtonForegroundColor -// hoveredIconColor: Style.current.contextMenuButtonForegroundColor -// highlightedBackgroundColor: Style.current.contextMenuButtonBackgroundHoverColor -// anchors.verticalCenter: parent.verticalCenter + StatusIconButton { + id: activityCenterBtn + icon.name: "bell" + iconColor: Style.current.contextMenuButtonForegroundColor + hoveredIconColor: Style.current.contextMenuButtonForegroundColor + highlightedBackgroundColor: Style.current.contextMenuButtonBackgroundHoverColor + anchors.verticalCenter: parent.verticalCenter -// onClicked: activityCenter.open() + onClicked: activityCenter.open() -// Rectangle { -// // TODO unhardcode this -// property int nbUnseenNotifs: 3 + Rectangle { + property int nbUnseenNotifs: chatsModel.activityNotificationList.unreadCount -// id: badge -// visible: nbUnseenNotifs > 0 -// anchors.top: parent.top -// anchors.topMargin: -2 -// anchors.left: parent.right -// anchors.leftMargin: -17 -// radius: height / 2 -// color: Style.current.blue -// border.color: activityCenterBtn.hovered ? Style.current.secondaryBackground : Style.current.background -// border.width: 2 -// width: badge.nbUnseenNotifs < 10 ? 18 : badge.width + 14 -// height: 18 + id: badge + visible: nbUnseenNotifs > 0 + anchors.top: parent.top + anchors.topMargin: -2 + anchors.left: parent.right + anchors.leftMargin: -17 + radius: height / 2 + color: Style.current.blue + border.color: activityCenterBtn.hovered ? Style.current.secondaryBackground : Style.current.background + border.width: 2 + width: badge.nbUnseenNotifs < 10 ? 18 : badgeText.width + 14 + height: 18 -// Text { -// font.pixelSize: 12 -// color: Style.current.white -// anchors.centerIn: parent -// text: badge.nbUnseenNotifs -// } -// } -// } -// } + Text { + id: badgeText + font.pixelSize: 12 + color: Style.current.white + anchors.centerIn: parent + text: badge.nbUnseenNotifs + } + } + } + } ActivityCenter { id: activityCenter