feat(Chat): add pinned messages feature

This commit is contained in:
Jonathan Rainville 2021-05-25 15:34:46 -04:00
parent 6abba06c42
commit b52dceb984
24 changed files with 469 additions and 22 deletions

View File

@ -18,6 +18,9 @@ proc handleChatEvents(self: ChatController) =
# Display emoji reactions
self.status.events.on("reactionsLoaded") do(e:Args):
self.view.reactions.push(ReactionsLoadedArgs(e).reactions)
# Display already pinned messages
self.status.events.on("pinnedMessagesLoaded") do(e:Args):
self.view.pushPinnedMessages(MsgsLoadedArgs(e).messages)
self.status.events.on("contactUpdate") do(e: Args):
var evArgs = ContactUpdateArgs(e)
@ -40,6 +43,8 @@ proc handleChatEvents(self: ChatController) =
self.view.communities.addCommunityToList(community)
if (evArgs.communityMembershipRequests.len > 0):
self.view.communities.addMembershipRequests(evArgs.communityMembershipRequests)
if (evArgs.pinnedMessages.len > 0):
self.view.addPinnedMessages(evArgs.pinnedMessages)
self.status.events.on("channelUpdate") do(e: Args):
var evArgs = ChatUpdateArgs(e)

View File

@ -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)
self.status.chat.update(data.chats, data.messages, data.emojiReactions, data.communities, data.membershipRequests, data.pinnedMessages)
self.status.events.on(SignalType.DiscoverySummary.event) do(e:Args):
## Handle mailserver peers being added and removed

View File

@ -65,10 +65,17 @@ const asyncMessageLoadTask: Task = proc(argEncoded: string) {.gcsafe, nimcall.}
if(reactionsCallSuccess):
reactions = reactionsCallResult.parseJson()["result"]
var pinnedMessages: JsonNode
var pinnedMessagesCallSuccess: bool
let pinnedMessagesCallResult = rpcPinnedChatMessages(arg.chatId, newJString(""), 20, pinnedMessagesCallSuccess)
if(reactionsCallSuccess):
pinnedMessages = pinnedMessagesCallResult.parseJson()["result"]
let responseJson = %*{
"chatId": arg.chatId,
"messages": messages,
"reactions": reactions
"reactions": reactions,
"pinnedMessages": pinnedMessages
}
arg.finish(responseJson)
@ -104,6 +111,7 @@ QtObject:
currentSuggestions*: SuggestionsList
callResult: string
messageList*: OrderedTable[string, ChatMessageList]
pinnedMessagesList*: OrderedTable[string, ChatMessageList]
reactions*: ReactionView
stickers*: StickersView
groups*: GroupsView
@ -129,11 +137,14 @@ QtObject:
self.currentSuggestions.delete
for msg in self.messageList.values:
msg.delete
for msg in self.pinnedMessagesList.values:
msg.delete
self.reactions.delete
self.stickers.delete
self.groups.delete
self.transactions.delete
self.messageList = initOrderedTable[string, ChatMessageList]()
self.pinnedMessagesList = initOrderedTable[string, ChatMessageList]()
self.communities.delete
self.channelOpenTime = initTable[string, int64]()
self.QAbstractListModel.delete
@ -147,6 +158,7 @@ QtObject:
result.contextChannel = newChatItemView(status)
result.currentSuggestions = newSuggestionsList()
result.messageList = initOrderedTable[string, ChatMessageList]()
result.pinnedMessagesList = initOrderedTable[string, ChatMessageList]()
result.reactions = newReactionView(status, result.messageList.addr, result.activeChannel)
result.stickers = newStickersView(status, result.activeChannel)
result.groups = newGroupsView(status,result.activeChannel)
@ -431,6 +443,7 @@ QtObject:
if not self.messageList.hasKey(channel):
self.beginInsertRows(newQModelIndex(), self.messageList.len, self.messageList.len)
self.messageList[channel] = newChatMessageList(channel, self.status, not chat.isNil and chat.chatType != ChatType.Profile)
self.pinnedMessagesList[channel] = newChatMessageList(channel, self.status, false)
self.channelOpenTime[channel] = now().toTime.toUnix * 1000
self.endInsertRows();
@ -451,6 +464,13 @@ QtObject:
proc isAddedContact*(self: ChatsView, id: string): bool {.slot.} =
result = self.status.contacts.isAdded(id)
proc pushPinnedMessages*(self:ChatsView, messages: var seq[Message]) =
for msg in messages.mitems:
self.upsertChannel(msg.chatId)
self.pinnedMessagesList[msg.chatId].add(msg)
# put the message as pinned in the message list
self.messageList[msg.chatId].changeMessagePinned(msg.id, true)
proc pushMessages*(self:ChatsView, messages: var seq[Message]) =
for msg in messages.mitems:
self.upsertChannel(msg.chatId)
@ -532,6 +552,14 @@ QtObject:
read = getMessageList
notify = activeChannelChanged
proc getPinnedMessagesList(self: ChatsView): QVariant {.slot.} =
self.upsertChannel(self.activeChannel.id)
return newQVariant(self.pinnedMessagesList[self.activeChannel.id])
QtProperty[QVariant] pinnedMessagesList:
read = getPinnedMessagesList
notify = activeChannelChanged
proc pushChatItem*(self: ChatsView, chatItem: Chat) =
discard self.chats.addChatItemToList(chatItem)
self.messagePushed(self.messageList[chatItem.id].messages.len - 1)
@ -599,6 +627,10 @@ QtObject:
let reactions = parseReactionsResponse(rpcResponseObj["chatId"].getStr, rpcResponseObj["reactions"])
self.status.chat.chatReactions(rpcResponseObj["chatId"].getStr, true, reactions[0], reactions[1])
if(rpcResponseObj["pinnedMessages"].kind != JNull):
let pinnedMessages = parseChatMessagesResponse(rpcResponseObj["chatId"].getStr, rpcResponseObj["pinnedMessages"])
self.status.chat.pinnedMessagesByChatID(rpcResponseObj["chatId"].getStr, pinnedMessages[0], pinnedMessages[1])
proc hideLoadingIndicator*(self: ChatsView) {.slot.} =
self.loadingMessages = false
self.loadingMessagesChanged(false)
@ -835,6 +867,35 @@ QtObject:
if(id == msg.id): return idx
return idx
proc addPinMessage*(self: ChatsView, messageId: string, chatId: string) =
self.upsertChannel(chatId)
self.messageList[chatId].changeMessagePinned(messageId, true)
self.pinnedMessagesList[chatId].add(self.messageList[chatId].getMessageById(messageId))
proc removePinMessage*(self: ChatsView, messageId: string, chatId: string) =
self.upsertChannel(chatId)
self.messageList[chatId].changeMessagePinned(messageId, false)
try:
self.pinnedMessagesList[chatId].remove(messageId)
except Exception as e:
error "Error removing ", msg = e.msg
proc pinMessage*(self: ChatsView, messageId: string, chatId: string) {.slot.} =
self.status.chat.setPinMessage(messageId, chatId, true)
self.addPinMessage(messageId, chatId)
proc unPinMessage*(self: ChatsView, messageId: string, chatId: string) {.slot.} =
self.status.chat.setPinMessage(messageId, chatId, false)
self.removePinMessage(messageId, chatId)
proc addPinnedMessages*(self: ChatsView, pinnedMessages: seq[Message]) =
for pinnedMessage in pinnedMessages:
if (pinnedMessage.isPinned):
self.addPinMessage(pinnedMessage.id, pinnedMessage.localChatId)
else:
self.removePinMessage(pinnedMessage.id, pinnedMessage.localChatId)
proc isActiveMailserverResult(self: ChatsView, resultEncoded: string) {.slot.} =
let isActiveMailserverAvailable = decode[bool](resultEncoded)
if isActiveMailserverAvailable:

View File

@ -1,4 +1,4 @@
import NimQml, Tables, sets, json, sugar
import NimQml, Tables, sets, json, sugar, chronicles
import ../../../status/status
import ../../../status/accounts
import ../../../status/chat
@ -37,8 +37,9 @@ type
CommunityId = UserRole + 27
HasMention = UserRole + 28
StickerPackId = UserRole + 29
GapFrom = UserRole + 30
GapTo = UserRole + 31
IsPinned = UserRole + 30
GapFrom = UserRole + 31
GapTo = UserRole + 32
QtObject:
type
@ -124,6 +125,15 @@ QtObject:
method rowCount(self: ChatMessageList, index: QModelIndex = nil): int =
return self.messages.len
proc countChanged*(self: ChatMessageList) {.signal.}
proc count*(self: ChatMessageList): int {.slot.} =
self.messages.len
QtProperty[int] count:
read = count
notify = countChanged
proc getReactions*(self:ChatMessageList, messageId: string):string =
if not self.messageReactions.hasKey(messageId): return ""
result = self.messageReactions[messageId]
@ -161,6 +171,7 @@ QtObject:
of ChatMessageRoles.LinkUrls: result = newQVariant(message.linkUrls)
of ChatMessageRoles.CommunityId: result = newQVariant(message.communityId)
of ChatMessageRoles.HasMention: result = newQVariant(message.hasMention)
of ChatMessageRoles.IsPinned: result = newQVariant(message.isPinned)
# Pass the command parameters as a JSON string
of ChatMessageRoles.CommandParameters: result = newQVariant($(%*{
"id": message.commandParameters.id,
@ -205,6 +216,7 @@ QtObject:
ChatMessageRoles.CommunityId.int: "communityId",
ChatMessageRoles.Alias.int:"alias",
ChatMessageRoles.HasMention.int:"hasMention",
ChatMessageRoles.IsPinned.int:"isPinned",
ChatMessageRoles.LocalName.int:"localName",
ChatMessageRoles.StickerPackId.int:"stickerPackId",
ChatMessageRoles.GapFrom.int:"gapFrom",
@ -237,9 +249,11 @@ QtObject:
proc add*(self: ChatMessageList, message: Message) =
if self.messageIndex.hasKey(message.id): return # duplicated msg
debug "New message", chatId = self.id, id = message.id, text = message.text
self.beginInsertRows(newQModelIndex(), self.messages.len, self.messages.len)
self.messageIndex[message.id] = self.messages.len
self.messages.add(message)
self.countChanged()
self.endInsertRows()
proc add*(self: ChatMessageList, messages: seq[Message]) =
@ -248,8 +262,19 @@ QtObject:
if self.messageIndex.hasKey(message.id): continue
self.messageIndex[message.id] = self.messages.len
self.messages.add(message)
self.countChanged()
self.endInsertRows()
proc remove*(self: ChatMessageList, messageId: string) =
if not self.messageIndex.hasKey(messageId): return
let index = self.getMessageIndex(messageId)
self.beginRemoveRows(newQModelIndex(), index, index)
self.messages.delete(index)
self.messageIndex.del(messageId)
self.countChanged()
self.endRemoveRows()
proc getMessageById*(self: ChatMessageList, messageId: string): Message =
if (not self.messageIndex.hasKey(messageId)): return
return self.messages[self.messageIndex[messageId]]
@ -269,6 +294,16 @@ QtObject:
let bottomRight = self.createIndex(msgIdx, 0, nil)
self.dataChanged(topLeft, bottomRight, @[ChatMessageRoles.EmojiReactions.int])
proc changeMessagePinned*(self: ChatMessageList, messageId: string, pinned: bool) =
if not self.messageIndex.hasKey(messageId): return
let msgIdx = self.messageIndex[messageId]
var message = self.messages[msgIdx]
message.isPinned = pinned
self.messages[msgIdx] = message
let topLeft = self.createIndex(msgIdx, 0, nil)
let bottomRight = self.createIndex(msgIdx, 0, nil)
self.dataChanged(topLeft, bottomRight, @[ChatMessageRoles.IsPinned.int])
proc markMessageAsSent*(self: ChatMessageList, messageId: string)=
let topLeft = self.createIndex(0, 0, nil)
let bottomRight = self.createIndex(self.messages.len, 0, nil)

View File

@ -24,6 +24,7 @@ type
ChatUpdateArgs* = ref object of Args
chats*: seq[Chat]
messages*: seq[Message]
pinnedMessages*: seq[Message]
contacts*: seq[Profile]
emojiReactions*: seq[Reaction]
communities*: seq[Community]
@ -56,6 +57,7 @@ type
contacts*: Table[string, Profile]
channels*: Table[string, Chat]
msgCursor*: Table[string, string]
pinnedMsgCursor*: Table[string, string]
emojiCursor*: Table[string, string]
lastMessageTimestamps*: Table[string, int64]
@ -73,6 +75,7 @@ proc newChatModel*(events: EventEmitter): ChatModel =
result.contacts = initTable[string, Profile]()
result.channels = initTable[string, Chat]()
result.msgCursor = initTable[string, string]()
result.pinnedMsgCursor = initTable[string, string]()
result.emojiCursor = initTable[string, string]()
result.lastMessageTimestamps = initTable[string, int64]()
@ -99,7 +102,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]) =
proc update*(self: ChatModel, chats: seq[Chat], messages: seq[Message], emojiReactions: seq[Reaction], communities: seq[Community], communityMembershipRequests: seq[CommunityMembershipRequest], pinnedMessages: seq[Message]) =
var contacts = getAddedContacts()
# Automatically decline chat group invitations if admin is not a contact
@ -118,7 +121,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))
self.events.emit("chatUpdate", ChatUpdateArgs(messages: messages, chats: chatList, contacts: @[], emojiReactions: emojiReactions, communities: communities, communityMembershipRequests: communityMembershipRequests, pinnedMessages: pinnedMessages))
proc hasChannel*(self: ChatModel, chatId: string): bool =
self.channels.hasKey(chatId)
@ -504,3 +507,20 @@ proc pendingRequestsToJoinForCommunity*(self: ChatModel, communityKey: string):
proc myPendingRequestsToJoin*(self: ChatModel): seq[CommunityMembershipRequest] =
result = status_chat.myPendingRequestsToJoin()
proc setPinMessage*(self: ChatModel, messageId: string, chatId: string, pinned: bool) =
status_chat.setPinMessage(messageId, chatId, pinned)
proc pinnedMessagesByChatID*(self: ChatModel, chatId: string): seq[Message] =
if not self.pinnedMsgCursor.hasKey(chatId):
self.pinnedMsgCursor[chatId] = "";
let messageTuple = status_chat.pinnedMessagesByChatID(chatId, self.pinnedMsgCursor[chatId])
self.pinnedMsgCursor[chatId] = messageTuple[0];
result = messageTuple[1]
proc pinnedMessagesByChatID*(self: ChatModel, chatId: string, cursor: string = "", pinnedMessages: seq[Message]) =
self.msgCursor[chatId] = cursor
self.events.emit("pinnedMessagesLoaded", MsgsLoadedArgs(messages: pinnedMessages))

View File

@ -68,6 +68,7 @@ type Message* = object
communityId*: string
audioDurationMs*: int
hasMention*: bool
isPinned*: bool
type Reaction* = object
id*: string

View File

@ -454,3 +454,31 @@ proc banUserFromCommunity*(pubKey: string, communityId: string): string =
"communityId": communityId,
"user": pubKey
}])
proc rpcPinnedChatMessages*(chatId: string, cursorVal: JsonNode, limit: int, success: var bool): string =
success = true
try:
result = callPrivateRPC("chatPinnedMessages".prefix, %* [chatId, cursorVal, limit])
except RpcException as e:
success = false
result = e.msg
proc pinnedMessagesByChatID*(chatId: string, cursor: string): (string, seq[Message]) =
var cursorVal: JsonNode
if cursor == "":
cursorVal = newJNull()
else:
cursorVal = newJString(cursor)
var success: bool
let callResult = rpcPinnedChatMessages(chatId, cursorVal, 20, success)
if success:
result = parseChatMessagesResponse(chatId, callResult.parseJson()["result"])
proc setPinMessage*(messageId: string, chatId: string, pinned: bool) =
discard callPrivateRPC("sendPinMessage".prefix, %*[{
"message_id": messageId,
"pinned": pinned,
"chat_id": chatId
}])

View File

@ -1,7 +1,7 @@
import json, json, options, json_serialization, stint, chronicles
import core, types, utils, strutils, strformat
import utils
from status_go import validateMnemonic, startWallet
from status_go import validateMnemonic#, startWallet
import ../wallet/account
import web3/ethtypes
import ./types

View File

@ -63,10 +63,29 @@ proc fromEvent*(event: JsonNode): Signal =
signal.communities.add(jsonCommunity.toCommunity)
if event["event"]{"requestsToJoinCommunity"} != nil:
debug "requests", event = event["event"]["requestsToJoinCommunity"]
for jsonCommunity in event["event"]["requestsToJoinCommunity"]:
signal.membershipRequests.add(jsonCommunity.toCommunityMembershipRequest)
if event["event"]{"pinMessages"} != nil:
for jsonPinnedMessage in event["event"]["pinMessages"]:
var contentType: ContentType
try:
contentType = ContentType(jsonPinnedMessage{"contentType"}.getInt)
except:
warn "Unknown content type received", type = jsonPinnedMessage{"contentType"}.getInt
contentType = ContentType.Message
signal.pinnedMessages.add(Message(
id: jsonPinnedMessage{"message_id"}.getStr,
chatId: jsonPinnedMessage{"chat_id"}.getStr,
localChatId: jsonPinnedMessage{"localChatId"}.getStr,
fromAuthor: jsonPinnedMessage{"from"}.getStr,
identicon: jsonPinnedMessage{"identicon"}.getStr,
alias: jsonPinnedMessage{"alias"}.getStr,
clock: jsonPinnedMessage{"clock"}.getInt,
isPinned: jsonPinnedMessage{"pinned"}.getBool,
contentType: contentType
))
result = signal
proc toChatMember*(jsonMember: JsonNode): ChatMember =
@ -206,8 +225,6 @@ proc toCommunity*(jsonCommunity: JsonNode): Community =
name: chat{"name"}.getStr,
canPost: chat{"canPost"}.getBool,
chatType: ChatType.CommunityChat
# TODO get this from access
#chat{"permissions"}{"access"}.getInt,
))
if jsonCommunity.hasKey("categories") and jsonCommunity["categories"].kind != JNull:

View File

@ -29,6 +29,7 @@ type EnvelopeExpiredSignal* = ref object of Signal
type MessageSignal* = ref object of Signal
messages*: seq[Message]
pinnedMessages*: seq[Message]
chats*: seq[Chat]
contacts*: seq[Profile]
installations*: seq[Installation]

View File

@ -335,6 +335,7 @@ ScrollView {
communityId: model.communityId
hasMention: model.hasMention
stickerPackId: model.stickerPackId
pinnedMessage: model.isPinned
gapFrom: model.gapFrom
gapTo: model.gapTo
prevMessageIndex: {

View File

@ -29,6 +29,8 @@ Item {
property bool hasMention: false
property string linkUrls: ""
property bool placeholderMessage: false
property bool pinnedMessage: false
property bool forceHoverHandler: false // Used to force the HoverHandler to be active (useful for messages in popups)
property string communityId: ""
property int stickerPackId: -1
property int gapFrom: 0
@ -164,7 +166,7 @@ Item {
}
}
function clickMessage(isProfileClick, isSticker = false, isImage = false, image = null, emojiOnly = false) {
function clickMessage(isProfileClick, isSticker = false, isImage = false, image = null, emojiOnly = false, hideEmojiPicker = false) {
if (isImage) {
imageClick(image);
return;
@ -176,13 +178,18 @@ Item {
// Get contact nickname
let nickname = appMain.getUserNickname(fromAuthor)
messageContextMenu.messageId = root.messageId
messageContextMenu.linkUrls = root.linkUrls
messageContextMenu.isProfile = !!isProfileClick
messageContextMenu.isSticker = isSticker
messageContextMenu.emojiOnly = emojiOnly
messageContextMenu.show(userName, fromAuthor, root.profileImageSource || identicon, "", nickname, emojiReactionsModel)
messageContextMenu.hideEmojiPicker = hideEmojiPicker
messageContextMenu.pinnedMessage = pinnedMessage
messageContextMenu.show(userName, fromAuthor, root.profileImageSource || identicon, plainText, nickname, emojiReactionsModel)
// Position the center of the menu where the mouse is
messageContextMenu.x = messageContextMenu.x - messageContextMenu.width / 2
if (messageContextMenu.x + messageContextMenu.width + Style.current.padding < root.width) {
messageContextMenu.x = messageContextMenu.x - messageContextMenu.width / 2
}
}
Loader {

View File

@ -96,5 +96,26 @@ Rectangle {
text: qsTrId("message-reply")
}
}
StatusIconButton {
id: otherBtn
icon.name: "dots-icon"
width: 32
height: 32
onClicked: {
if (typeof isMessageActive !== "undefined") {
isMessageActive = true
}
clickMessage(false, isSticker, false, null, false, true)
}
onHoveredChanged: {
buttonsContainer.hoverChanged(this.hovered)
}
StatusToolTip {
visible: otherBtn.hovered
text: qsTr("More")
}
}
}
}

View File

@ -67,13 +67,61 @@ Item {
+ (!chatName.visible && chatImageContent.active ? 6 : 0)
+ (emojiReactionLoader.active ? emojiReactionLoader.height: 0)
+ (retry.visible && !chatTime.visible ? Style.current.smallPadding : 0)
+ (pinnedRectangleLoader.active ? Style.current.smallPadding : 0)
width: parent.width
color: root.isHovered || isMessageActive ? (hasMention ? Style.current.mentionMessageHoverColor : Style.current.backgroundHoverLight) :
color: {
if (pinnedMessage) {
return root.isHovered || isMessageActive ? Style.current.pinnedMessageBackgroundHovered : Style.current.pinnedMessageBackground
}
return root.isHovered || isMessageActive ? (hasMention ? Style.current.mentionMessageHoverColor : Style.current.backgroundHoverLight) :
(hasMention ? Style.current.mentionMessageColor : Style.current.transparent)
}
Loader {
id: pinnedRectangleLoader
active: pinnedMessage
anchors.left: chatName.left
anchors.top: parent.top
anchors.topMargin: active ? Style.current.halfPadding : 0
sourceComponent: Component {
Rectangle {
id: pinnedRectangle
height: 24
width: childrenRect.width + Style.current.smallPadding
color: Style.current.pinnedRectangleBackground
radius: 12
SVGImage {
id: pinImage
source: "../../../../img/pin.svg"
anchors.left: parent.left
anchors.leftMargin: 3
anchors.verticalCenter: parent.verticalCenter
ColorOverlay {
anchors.fill: parent
source: parent
color: Style.current.pinnedMessageBorder
}
}
StyledText {
text: qsTr("Pinned")
anchors.left: pinImage.right
anchors.verticalCenter: parent.verticalCenter
font.pixelSize: 13
}
}
}
}
ChatReply {
id: chatReply
anchors.top: pinnedRectangleLoader.active ? pinnedRectangleLoader.bottom : parent.top
anchors.topMargin: active ? 4 : 0
anchors.left: chatImage.left
longReply: active && textFieldImplicitWidth > width
container: root.container
@ -87,8 +135,9 @@ Item {
active: isMessage && headerRepeatCondition
anchors.left: parent.left
anchors.leftMargin: Style.current.padding
anchors.top: chatReply.active ? chatReply.bottom : parent.top
anchors.topMargin: Style.current.smallPadding
anchors.top: chatReply.active ? chatReply.bottom :
pinnedRectangleLoader.active ? pinnedRectangleLoader.bottom : parent.top
anchors.topMargin: chatReply.active || pinnedRectangleLoader.active ? 4 : Style.current.smallPadding
}
UsernameLabel {
@ -245,14 +294,15 @@ Item {
}
Loader {
active: hasMention
active: hasMention || pinnedMessage
height: messageContainer.height
anchors.left: messageContainer.left
anchors.top: messageContainer.top
sourceComponent: Component {
Rectangle {
id: mentionBorder
color: Style.current.mentionColor
color: pinnedMessage ? Style.current.pinnedMessageBorder : Style.current.mentionColor
width: 2
height: parent.height
}
@ -260,7 +310,7 @@ Item {
}
HoverHandler {
enabled: typeof messageContextMenu !== "undefined" && typeof profilePopupOpened !== "undefined" && !messageContextMenu.opened && !profilePopupOpened && !popupOpened
enabled: forceHoverHandler || (typeof messageContextMenu !== "undefined" && typeof profilePopupOpened !== "undefined" && !messageContextMenu.opened && !profilePopupOpened && !popupOpened)
onHoveredChanged: setHovered(messageId, hovered)
}

View File

@ -64,6 +64,9 @@ Item {
}
}
PinnedMessagesPopup {
id: pinnedMessagesPopup
}
StatusContextMenuButton {
id: moreActionsBtn
anchors.verticalCenter: parent.verticalCenter

View File

@ -8,9 +8,12 @@ import "../../../../shared/status"
import "./"
PopupMenu {
property string messageId
property bool isProfile: false
property bool isSticker: false
property bool emojiOnly: false
property bool hideEmojiPicker: false
property bool pinnedMessage: false
property string linkUrls: ""
property alias emojiContainer: emojiContainer
@ -53,7 +56,7 @@ PopupMenu {
Item {
id: emojiContainer
visible: messageContextMenu.emojiOnly || !messageContextMenu.isProfile
visible: !hideEmojiPicker && (messageContextMenu.emojiOnly || !messageContextMenu.isProfile)
width: emojiRow.width
height: visible ? emojiRow.height : 0
@ -134,6 +137,37 @@ PopupMenu {
visible: !messageContextMenu.emojiOnly
}
Action {
id: pinAction
text: pinnedMessage ? qsTr("Unpin") :
qsTr("Pin")
onTriggered: {
if (pinnedMessage) {
chatsModel.unPinMessage(messageId, chatsModel.activeChannel.id)
return
}
chatsModel.pinMessage(messageId, chatsModel.activeChannel.id)
messageContextMenu.close()
}
icon.source: "../../../img/pin"
icon.width: 16
icon.height: 16
enabled: chatsModel.activeChannel.chatType !== Constants.chatTypePublic
}
Action {
id: copyAction
text: qsTr("Copy")
onTriggered: {
chatsModel.copyToClipboard(messageContextMenu.text)
messageContextMenu.close()
}
icon.source: "../../../../shared/img/copy-to-clipboard-icon"
icon.width: 16
icon.height: 16
}
Action {
id: copyLinkAction
text: qsTr("Copy link")

View File

@ -0,0 +1,90 @@
import QtQuick 2.13
import "../../../../imports"
import "../../../../shared"
import "../../../../shared/status"
import "../ChatColumn"
ModalPopup {
id: popup
header: Item {
height: childrenRect.height
width: parent.width
StyledText {
id: title
text: qsTr("Pinned messages")
anchors.top: parent.top
anchors.left: parent.left
font.bold: true
font.pixelSize: 17
}
StyledText {
id: nbPinnedMessages
text: qsTr("%1 message").arg(pinnedMessageListView.count)
anchors.left: parent.left
anchors.top: title.bottom
anchors.topMargin: 2
font.pixelSize: 15
color: Style.current.secondaryText
}
Separator {
anchors.top: nbPinnedMessages.bottom
anchors.topMargin: Style.current.padding
anchors.left: parent.left
anchors.leftMargin: -Style.current.padding
anchors.right: parent.right
anchors.rightMargin: -Style.current.padding
}
}
ListView {
id: pinnedMessageListView
model: chatsModel.pinnedMessagesList
height: parent.height
anchors.left: parent.left
anchors.leftMargin: -Style.current.padding
anchors.right: parent.right
anchors.rightMargin: -Style.current.padding
clip: true
delegate: Message {
fromAuthor: model.fromAuthor
chatId: model.chatId
userName: model.userName
alias: model.alias
localName: model.localName
message: model.message
plainText: model.plainText
identicon: model.identicon
isCurrentUser: model.isCurrentUser
timestamp: model.timestamp
sticker: model.sticker
contentType: model.contentType
outgoingStatus: model.outgoingStatus
responseTo: model.responseTo
imageClick: imagePopup.openPopup.bind(imagePopup)
messageId: model.messageId
emojiReactions: model.emojiReactions
linkUrls: model.linkUrls
communityId: model.communityId
hasMention: model.hasMention
stickerPackId: model.stickerPackId
timeout: model.timeout
pinnedMessage: true
forceHoverHandler: true
}
}
footer: StatusRoundButton {
id: btnBack
anchors.left: parent.left
icon.name: "arrow-right"
icon.width: 20
icon.height: 16
rotation: 180
onClicked: popup.close()
}
}

4
ui/app/img/pin.svg Normal file
View File

@ -0,0 +1,4 @@
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8.6891 4.24964C8.6891 3.7664 8.31098 3.27219 7.84455 3.14582C7.37812 3.01944 7 3.30874 7 3.79199C7 4.27524 7.37812 4.76944 7.84455 4.89582C8.31098 5.02219 8.6891 4.73289 8.6891 4.24964Z" fill="#939BA1"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M7 1.16699C5.067 1.16699 3.5 2.734 3.5 4.66699C3.5 6.36315 4.70653 7.7775 6.30817 8.09863C6.45243 8.12755 6.5625 8.25034 6.5625 8.39747L6.5625 12.2503C6.5625 12.492 6.75837 12.6878 7 12.6878C7.24162 12.6878 7.4375 12.492 7.4375 12.2503L7.4375 8.39747C7.4375 8.25034 7.54757 8.12755 7.69183 8.09863C9.29347 7.7775 10.5 6.36315 10.5 4.66699C10.5 2.734 8.933 1.16699 7 1.16699ZM4.375 4.66699C4.375 6.11674 5.55025 7.29199 7 7.29199C8.44975 7.29199 9.625 6.11674 9.625 4.66699C9.625 3.21724 8.44975 2.04199 7 2.04199C5.55025 2.04199 4.375 3.21724 4.375 4.66699Z" fill="#939BA1"/>
</svg>

After

Width:  |  Height:  |  Size: 942 B

View File

@ -90,6 +90,11 @@ Theme {
property color contextMenuButtonForegroundColor: midGrey
property color contextMenuButtonBackgroundHoverColor: Qt.hsla(white.hslHue, white.hslSaturation, white.hslLightness, 0.05)
property color pinnedMessageBorder: "#FFA67B"
property color pinnedMessageBackground: "#1afe8f59"
property color pinnedMessageBackgroundHovered: "#33fe8f59"
property color pinnedRectangleBackground: "#1afe8f59"
property color roundedButtonForegroundColor: white
property color roundedButtonBackgroundColor: buttonBackgroundColor
property color roundedButtonSecondaryForegroundColor: black

View File

@ -90,6 +90,11 @@ Theme {
property color contextMenuButtonForegroundColor: black
property color contextMenuButtonBackgroundHoverColor: Qt.hsla(black.hslHue, black.hslSaturation, black.hslLightness, 0.1)
property color pinnedMessageBorder: "#FE8F59"
property color pinnedMessageBackground: "#1aFF9F0F"
property color pinnedMessageBackgroundHovered: "#33FF9F0F"
property color pinnedRectangleBackground: "#1affffff"
property color roundedButtonForegroundColor: buttonForegroundColor
property color roundedButtonBackgroundColor: secondaryBackground
property color roundedButtonSecondaryForegroundColor: grey2

View File

@ -72,6 +72,11 @@ QtObject {
property color tooltipBackgroundColor
property color tooltipForegroundColor
property color pinnedMessageBorder
property color pinnedMessageBackground
property color pinnedMessageBackgroundHovered
property color pinnedRectangleBackground
property int xlPadding: 32
property int bigPadding: 24
property int padding: 16

View File

@ -162,6 +162,7 @@ DISTFILES += \
app/AppLayouts/Chat/components/EmojiReaction.qml \
app/AppLayouts/Chat/components/LeftTabBottomButtons.qml \
app/AppLayouts/Chat/components/NoFriendsRectangle.qml \
app/AppLayouts/Chat/components/PinnedMessagesPopup.qml \
app/AppLayouts/Chat/components/ProfilePopup.qml \
app/AppLayouts/Chat/components/EmojiSection.qml \
app/AppLayouts/Chat/components/InviteFriendsPopup.qml \

View File

@ -137,6 +137,59 @@ Item {
font.pixelSize: 12
anchors.top: chatName.bottom
}
Item {
property bool hovered: false
id: pinnedMessagesGroup
visible: chatType !== Constants.chatTypePublic && chatsModel.pinnedMessagesList.count > 0
width: childrenRect.width
height: vertiSep.height
anchors.left: chatInfo.right
anchors.leftMargin: 4
anchors.verticalCenter: chatInfo.verticalCenter
Rectangle {
id: vertiSep
height: 12
width: 1
color: Style.current.border
}
SVGImage {
id: pinImg
source: "../../app/img/pin.svg"
height: 14
width: 14
anchors.left: vertiSep.right
anchors.leftMargin: 4
anchors.verticalCenter: vertiSep.verticalCenter
ColorOverlay {
anchors.fill: parent
source: parent
color: pinnedMessagesGroup.hovered ? Style.current.textColor : Style.current.secondaryText
}
}
StyledText {
id: nbPinnedMessagesText
color: pinnedMessagesGroup.hovered ? Style.current.textColor : Style.current.secondaryText
text: chatsModel.pinnedMessagesList.count
font.pixelSize: 12
font.underline: pinnedMessagesGroup.hovered
anchors.left: pinImg.right
anchors.verticalCenter: vertiSep.verticalCenter
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
onEntered: pinnedMessagesGroup.hovered = true
onExited: pinnedMessagesGroup.hovered = false
onClicked: pinnedMessagesPopup.open()
}
}
}
}

2
vendor/status-go vendored

@ -1 +1 @@
Subproject commit 71f66f68064e9897cd17b6bcecba426a91405034
Subproject commit e9a42bfa2be93d9ee09a82e0893d8019c4bcdd3d