parent
00f10f600a
commit
1e39cf4821
|
@ -1,4 +1,4 @@
|
|||
import NimQml, Tables, json, sequtils, chronicles, times, strutils
|
||||
import NimQml, Tables, json, sequtils, chronicles, times, re, sugar, strutils
|
||||
|
||||
import ../../status/status
|
||||
import ../../status/accounts as status_accounts
|
||||
|
@ -11,7 +11,7 @@ import ../../status/profile/profile
|
|||
|
||||
import ../../status/threads
|
||||
|
||||
import views/channels_list, views/message_list, views/chat_item, views/sticker_pack_list, views/sticker_list
|
||||
import views/channels_list, views/message_list, views/chat_item, views/sticker_pack_list, views/sticker_list, views/suggestions_list
|
||||
|
||||
logScope:
|
||||
topics = "chats-view"
|
||||
|
@ -21,6 +21,7 @@ QtObject:
|
|||
ChatsView* = ref object of QAbstractListModel
|
||||
status: Status
|
||||
chats*: ChannelsList
|
||||
currentSuggestions*: SuggestionsList
|
||||
callResult: string
|
||||
messageList: Table[string, ChatMessageList]
|
||||
activeChannel*: ChatItemView
|
||||
|
@ -35,6 +36,7 @@ QtObject:
|
|||
proc delete(self: ChatsView) =
|
||||
self.chats.delete
|
||||
self.activeChannel.delete
|
||||
self.currentSuggestions.delete
|
||||
for msg in self.messageList.values:
|
||||
msg.delete
|
||||
self.messageList = initTable[string, ChatMessageList]()
|
||||
|
@ -47,6 +49,7 @@ QtObject:
|
|||
result.connected = false
|
||||
result.chats = newChannelsList(status)
|
||||
result.activeChannel = newChatItemView(status)
|
||||
result.currentSuggestions = newSuggestionsList()
|
||||
result.messageList = initTable[string, ChatMessageList]()
|
||||
result.stickerPacks = newStickerPackList()
|
||||
result.recentStickers = newStickerList()
|
||||
|
@ -70,8 +73,30 @@ QtObject:
|
|||
proc getChannelColor*(self: ChatsView, channel: string): string {.slot.} =
|
||||
self.chats.getChannelColor(channel)
|
||||
|
||||
proc replaceMentionsWithPubKeys(self: ChatsView, mentions: seq[string], contacts: seq[Profile], message: string, predicate: proc (contact: Profile): string): string =
|
||||
result = message
|
||||
for mention in mentions:
|
||||
let matches = contacts.filter(c => "@" & predicate(c) == mention).map(c => c.address)
|
||||
if matches.len > 0:
|
||||
let pubKey = matches[0]
|
||||
result = message.replace(mention, "@" & pubKey)
|
||||
|
||||
proc sendMessage*(self: ChatsView, message: string, replyTo: string) {.slot.} =
|
||||
self.status.chat.sendMessage(self.activeChannel.id, message, replyTo)
|
||||
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})
|
||||
|
||||
let contacts = self.status.contacts.getContacts()
|
||||
|
||||
let aliasMentions = findAll(message, aliasPattern)
|
||||
let ensMentions = findAll(message, ensPattern)
|
||||
let nameMentions = findAll(message, namePattern)
|
||||
|
||||
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]))
|
||||
|
||||
self.status.chat.sendMessage(self.activeChannel.id, m, replyTo)
|
||||
|
||||
proc activeChannelChanged*(self: ChatsView) {.signal.}
|
||||
|
||||
|
@ -93,6 +118,7 @@ QtObject:
|
|||
|
||||
self.activeChannel.setChatItem(selectedChannel)
|
||||
self.status.chat.setActiveChannel(selectedChannel.id)
|
||||
self.currentSuggestions.setNewData(self.status.contacts.getContacts())
|
||||
self.activeChannelChanged()
|
||||
|
||||
proc getActiveChannelIdx(self: ChatsView): QVariant {.slot.} =
|
||||
|
@ -122,6 +148,7 @@ QtObject:
|
|||
proc setActiveChannel*(self: ChatsView, channel: string) {.slot.} =
|
||||
if(channel == ""): return
|
||||
self.activeChannel.setChatItem(self.chats.getChannel(self.chats.chats.findIndexById(channel)))
|
||||
self.currentSuggestions.setNewData(self.status.contacts.getContacts())
|
||||
self.activeChannelChanged()
|
||||
|
||||
proc getActiveChannel*(self: ChatsView): QVariant {.slot.} =
|
||||
|
@ -132,6 +159,13 @@ QtObject:
|
|||
write = setActiveChannel
|
||||
notify = activeChannelChanged
|
||||
|
||||
|
||||
proc getCurrentSuggestions(self: ChatsView): QVariant {.slot.} =
|
||||
return newQVariant(self.currentSuggestions)
|
||||
|
||||
QtProperty[QVariant] suggestionList:
|
||||
read = getCurrentSuggestions
|
||||
|
||||
proc upsertChannel(self: ChatsView, channel: string) =
|
||||
if not self.messageList.hasKey(channel):
|
||||
self.messageList[channel] = newChatMessageList(channel, self.status)
|
||||
|
@ -222,6 +256,7 @@ QtObject:
|
|||
self.chats.updateChat(chat)
|
||||
if(self.activeChannel.id == chat.id):
|
||||
self.activeChannel.setChatItem(chat)
|
||||
self.currentSuggestions.setNewData(self.status.contacts.getContacts())
|
||||
self.activeChannelChanged()
|
||||
|
||||
proc renameGroup*(self: ChatsView, newName: string) {.slot.} =
|
||||
|
|
|
@ -0,0 +1,73 @@
|
|||
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
|
||||
|
||||
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 - 1):
|
||||
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
|
||||
|
||||
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)
|
||||
|
||||
method roleNames(self: SuggestionsList): Table[int, string] =
|
||||
{ SuggestionRoles.Alias.int:"alias",
|
||||
SuggestionRoles.Identicon.int:"identicon",
|
||||
SuggestionRoles.Address.int:"address",
|
||||
SuggestionRoles.EnsName.int:"ensName",
|
||||
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()
|
|
@ -108,6 +108,57 @@ StackLayout {
|
|||
}
|
||||
}
|
||||
|
||||
ListModel {
|
||||
id: suggestions
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: chatsModel
|
||||
onActiveChannelChanged: {
|
||||
suggestions.clear()
|
||||
for (let i = 0; i < chatsModel.suggestionList.rowCount(); i++) {
|
||||
suggestions.append({
|
||||
alias: chatsModel.suggestionList.rowData(i, "alias"),
|
||||
ensName: chatsModel.suggestionList.rowData(i, "ensName"),
|
||||
address: chatsModel.suggestionList.rowData(i, "address"),
|
||||
identicon: chatsModel.suggestionList.rowData(i, "identicon"),
|
||||
ensVerified: chatsModel.suggestionList.rowData(i, "ensVerified")
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SuggestionBox {
|
||||
id: suggestionsBox
|
||||
model: suggestions
|
||||
width: chatContainer.width
|
||||
anchors.bottom: inputArea.top
|
||||
anchors.left: inputArea.left
|
||||
filter: chatInput.textInput.text
|
||||
property: "ensName, alias"
|
||||
onItemSelected: function (item) {
|
||||
let currentText = chatInput.textInput.text
|
||||
let lastAt = currentText.lastIndexOf("@")
|
||||
let aliasName = item[suggestionsBox.property.split(",").map(p => p.trim()).find(p => !!item[p])]
|
||||
let nameLen = aliasName.length + 2 // We're doing a +2 here because of the `@` and the trailing whitespace
|
||||
let position = 0;
|
||||
let text = ""
|
||||
|
||||
if (currentText.length == 1) {
|
||||
position = nameLen
|
||||
text = "@" + aliasName + " "
|
||||
} else {
|
||||
let left = currentText.slice(0, lastAt)
|
||||
position = left.length + nameLen
|
||||
text = left + "@" + aliasName + " "
|
||||
}
|
||||
|
||||
chatInput.textInput.text = text
|
||||
chatInput.textInput.cursorPosition = position
|
||||
suggestionsBox.suggestionsModel.clear()
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: inputArea
|
||||
color: Style.current.background
|
||||
|
@ -126,6 +177,7 @@ StackLayout {
|
|||
}
|
||||
|
||||
ChatInput {
|
||||
id: chatInput
|
||||
height: 40
|
||||
anchors.top: !isReply ? inputArea.top : replyAreaContainer.bottom
|
||||
anchors.topMargin: 4
|
||||
|
|
|
@ -8,6 +8,7 @@ import "../../../../imports"
|
|||
|
||||
Rectangle {
|
||||
id: rectangle
|
||||
property alias textInput: txtData
|
||||
border.width: 0
|
||||
height: 52
|
||||
color: Style.current.transparent
|
||||
|
@ -31,12 +32,14 @@ Rectangle {
|
|||
}
|
||||
|
||||
function onEnter(event){
|
||||
|
||||
if (event.modifiers === Qt.NoModifier && (event.key === Qt.Key_Enter || event.key === Qt.Key_Return)) {
|
||||
|
||||
if(txtData.text.trim().length > 0){
|
||||
let msg = interpretMessage(txtData.text.trim())
|
||||
chatsModel.sendMessage(msg, chatColumn.isReply ? SelectedMessage.messageId : "");
|
||||
txtData.text = "";
|
||||
event.accepted = true;
|
||||
event.accepted = true
|
||||
sendMessageSound.stop()
|
||||
Qt.callLater(sendMessageSound.play);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,137 @@
|
|||
/*
|
||||
Copyright (C) 2011 Jocelyn Turcotte <turcotte.j@gmail.com>
|
||||
|
||||
This program is free software; you can redistribute it and/or
|
||||
modify it under the terms of the GNU Library General Public
|
||||
License as published by the Free Software Foundation; either
|
||||
version 2 of the License, or (at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
Library General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Library General Public License
|
||||
along with this program; see the file COPYING.LIB. If not, write to
|
||||
the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
|
||||
Boston, MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
import QtQuick 2.13
|
||||
import QtQuick.Controls 2.13
|
||||
import QtGraphicalEffects 1.13
|
||||
import "../../../../imports"
|
||||
import "../../../../shared"
|
||||
|
||||
Rectangle {
|
||||
id: container
|
||||
|
||||
property QtObject model: undefined
|
||||
property Item delegate
|
||||
property alias suggestionsModel: filterItem.model
|
||||
property alias filter: filterItem.filter
|
||||
property alias property: filterItem.property
|
||||
signal itemSelected(var item)
|
||||
|
||||
|
||||
z: parent.z + 100
|
||||
visible: filter.length > 0 && suggestionsModel.count > 0
|
||||
height: visible ? childrenRect.height + (Style.current.padding * 2) : 0
|
||||
opacity: visible ? 1.0 : 0
|
||||
Behavior on opacity {
|
||||
NumberAnimation { }
|
||||
}
|
||||
|
||||
// --- defaults
|
||||
color: Style.current.white2
|
||||
radius: 16
|
||||
layer.enabled: true
|
||||
layer.effect: DropShadow{
|
||||
width: container.width
|
||||
height: container.height
|
||||
x: container.x
|
||||
y: container.y + 10
|
||||
visible: container.visible
|
||||
source: container
|
||||
horizontalOffset: 0
|
||||
verticalOffset: 2
|
||||
radius: 10
|
||||
samples: 15
|
||||
color: "#22000000"
|
||||
}
|
||||
|
||||
SuggestionFilter {
|
||||
id: filterItem
|
||||
sourceModel: container.model
|
||||
}
|
||||
|
||||
|
||||
ScrollView {
|
||||
id: popup
|
||||
height: items.height >= 400 ? 400 : items.height
|
||||
width: parent.width
|
||||
anchors.centerIn: parent
|
||||
clip: true
|
||||
|
||||
property int selectedIndex
|
||||
property var selectedItem: selectedIndex == -1 ? null : model[selectedIndex]
|
||||
signal suggestionClicked(var item)
|
||||
ScrollBar.vertical.policy: items.contentHeight > items.height ? ScrollBar.AlwaysOn : ScrollBar.AlwaysOff
|
||||
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
|
||||
|
||||
Column {
|
||||
id: items
|
||||
clip: true
|
||||
height: childrenRect.height
|
||||
width: parent.width
|
||||
Repeater {
|
||||
id: repeater
|
||||
model: container.suggestionsModel
|
||||
delegate: Rectangle {
|
||||
id: delegateItem
|
||||
property var suggestion: model
|
||||
property bool hovered
|
||||
|
||||
height: 50
|
||||
width: container.width
|
||||
color: hovered ? Style.current.blue : "white"
|
||||
|
||||
Identicon {
|
||||
id: accountImage
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Style.current.smallPadding
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
source: suggestion.identicon
|
||||
}
|
||||
|
||||
Text {
|
||||
id: textComponent
|
||||
color: delegateItem.hovered ? Style.current.white : Style.current.black
|
||||
text: suggestion[container.property.split(",").map(p => p.trim()).find(p => !!suggestion[p])]
|
||||
width: parent.width
|
||||
height: parent.height
|
||||
anchors.left: accountImage.right
|
||||
anchors.leftMargin: Style.current.padding
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
font.pixelSize: 15
|
||||
}
|
||||
MouseArea {
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
onEntered: {
|
||||
delegateItem.hovered = true
|
||||
}
|
||||
onExited: {
|
||||
delegateItem.hovered = false
|
||||
}
|
||||
onClicked: container.itemSelected(delegateItem.suggestion)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,79 @@
|
|||
import QtQuick 2.13
|
||||
|
||||
Item {
|
||||
id: component
|
||||
property alias model: filterModel
|
||||
|
||||
property QtObject sourceModel: undefined
|
||||
property string filter: ""
|
||||
property string property: ""
|
||||
|
||||
Connections {
|
||||
onFilterChanged: invalidateFilter()
|
||||
onPropertyChanged: invalidateFilter()
|
||||
onSourceModelChanged: invalidateFilter()
|
||||
}
|
||||
|
||||
Component.onCompleted: invalidateFilter()
|
||||
|
||||
ListModel {
|
||||
id: filterModel
|
||||
}
|
||||
|
||||
function invalidateFilter() {
|
||||
if (sourceModel === undefined)
|
||||
return;
|
||||
|
||||
filterModel.clear();
|
||||
|
||||
if (!isFilteringPropertyOk())
|
||||
return
|
||||
|
||||
var length = sourceModel.count
|
||||
for (var i = 0; i < length; ++i) {
|
||||
var item = sourceModel.get(i);
|
||||
if (isAcceptedItem(item)) {
|
||||
filterModel.append(item)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function isAcceptedItem(item) {
|
||||
let properties = this.property.split(",")
|
||||
.map(p => p.trim())
|
||||
.filter(p => !!item[p])
|
||||
|
||||
if (properties.length == 0) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (this.filter.endsWith("@")) {
|
||||
return true
|
||||
}
|
||||
|
||||
let lastAt = this.filter.lastIndexOf("@")
|
||||
|
||||
if (lastAt == -1) {
|
||||
return false
|
||||
}
|
||||
|
||||
let filterWithoutAt = this.filter.substring(lastAt+1)
|
||||
|
||||
if (filterWithoutAt == "") {
|
||||
return true
|
||||
}
|
||||
|
||||
return !properties.every(p => item[p].toLowerCase().match(filterWithoutAt.toLowerCase()) == null)
|
||||
}
|
||||
|
||||
function isFilteringPropertyOk() {
|
||||
if(this.property === undefined || this.property === "") {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -1,2 +1,4 @@
|
|||
ContactsColumn 1.0 ContactsColumn.qml
|
||||
ChatColumn 1.0 ChatColumn.qml
|
||||
SuggestionBox 1.0 SuggestionBox.qml
|
||||
SuggestionFilter 1.0 SuggestionFilter.qml
|
||||
|
|
Loading…
Reference in New Issue