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