feat: get and display emoji reactions in chat

This commit is contained in:
Jonathan Rainville 2020-08-12 11:01:03 -04:00 committed by Iuri Matias
parent 60f7a3cbe2
commit 72af6adb69
20 changed files with 253 additions and 8 deletions

View File

@ -243,7 +243,7 @@ $(STATUS_CLIENT_APPIMAGE): nim_status_client $(APPIMAGE_TOOL) nim-status.desktop
cp ui/i18n/* tmp/linux/dist/usr/i18n
echo -e $(BUILD_MSG) "AppImage"
linuxdeployqt tmp/linux/dist/nim-status.desktop -no-translations -no-copy-copyright-files -qmldir=ui -qmlimport=$(QTDIR)/qml -bundle-non-qt-libs
linuxdeployqt tmp/linux/dist/nim-status.desktop -no-copy-copyright-files -qmldir=ui -qmlimport=$(QTDIR)/qml -bundle-non-qt-libs
rm tmp/linux/dist/AppRun
cp AppRun tmp/linux/dist/.

View File

@ -3,6 +3,9 @@ proc handleChatEvents(self: ChatController) =
# Display already saved messages
self.status.events.on("messagesLoaded") do(e:Args):
self.view.pushMessages(MsgsLoadedArgs(e).messages)
# Display emoji reactions
self.status.events.on("reactionsLoaded") do(e:Args):
self.view.pushReactions(ReactionsLoadedArgs(e).reactions)
self.status.events.on("contactUpdate") do(e: Args):
var evArgs = ContactUpdateArgs(e)
@ -13,6 +16,7 @@ proc handleChatEvents(self: ChatController) =
self.view.updateUsernames(evArgs.contacts)
self.view.updateChats(evArgs.chats)
self.view.pushMessages(evArgs.messages)
self.view.pushReactions(evArgs.emojiReactions)
self.status.events.on("channelUpdate") do(e: Args):
var evArgs = ChatUpdateArgs(e)
@ -26,6 +30,7 @@ proc handleChatEvents(self: ChatController) =
var channel = ChannelArgs(e)
discard self.view.chats.addChatItemToList(channel.chat)
self.status.chat.chatMessages(channel.chat.id)
self.status.chat.chatReactions(channel.chat.id)
self.status.events.on("chatsLoaded") do(e:Args):
self.view.calculateUnreadMessages()
@ -36,6 +41,7 @@ proc handleChatEvents(self: ChatController) =
var channel = ChannelArgs(e)
discard self.view.chats.addChatItemToList(channel.chat)
self.status.chat.chatMessages(channel.chat.id)
self.status.chat.chatReactions(channel.chat.id)
self.view.setActiveChannel(channel.chat.id)
self.status.events.on("channelLeft") do(e: Args):

View File

@ -1,6 +1,6 @@
proc handleMessage(self: ChatController, data: MessageSignal) =
self.status.chat.update(data.chats, data.messages)
self.status.chat.update(data.chats, data.messages, data.emojiReactions)
proc handleDiscoverySummary(self: ChatController, data: DiscoverySummarySignal) =
## Handle mailserver peers being added and removed

View File

@ -253,6 +253,31 @@ QtObject:
else:
self.newMessagePushed()
proc pushReactions*(self:ChatsView, reactions: var seq[Reaction]) =
let t = reactions.len
for reaction in reactions.mitems:
let messageList = self.messageList[reaction.chatId]
var message = messageList.getMessageById(reaction.messageId)
var oldReactions: JsonNode
if (message.emojiReactions == "") :
oldReactions = %*{}
else:
oldReactions = parseJson(message.emojiReactions)
if (oldReactions.hasKey(reaction.id)):
if (reaction.retracted):
# Remove the reaction
oldReactions.delete(reaction.id)
messageList.setMessageReactions(reaction.messageId, $oldReactions)
continue
oldReactions[reaction.id] = %* {
"from": reaction.fromAccount,
"emojiId": reaction.emojiId
}
messageList.setMessageReactions(reaction.messageId, $oldReactions)
proc updateUsernames*(self:ChatsView, contacts: seq[Profile]) =
if contacts.len > 0:
# Updating usernames for all the messages list
@ -297,6 +322,7 @@ QtObject:
proc loadMoreMessages*(self: ChatsView) {.slot.} =
trace "Loading more messages", chaId = self.activeChannel.id
self.status.chat.chatMessages(self.activeChannel.id, false)
self.status.chat.chatReactions(self.activeChannel.id, false)
self.messagesLoaded();
proc loadMoreMessagesWithIndex*(self: ChatsView, channelIndex: int) {.slot.} =
@ -305,6 +331,7 @@ QtObject:
if (selectedChannel == nil): return
trace "Loading more messages", chaId = selectedChannel.id
self.status.chat.chatMessages(selectedChannel.id, false)
self.status.chat.chatReactions(selectedChannel.id, false)
self.messagesLoaded();
proc leaveChatByIndex*(self: ChatsView, channelIndex: int) {.slot.} =

View File

@ -30,6 +30,7 @@ type
Image = UserRole + 19
Audio = UserRole + 20
AudioDurationMs = UserRole + 21
EmojiReactions = UserRole + 22
QtObject:
type
@ -115,6 +116,7 @@ QtObject:
of ChatMessageRoles.Image: result = newQVariant(message.image)
of ChatMessageRoles.Audio: result = newQVariant(message.audio)
of ChatMessageRoles.AudioDurationMs: result = newQVariant(message.audioDurationMs)
of ChatMessageRoles.EmojiReactions: result = newQVariant(message.emojiReactions)
method roleNames(self: ChatMessageList): Table[int, string] =
{
@ -138,7 +140,8 @@ QtObject:
ChatMessageRoles.Timeout.int: "timeout",
ChatMessageRoles.Image.int: "image",
ChatMessageRoles.Audio.int: "audio",
ChatMessageRoles.AudioDurationMs.int: "audioDurationMs"
ChatMessageRoles.AudioDurationMs.int: "audioDurationMs",
ChatMessageRoles.EmojiReactions.int: "emojiReactions"
}.toTable
proc getMessageIndex(self: ChatMessageList, messageId: string): int {.slot.} =
@ -175,17 +178,29 @@ QtObject:
self.messages.add(message)
self.endInsertRows()
proc getMessageById*(self: ChatMessageList, messageId: string): Message =
if (not self.messageIndex.hasKey(messageId)): return
return self.messages[self.messageIndex[messageId]]
proc clear*(self: ChatMessageList) =
self.beginResetModel()
self.messages = @[]
self.endResetModel()
proc setMessageReactions*(self: ChatMessageList, messageId: string, newReactions: string)=
let msgIdx = self.messageIndex[messageId]
self.messages[msgIdx].emojiReactions = newReactions
let topLeft = self.createIndex(msgIdx, 0, nil)
let bottomRight = self.createIndex(msgIdx, 0, nil)
self.dataChanged(topLeft, bottomRight, @[ChatMessageRoles.EmojiReactions.int])
proc markMessageAsSent*(self: ChatMessageList, messageId: string)=
let topLeft = self.createIndex(0, 0, nil)
let bottomRight = self.createIndex(self.messages.len, 0, nil)
for m in self.messages.mitems:
if m.id == messageId:
m.outgoingStatus = "sent"
break
self.dataChanged(topLeft, bottomRight, @[ChatMessageRoles.OutgoingStatus.int])
proc updateUsernames*(self: ChatMessageList, contacts: seq[Profile]) =

View File

@ -62,6 +62,7 @@ QtObject:
case signalType:
of SignalType.Message:
echo $jsonSignal
signal = messages.fromEvent(jsonSignal)
of SignalType.EnvelopeSent:
signal = envelopes.fromEvent(jsonSignal)

View File

@ -11,6 +11,8 @@ proc toMessage*(jsonMsg: JsonNode): Message
proc toChat*(jsonChat: JsonNode): Chat
proc toReaction*(jsonReaction: JsonNode): Reaction
proc fromEvent*(event: JsonNode): Signal =
var signal:MessageSignal = MessageSignal()
signal.messages = @[]
@ -43,6 +45,10 @@ proc fromEvent*(event: JsonNode): Signal =
for jsonInstallation in event["event"]["installations"]:
signal.installations.add(jsonInstallation.toInstallation)
if event["event"]{"emojiReactions"} != nil:
for jsonReaction in event["event"]["emojiReactions"]:
signal.emojiReactions.add(jsonReaction.toReaction)
result = signal
proc toChatMember*(jsonMember: JsonNode): ChatMember =
@ -195,4 +201,12 @@ proc toMessage*(jsonMsg: JsonNode): Message =
result = message
proc toReaction*(jsonReaction: JsonNode): Reaction =
result = Reaction(
id: jsonReaction{"id"}.getStr,
chatId: jsonReaction{"chatId"}.getStr,
fromAccount: jsonReaction{"from"}.getStr,
messageId: jsonReaction{"messageId"}.getStr,
emojiId: jsonReaction{"emojiId"}.getInt,
retracted: jsonReaction{"retracted"}.getBool
)

View File

@ -32,6 +32,7 @@ type MessageSignal* = ref object of Signal
chats*: seq[Chat]
contacts*: seq[Profile]
installations*: seq[Installation]
emojiReactions*: seq[Reaction]
type Filter* = object
chatId*: string

View File

@ -17,6 +17,7 @@ type
chats*: seq[Chat]
messages*: seq[Message]
contacts*: seq[Profile]
emojiReactions*: seq[Reaction]
ChatIdArg* = ref object of Args
chatId*: string
@ -33,11 +34,15 @@ type
MsgsLoadedArgs* = ref object of Args
messages*: seq[Message]
ReactionsLoadedArgs* = ref object of Args
reactions*: seq[Reaction]
ChatModel* = ref object
events*: EventEmitter
contacts*: Table[string, Profile]
channels*: Table[string, Chat]
msgCursor*: Table[string, string]
emojiCursor*: Table[string, string]
recentStickers*: seq[Sticker]
availableStickerPacks*: Table[int, StickerPack]
installedStickerPacks*: Table[int, StickerPack]
@ -55,6 +60,7 @@ proc newChatModel*(events: EventEmitter): ChatModel =
result.contacts = initTable[string, Profile]()
result.channels = initTable[string, Chat]()
result.msgCursor = initTable[string, string]()
result.emojiCursor = initTable[string, string]()
result.recentStickers = @[]
result.availableStickerPacks = initTable[int, StickerPack]()
result.installedStickerPacks = initTable[int, StickerPack]()
@ -64,12 +70,12 @@ proc newChatModel*(events: EventEmitter): ChatModel =
proc delete*(self: ChatModel) =
discard
proc update*(self: ChatModel, chats: seq[Chat], messages: seq[Message]) =
proc update*(self: ChatModel, chats: seq[Chat], messages: seq[Message], emojiReactions: seq[Reaction]) =
for chat in chats:
if chat.isActive:
self.channels[chat.id] = chat
self.events.emit("chatUpdate", ChatUpdateArgs(messages: messages, chats: chats, contacts: @[]))
self.events.emit("chatUpdate", ChatUpdateArgs(messages: messages, chats: chats, contacts: @[], emojiReactions: emojiReactions))
proc hasChannel*(self: ChatModel, chatId: string): bool =
self.channels.hasKey(chatId)
@ -256,6 +262,22 @@ proc chatMessages*(self: ChatModel, chatId: string, initialLoad:bool = true) =
self.msgCursor[chatId] = messageTuple[0];
self.events.emit("messagesLoaded", MsgsLoadedArgs(messages: messageTuple[1]))
proc chatReactions*(self: ChatModel, chatId: string, initialLoad:bool = true) =
try:
if not self.emojiCursor.hasKey(chatId):
self.emojiCursor[chatId] = "";
# Messages were already loaded, since cursor will
# be nil/empty if there are no more messages
if(not initialLoad and self.emojiCursor[chatId] == ""): return
let reactionTuple = status_chat.getEmojiReactionsByChatId(chatId, self.emojiCursor[chatId])
self.emojiCursor[chatId] = reactionTuple[0];
self.events.emit("reactionsLoaded", ReactionsLoadedArgs(reactions: reactionTuple[1]))
except Exception as e:
error "Error reactions", msg = e.msg
proc markAllChannelMessagesRead*(self: ChatModel, chatId: string): JsonNode =
var response = status_chat.markAllRead(chatId)
result = parseJson(response)

View File

@ -48,6 +48,15 @@ type Message* = object
image*: string
audio*: string
audioDurationMs*: int
emojiReactions*: string
type Reaction* = object
id*: string
chatId*: string
fromAccount*: string
messageId*: string
emojiId*: int
retracted*: bool
proc `$`*(self: Message): string =

View File

@ -65,6 +65,24 @@ proc chatMessages*(chatId: string, cursor: string = ""): (string, seq[Message])
return (rpcResult{"cursor"}.getStr, messages)
proc getEmojiReactionsByChatId*(chatId: string, cursor: string = ""): (string, seq[Reaction]) =
var reactions: seq[Reaction] = @[]
var cursorVal: JsonNode
if cursor == "":
cursorVal = newJNull()
else:
cursorVal = newJString(cursor)
let rpcResult = parseJson(callPrivateRPC("emojiReactionsByChatID".prefix, %* [chatId, cursorVal, 20]))["result"]
if rpcResult != nil and rpcResult.len != 0:
for jsonMsg in rpcResult:
reactions.add(jsonMsg.toReaction)
return (rpcResult{"cursor"}.getStr, reactions)
# TODO this probably belongs in another file
proc generateSymKeyFromPassword*(): string =
result = ($parseJson(callPrivateRPC("waku_generateSymKeyFromPassword", %* [

View File

@ -229,6 +229,7 @@ ScrollView {
authorPrevMsg: msgDelegate.ListView.previousSection
profileClick: profilePopup.setPopupData.bind(profilePopup)
messageId: model.messageId
emojiReactions: model.emojiReactions
prevMessageIndex: {
// 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

View File

@ -18,6 +18,7 @@ Item {
property string outgoingStatus: ""
property string responseTo: ""
property string messageId: ""
property string emojiReactions: ""
property int prevMessageIndex: -1
property bool timeout: false

View File

@ -45,7 +45,7 @@ StyledTextEdit {
`white-space: pre-wrap;`+
`}`+
`a {`+
`color: ${isCurrentUser ? Style.current.white : Style.current.textColor};`+
`color: ${isCurrentUser && !appSettings.compactMode ? Style.current.white : Style.current.textColor};`+
`}`+
`a.mention {`+
`color: ${isCurrentUser ? Style.current.cyan : Style.current.turquoise};`+

View File

@ -151,4 +151,18 @@ Item {
audioSource: audio
}
}
Loader {
id: emojiReactionLoader
active: emojiReactions !== ""
sourceComponent: emojiReactionsComponent
anchors.top: chatText.bottom
anchors.left: chatText.left
anchors.topMargin: 2
}
Component {
id: emojiReactionsComponent
EmojiReactions {}
}
}

View File

@ -0,0 +1,99 @@
import QtQuick 2.3
import "../../../../../shared"
import "../../../../../imports"
Item {
property int imageMargin: 4
id: root
height: 20
width: childrenRect.width
Repeater {
id: reactionepeater
model: {
if (!emojiReactions) {
return []
}
try {
// group by id
var allReactions = Object.values(JSON.parse(emojiReactions))
var byEmoji = {}
allReactions.forEach(function (reaction) {
if (!byEmoji[reaction.emojiId]) {
byEmoji[reaction.emojiId] = {
emojiId: reaction.emojiId,
count: 0,
currentUserReacted: false
}
}
byEmoji[reaction.emojiId].count++;
if (!byEmoji[reaction.emojiId].currentUserReacted && reaction.fromAuthor === profileModel.profile.pubKey) {
byEmoji[reaction.emojiId].currentUserReacted = true
}
})
return Object.values(byEmoji)
} catch (e) {
console.error('Error parsing emoji reactions', e)
return []
}
}
Rectangle {
width: emojiImage.width + emojiCount.width + (root.imageMargin * 2) + + 8
height: 20
radius: 10
anchors.left: (index === 0) ? parent.left: parent.children[index-1].right
anchors.leftMargin: (index === 0) ? 0 : root.imageMargin
color: modelData.currentUserReacted ? Style.current.blue : Style.current.grey
// Rounded corner to cover one corner
Rectangle {
color: parent.color
width: 8
height: 8
anchors.top: parent.top
anchors.left: !isCurrentUser ? parent.left : undefined
anchors.leftMargin: 0
anchors.right: !isCurrentUser ? undefined : parent.right
anchors.rightMargin: 0
radius: 2
z: -1
}
SVGImage {
id: emojiImage
width: 15
fillMode: Image.PreserveAspectFit
source: {
const basePath = "../../../../img/emojiReactions/"
switch (modelData.emojiId) {
case 1: return basePath + "heart.svg"
case 2: return basePath + "thumbsUp.svg"
case 3: return basePath + "thumbsDown.svg"
case 4: return basePath + "laughing.svg"
case 5: return basePath + "sad.svg"
case 6: return basePath + "angry.svg"
default: return ""
}
}
anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left
anchors.leftMargin: root.imageMargin
}
StyledText {
id: emojiCount
text: modelData.count
anchors.verticalCenter: parent.verticalCenter
anchors.left: emojiImage.right
anchors.leftMargin: root.imageMargin
font.pixelSize: 12
color: modelData.currentUserReacted ? Style.current.currentUserTextColor : Style.current.textColor
}
}
}
}

View File

@ -42,6 +42,7 @@ Rectangle {
verticalPadding: imageChatBox.chatVerticalPadding
anchors.top: (index === 0) ? parent.top: parent.children[index-1].bottom
anchors.topMargin: verticalPadding
anchors.horizontalCenter: parent.horizontalCenter
source: modelData
}
}

View File

@ -199,4 +199,19 @@ Item {
id: imageComponent
ImageMessage {}
}
Loader {
id: emojiReactionLoader
active: emojiReactions !== ""
sourceComponent: emojiReactionsComponent
anchors.left: !isCurrentUser ? chatBox.left : undefined
anchors.right: !isCurrentUser ? undefined : chatBox.right
anchors.top: chatBox.bottom
anchors.topMargin: 2
}
Component {
id: emojiReactionsComponent
EmojiReactions {}
}
}

View File

@ -126,6 +126,7 @@ DISTFILES += \
app/AppLayouts/Chat/ChatColumn/MessageComponents/ChatTime.qml \
app/AppLayouts/Chat/ChatColumn/MessageComponents/CompactMessage.qml \
app/AppLayouts/Chat/ChatColumn/MessageComponents/DateGroup.qml \
app/AppLayouts/Chat/ChatColumn/MessageComponents/EmojiReactions.qml \
app/AppLayouts/Chat/ChatColumn/MessageComponents/ImageLoader.qml \
app/AppLayouts/Chat/ChatColumn/MessageComponents/ImageMessage.qml \
app/AppLayouts/Chat/ChatColumn/MessageComponents/MessageMouseArea.qml \

2
vendor/status-go vendored

@ -1 +1 @@
Subproject commit eadf68325ebb3b775ba463016098abfac883e522
Subproject commit 2d0818d873fee570a73867d8ed4bc8e792215cf1