feat(@desktop/chat): Align chat member and suggestion

Removed all computation of suggestion from qml
Reuse user list in order to populate the suggestion box

As a side effect, the suggestion are not serialized from qml to
nim

Remove InputArea which seems not used anymore
This commit is contained in:
Anthony Laibe 2021-08-24 11:10:26 +02:00 committed by Iuri Matias
parent 06b31c69c1
commit 94f6041ec5
13 changed files with 97 additions and 335 deletions

View File

@ -10,7 +10,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, format_input, ens, activity_notification_list, channel, messages, message_item, gif]
import views/[channels_list, message_list, chat_item, reactions, stickers, groups, transactions, communities, community_list, community_item, format_input, ens, activity_notification_list, channel, messages, message_item, gif]
import ../utils/image_utils
import ../../status/tasks/[qt, task_runner_impl]
import ../../status/tasks/marathon/mailserver/worker
@ -78,7 +78,6 @@ QtObject:
channelView*: ChannelView
messageView*: MessageView
messageSearchViewController: MessageSearchViewController
currentSuggestions*: SuggestionsList
activityNotificationList*: ActivityNotificationList
callResult: string
reactions*: ReactionView
@ -97,7 +96,6 @@ QtObject:
proc delete(self: ChatsView) =
self.formatInputView.delete
self.ensView.delete
self.currentSuggestions.delete
self.activityNotificationList.delete
self.reactions.delete
self.stickers.delete
@ -119,7 +117,6 @@ QtObject:
result.messageSearchViewController = newMessageSearchViewController(status,
result.channelView, result.communities)
result.connected = false
result.currentSuggestions = newSuggestionsList()
result.activityNotificationList = newActivityNotificationList(status)
result.reactions = newReactionView(
status,
@ -221,12 +218,6 @@ QtObject:
return status_ens.userNameOrAlias(self.status.chat.contacts[pubKey])
generateAlias(pubKey)
proc getCurrentSuggestions(self: ChatsView): QVariant {.slot.} =
return newQVariant(self.currentSuggestions)
QtProperty[QVariant] suggestionList:
read = getCurrentSuggestions
proc activityNotificationsChanged*(self: ChatsView) {.signal.}
proc getActivityNotificationList(self: ChatsView): QVariant {.slot.} =
@ -336,16 +327,20 @@ QtObject:
self.communities.updateCommunityChat(chat)
if(self.channelView.activeChannel.id == chat.id):
self.activeChannelChanged()
continue
self.messageView.upsertChannel(chat.id)
self.channelView.chats.updateChat(chat)
if(self.channelView.activeChannel.id == chat.id):
self.channelView.activeChannel.setChatItem(chat)
self.activeChannelChanged()
self.currentSuggestions.setNewData(self.status.contacts.getContacts())
if self.channelView.contextChannel.id == chat.id:
self.channelView.contextChannel.setChatItem(chat)
self.channelView.contextChannelChanged()
self.messageView.calculateUnreadMessages()
proc isConnected*(self: ChatsView): bool {.slot.} =

View File

@ -56,7 +56,7 @@ QtObject:
isEdited*: Table[string, bool]
messageReactions*: Table[string, string]
timedoutMessages: HashSet[string]
userList: UserListView
userList*: UserListView
loadingHistoryMessages: bool
initialMessagesLoaded: bool

View File

@ -65,63 +65,55 @@ QtObject:
proc setLoadingHistoryMessages*(self: MessageView, chatId: string, value: bool)
proc setInitialMessagesLoaded*(self: MessageView, chatId: string, value: bool)
proc replaceMentionsWithPubKeys(self: MessageView, mentions: seq[string], contacts: seq[Profile], message: string, predicate: proc (contact: Profile): string): string =
var updatedMessage = message
for mention in mentions:
let matches = contacts.filter(c => "@" & predicate(c).toLowerAscii == mention.toLowerAscii).map(c => c.address)
if matches.len > 0:
let pubKey = matches[0]
var startIndex = 0
var index = updatedMessage.find(mention)
while index > -1:
if index == 0 or updatedMessage[index-1] == ' ':
updatedMessage = updatedMessage.replaceWord(mention, '@' & pubKey)
startIndex = index + mention.len
index = updatedMessage.find(mention, startIndex)
result = updatedMessage
proc hideMessage(self: MessageView, mId: string) {.signal.}
proc sendOrEditMessage*(self: MessageView, message: string, replyTo: string, contentType: int = ContentType.Message.int, isStatusUpdate: bool = false, contactsString: string = "", isEdit: bool = false, messageId: string = "") {.slot.} =
proc replaceMentionsWithPubKeys(self: MessageView, message: string): string =
let aliasPattern = re(r"(@[A-z][a-z]+ [A-z][a-z]* [A-z][a-z]*)", flags = {reStudy, reIgnoreCase})
let ensPattern = re(r"(@\w+(?=(\.stateofus)?\.eth))", flags = {reStudy, reIgnoreCase})
let namePattern = re(r"(@\w+)", flags = {reStudy, reIgnoreCase})
var contacts: seq[Profile]
if (contactsString == ""):
contacts = self.status.contacts.getContacts()
else:
let contactsJSON = parseJson(contactsString)
contacts = @[]
for contact in contactsJSON:
contacts.add(Profile(
address: contact["address"].str,
alias: contact["alias"].str,
ensName: contact["ensName"].str
))
let aliasMentions = findAll(message, aliasPattern)
let ensMentions = findAll(message, ensPattern)
let nameMentions = findAll(message, namePattern)
var updatedMessage = message
var m = self.replaceMentionsWithPubKeys(aliasMentions, contacts, message, (c => c.alias))
m = self.replaceMentionsWithPubKeys(ensMentions, contacts, m, (c => c.ensName))
m = self.replaceMentionsWithPubKeys(nameMentions, contacts, m, (c => c.ensName.split(".")[0]))
for publicKey in self.messageList[self.channelView.activeChannel.id].userList.users:
let user = self.messageList[self.channelView.activeChannel.id].userList.userDetails[publicKey]
for mention in aliasMentions:
if "@" & user.alias.toLowerAscii != mention.toLowerAscii:
continue
updatedMessage = updatedMessage.replaceWord(mention, '@' & publicKey)
for mention in ensMentions:
if "@" & user.userName.toLowerAscii != mention.toLowerAscii:
continue
updatedMessage = updatedMessage.replaceWord(mention, '@' & publicKey)
for mention in nameMentions:
if "@" & user.userName.split(".")[0].toLowerAscii != mention.toLowerAscii:
continue
updatedMessage = updatedMessage.replaceWord(mention, '@' & publicKey)
return updatedMessage
proc hideMessage(self: MessageView, mId: string) {.signal.}
proc sendOrEditMessage*(self: MessageView, message: string, replyTo: string, contentType: int = ContentType.Message.int, isStatusUpdate: bool = false, isEdit: bool = false, messageId: string = "") {.slot.} =
let updatedMessage = self.replaceMentionsWithPubKeys(message)
var channelId = self.channelView.activeChannel.id
if isStatusUpdate:
channelId = "@" & self.pubKey
if not isEdit:
self.status.chat.sendMessage(channelId, m, replyTo, contentType)
self.status.chat.sendMessage(channelId, updatedMessage, replyTo, contentType)
else:
self.status.chat.editMessage(messageId, m)
self.status.chat.editMessage(messageId, updatedMessage)
proc sendMessage*(self: MessageView, message: string, replyTo: string, contentType: int = ContentType.Message.int, isStatusUpdate: bool = false, contactsString: string = "") {.slot.} =
self.sendOrEditMessage(message, replyTo, contentType, isStatusUpdate, contactsString, false, "")
proc sendMessage*(self: MessageView, message: string, replyTo: string, contentType: int = ContentType.Message.int, isStatusUpdate: bool = false) {.slot.} =
self.sendOrEditMessage(message, replyTo, contentType, isStatusUpdate, false, "")
proc verifyMessageSent*(self: MessageView, data: string) {.slot.} =
let messageData = data.parseJson
@ -138,8 +130,8 @@ QtObject:
proc messageEdited(self: MessageView, editedMessageId: string, editedMessageContent: string) {.signal.}
proc editMessage*(self: MessageView, messageId: string, originalMessageId: string, message: string, contactsString: string = "") {.slot.} =
self.sendOrEditMessage(message, "", ContentType.Message.int, false, contactsString, true, originalMessageId)
proc editMessage*(self: MessageView, messageId: string, originalMessageId: string, message: string) {.slot.} =
self.sendOrEditMessage(message, "", ContentType.Message.int, false, true, originalMessageId)
self.messageEdited(originalMessageId, message)
proc messagePushed*(self: MessageView, messageIndex: int) {.signal.}

View File

@ -1,77 +0,0 @@
import NimQml, tables
import ../../../status/profile/profile
type
SuggestionRoles {.pure.} = enum
Alias = UserRole + 1,
Identicon = UserRole + 2,
Address = UserRole + 3,
EnsName = UserRole + 4,
EnsVerified = UserRole + 5
LocalNickname = UserRole + 6
QtObject:
type SuggestionsList* = ref object of QAbstractListModel
suggestions*: seq[Profile]
proc setup(self: SuggestionsList) = self.QAbstractListModel.setup
proc delete(self: SuggestionsList) =
self.suggestions = @[]
self.QAbstractListModel.delete
proc newSuggestionsList*(): SuggestionsList =
new(result, delete)
result.suggestions = @[]
result.setup
proc rowData(self: SuggestionsList, index: int, column: string): string {.slot.} =
if (index >= self.suggestions.len):
return
let suggestion = self.suggestions[index]
case column:
of "alias": result = suggestion.alias
of "ensName": result = suggestion.ensName
of "address": result = suggestion.address
of "identicon": result = suggestion.identicon
of "localNickname": result = suggestion.localNickname
method rowCount(self: SuggestionsList, index: QModelIndex = nil): int =
return self.suggestions.len
method data(self: SuggestionsList, index: QModelIndex, role: int): QVariant =
if not index.isValid:
return
if index.row < 0 or index.row >= self.suggestions.len:
return
let suggestion = self.suggestions[index.row]
let suggestionRole = role.SuggestionRoles
case suggestionRole:
of SuggestionRoles.Alias: result = newQVariant(suggestion.alias)
of SuggestionRoles.Identicon: result = newQVariant(suggestion.identicon)
of SuggestionRoles.Address: result = newQVariant(suggestion.address)
of SuggestionRoles.EnsName: result = newQVariant(suggestion.ensName)
of SuggestionRoles.EnsVerified: result = newQVariant(suggestion.ensVerified)
of SuggestionRoles.LocalNickname: result = newQVariant(suggestion.localNickname)
method roleNames(self: SuggestionsList): Table[int, string] =
{ SuggestionRoles.Alias.int:"alias",
SuggestionRoles.Identicon.int:"identicon",
SuggestionRoles.Address.int:"address",
SuggestionRoles.EnsName.int:"ensName",
SuggestionRoles.LocalNickname.int:"localNickname",
SuggestionRoles.EnsVerified.int:"ensVerified" }.toTable
proc addSuggestionToList*(self: SuggestionsList, profile: Profile) =
self.beginInsertRows(newQModelIndex(), self.suggestions.len, self.suggestions.len)
self.suggestions.add(profile)
self.endInsertRows()
proc setNewData*(self: SuggestionsList, suggestionsList: seq[Profile]) =
self.beginResetModel()
self.suggestions = suggestionsList
self.endResetModel()
proc forceUpdate*(self: SuggestionsList) =
self.beginResetModel()
self.endResetModel()

View File

@ -17,8 +17,8 @@ type
Identicon = UserRole + 6
User = object
username: string
alias: string
username*: string
alias*: string
localName: string
lastSeen: string
identicon: string
@ -27,8 +27,8 @@ QtObject:
type
UserListView* = ref object of QAbstractListModel
status: Status
users: seq[string]
userDetails: OrderedTable[string, User]
users*: seq[string]
userDetails*: OrderedTable[string, User]
proc delete(self: UserListView) =
self.userDetails.clear()
@ -44,6 +44,19 @@ QtObject:
result.status = status
result.setup
proc rowData(self: UserListView, index: int, column: string): string {.slot.} =
if (index >= self.users.len):
return
let publicKey = self.users[index]
case column:
of "publicKey": result = publicKey
of "userName": result = self.userDetails[publicKey].username
of "lastSeen": result = self.userDetails[publicKey].lastSeen
of "alias": result = self.userDetails[publicKey].alias
of "localName": result = self.userDetails[publicKey].localName
of "identicon": result = self.userdetails[publicKey].identicon
method rowCount*(self: UserListView, index: QModelIndex = nil): int = self.users.len
method data(self: UserListView, index: QModelIndex, role: int): QVariant =
@ -55,7 +68,7 @@ QtObject:
let pubkey = self.users[index.row]
case role.UserListRoles:
of UserListRoles.UserName: result = newQVariant(self.userDetails[pubkey].userName)
of UserListRoles.UserName: result = newQVariant(self.userDetails[pubkey].username)
of UserListRoles.LastSeen: result = newQVariant(self.userDetails[pubkey].lastSeen)
of UserListRoles.Alias: result = newQVariant(self.userDetails[pubkey].alias)
of UserListRoles.LocalName: result = newQVariant(self.userDetails[pubkey].localName)

View File

@ -327,6 +327,7 @@ QtObject:
if (forceActiveChat):
chats[0].isActive = true
self.events.emit("chatUpdate", ChatUpdateArgs(messages: messages, chats: chats, contacts: @[]))
for msg in messages:
self.events.emit("sendingMessage", MessageArgs(id: msg.id, channel: msg.chatId))

View File

@ -1,4 +1,4 @@
import json
import json, strformat
import ../types
type Profile* = ref object
@ -11,6 +11,8 @@ type Profile* = ref object
appearance*: int
systemTags*: seq[string]
proc `$`*(self: Profile): string =
return fmt"Profile(id:{self.id}, username:{self.username})"
const contactAdded* = ":contact/added"
const contactBlocked* = ":contact/blocked"

View File

@ -41,7 +41,6 @@ Item {
property string activeMessage
property var currentTime: 0
property var idMap: ({})
property var suggestionsObj: ([])
property Timer timer: Timer { }
property var userList
property var onActivated: function () {
@ -64,52 +63,6 @@ Item {
}
}
function addSuggestionFromMessageList(i){
const contactAddr = chatsModel.messageView.messageList.getMessageData(i, "publicKey");
if(idMap[contactAddr]) return;
suggestionsObj.push({
alias: chatsModel.messageView.messageList.getMessageData(i, "alias"),
ensName: chatsModel.messageView.messageList.getMessageData(i, "ensName"),
address: contactAddr,
identicon: chatsModel.messageView.messageList.getMessageData(i, "identicon"),
localNickname: chatsModel.messageView.messageList.getMessageData(i, "localName")
})
chatInput.suggestionsList.append(suggestionsObj[suggestionsObj.length - 1]);
idMap[contactAddr] = true;
}
function populateSuggestions() {
chatInput.suggestionsList.clear()
idMap = {}
let isCommunity = chatsModel.communities.activeCommunity.active
let dataSource = isCommunity ? chatsModel.communities.activeCommunity.members : chatsModel.suggestionList
const len = dataSource.rowCount()
for (let i = 0; i < len; i++) {
const contactAddr = dataSource.rowData(i, "address");
if(idMap[contactAddr]) continue;
suggestionsObj.push({
alias: dataSource.rowData(i, "alias"),
ensName: dataSource.rowData(i, "ensName"),
address: contactAddr,
identicon: getProfileImage(contactAddr, false, false) || dataSource.rowData(i, "identicon"),
localNickname: dataSource.rowData(i, "localNickname")
})
chatInput.suggestionsList.append(suggestionsObj[suggestionsObj.length - 1]);
idMap[contactAddr] = true;
}
if (isCommunity) return;
const len2 = chatsModel.messageView.messageList.rowCount();
for (let f = 0; f < len2; f++) {
addSuggestionFromMessageList(f);
}
}
function showReplyArea() {
isReply = true;
isImage = false;
@ -356,21 +309,6 @@ Item {
isBlocked = profileModel.contacts.isContactBlocked(activeChatId);
chatInput.suggestions.hide();
chatInput.textInput.forceActiveFocus(Qt.MouseFocusReason)
populateSuggestions();
}
}
Connections {
target: chatsModel.messageView
onMessagePushed: {
addSuggestionFromMessageList(messageIndex);
}
}
Connections {
target: profileModel
onContactsChanged: {
populateSuggestions();
}
}
@ -462,7 +400,7 @@ Item {
let msg = chatsModel.plainText(Emoji.deparse(chatInput.textInput.text))
if (msg.length > 0){
msg = chatInput.interpretMessage(msg)
chatsModel.messageView.sendMessage(msg, chatInput.isReply ? SelectedMessage.messageId : "", Utils.isOnlyEmoji(msg) ? Constants.emojiType : Constants.messageType, false, JSON.stringify(suggestionsObj));
chatsModel.messageView.sendMessage(msg, chatInput.isReply ? SelectedMessage.messageId : "", Utils.isOnlyEmoji(msg) ? Constants.emojiType : Constants.messageType, false);
if(event) event.accepted = true
sendMessageSound.stop();
Qt.callLater(sendMessageSound.play);

View File

@ -1,95 +0,0 @@
import QtQuick 2.13
import "../../../../../imports"
import "../../../../../shared"
import "../../../../../shared/status"
import "../../components"
Item {
property alias chatInput: chatInput
id: inputArea
height: chatInput.height
Connections {
target: chatsModel.messageView
onLoadingMessagesChanged:
if(value){
loadingMessagesIndicator.active = true
} else {
timer.setTimeout(function(){
loadingMessagesIndicator.active = false;
}, 5000);
}
}
Loader {
id: loadingMessagesIndicator
active: chatsModel.messageView.loadingMessages
sourceComponent: loadingIndicator
anchors.right: parent.right
anchors.bottom: chatInput.top
anchors.rightMargin: Style.current.padding
anchors.bottomMargin: Style.current.padding
}
Component {
id: loadingIndicator
LoadingAnimation {}
}
StatusChatInput {
id: chatInput
visible: {
const community = chatsModel.communities.activeCommunity
if (chatsModel.channelView.activeChannel.chatType === Constants.chatTypePrivateGroupChat) {
return chatsModel.channelView.activeChannel.isMember
}
return !community.active ||
community.access === Constants.communityChatPublicAccess ||
community.admin ||
chatsModel.channelView.activeChannel.canPost
}
isContactBlocked: isBlocked
chatInputPlaceholder: isBlocked ?
//% "This user has been blocked."
qsTrId("this-user-has-been-blocked-") :
//% "Type a message."
qsTrId("type-a-message-")
anchors.bottom: parent.bottom
recentStickers: chatsModel.stickers.recent
stickerPackList: chatsModel.stickers.stickerPacks
chatType: chatsModel.channelView.activeChannel.chatType
onSendTransactionCommandButtonClicked: {
if (chatsModel.channelView.activeChannel.ensVerified) {
txModalLoader.sourceComponent = cmpSendTransactionWithEns
} else {
txModalLoader.sourceComponent = cmpSendTransactionNoEns
}
txModalLoader.item.open()
}
onReceiveTransactionCommandButtonClicked: {
txModalLoader.sourceComponent = cmpReceiveTransaction
txModalLoader.item.open()
}
onStickerSelected: {
chatsModel.stickers.send(hashId, packId)
}
onSendMessage: {
if (chatInput.fileUrls.length > 0){
chatsModel.sendImages(JSON.stringify(fileUrls));
}
let msg = chatsModel.plainText(Emoji.deparse(chatInput.textInput.text))
if (msg.length > 0){
msg = chatInput.interpretMessage(msg)
chatsModel.messageView.sendMessage(msg, chatInput.isReply ? SelectedMessage.messageId : "", Utils.isOnlyEmoji(msg) ? Constants.emojiType : Constants.messageType, false, JSON.stringify(suggestionsObj));
if(event) event.accepted = true
sendMessageSound.stop();
Qt.callLater(sendMessageSound.play);
chatInput.textInput.clear();
chatInput.textInput.textFormat = TextEdit.PlainText;
chatInput.textInput.textFormat = TextEdit.RichText;
}
}
}
}

View File

@ -224,14 +224,6 @@ Item {
StatusChatInput {
id: editTextInput
readonly property string originalText: Utils.getMessageWithStyle(Emoji.parse(message))
Component.onCompleted: {
suggestionsList.clear()
for (let i = 0; i < chatInput.suggestionsList.count; i++) {
suggestionsList.append(chatInput.suggestionsList.get(i))
}
textInput.forceActiveFocus()
textInput.cursorPosition = textInput.length
}
chatInputPlaceholder: qsTrId("type-a-message-")
chatType: chatsModel.channelView.activeChannel.chatType
isEdit: true
@ -274,7 +266,7 @@ Item {
if (msg.length > 0){
msg = chatInput.interpretMessage(msg)
isEdit = false
chatsModel.messageView.editMessage(messageId, contentType == Constants.editType ? replaces : messageId, msg, JSON.stringify(suggestionsObj));
chatsModel.messageView.editMessage(messageId, contentType == Constants.editType ? replaces : messageId, msg);
}
}
}

View File

@ -32,9 +32,16 @@ Item {
if (!isFilteringPropertyOk())
return
var length = sourceModel.count
var length = sourceModel.rowCount()
for (var i = 0; i < length; ++i) {
var item = sourceModel.get(i);
const publicKey = sourceModel.rowData(i, "publicKey");
var item = {
alias: sourceModel.rowData(i, "alias"),
userName: sourceModel.rowData(i, "userName"),
publicKey: publicKey,
identicon: getProfileImage(publicKey, false, false) || sourceModel.rowData(i, "identicon"),
localName: sourceModel.rowData(i, "localName")
}
if (isAcceptedItem(item)) {
filterModel.append(item)
}

View File

@ -106,7 +106,6 @@ DISTFILES += \
app/AppLayouts/Chat/ChatColumn/ChatComponents/ChatCommandsPopup.qml \
app/AppLayouts/Chat/ChatColumn/ChatComponents/ChatInputButton.qml \
app/AppLayouts/Chat/ChatColumn/ChatComponents/ChatRequestMessage.qml \
app/AppLayouts/Chat/ChatColumn/ChatComponents/InputArea.qml \
app/AppLayouts/Chat/ChatColumn/ChatComponents/RequestModal.qml \
app/AppLayouts/Chat/ChatColumn/ChatComponents/SignTransactionModal.qml \
app/AppLayouts/Chat/ChatColumn/CompactMessage.qml \

View File

@ -47,11 +47,10 @@ Rectangle {
property var fileUrls: []
property alias suggestionsList: suggestions
property alias suggestions: suggestionsBox
property var imageErrorMessageLocation: StatusChatInput.ImageErrorMessageLocation.Top
property alias suggestions: suggestionsBox
enum ImageErrorMessageLocation {
Top,
Bottom
@ -522,10 +521,6 @@ Rectangle {
}
}
ListModel {
id: suggestions
}
FileDialog {
id: imageDialog
//% "Please choose an image"
@ -567,17 +562,17 @@ Rectangle {
SuggestionBox {
id: suggestionsBox
model: suggestions
model: chatsModel.messageView.messageList.userList
x : messageInput.x
y: -height - Style.current.smallPadding
width: messageInput.width
filter: messageInputField.text
cursorPosition: messageInputField.cursorPosition
property: ["ensName", "localNickname", "alias"]
property: ["userName", "localName", "alias"]
onItemSelected: function (item, lastAtPosition, lastCursorPosition) {
const hasEmoji = Emoji.hasEmoji(messageInputField.text)
const properties = "ensName, alias"; // Ignore localNickname
const properties = "userName, alias"; // Ignore localName
let aliasName = item[properties.split(",").map(p => p.trim()).find(p => !!item[p])]
aliasName = aliasName.replace(/(\.stateofus)?\.eth/, "")