fix(@desktop/chat): rework members selector
fixes: #5941 fixes: #7234 fixes: #7235 fixes: #7236 fixes: #7237 fixes: #7238 fixes: #7239 fixes: #7240
This commit is contained in:
parent
2d5c69bc6c
commit
6760870dc9
|
@ -99,11 +99,11 @@ proc init*(self: Controller) =
|
|||
self.events.on(SIGNAL_CONTACT_UNTRUSTWORTHY) do(e: Args):
|
||||
var args = TrustArgs(e)
|
||||
self.delegate.contactUpdated(args.publicKey)
|
||||
|
||||
|
||||
self.events.on(SIGNAL_CONTACT_TRUSTED) do(e: Args):
|
||||
var args = TrustArgs(e)
|
||||
self.delegate.contactUpdated(args.publicKey)
|
||||
|
||||
|
||||
self.events.on(SIGNAL_REMOVED_TRUST_STATUS) do(e: Args):
|
||||
var args = TrustArgs(e)
|
||||
self.delegate.contactUpdated(args.publicKey)
|
||||
|
@ -181,3 +181,9 @@ proc getContactDetails*(self: Controller, contactId: string): ContactDetails =
|
|||
|
||||
proc getStatusForContact*(self: Controller, contactId: string): StatusUpdateDto =
|
||||
return self.contactService.getStatusForContactWithId(contactId)
|
||||
|
||||
proc addGroupMembers*(self: Controller, pubKeys: seq[string]) =
|
||||
self.chatService.addGroupMembers("", self.chatId, pubKeys)
|
||||
|
||||
proc removeGroupMembers*(self: Controller, pubKeys: seq[string]) =
|
||||
self.chatService.removeMembersFromGroupChat("", self.chatId, pubKeys)
|
||||
|
|
|
@ -57,3 +57,9 @@ method viewDidLoad*(self: AccessInterface) {.base.} =
|
|||
|
||||
method getMembersPublicKeys*(self: AccessInterface): string {.base.} =
|
||||
raise newException(ValueError, "No implementation available")
|
||||
|
||||
method addGroupMembers*(self: AccessInterface, pubKeys: seq[string]) {.base.} =
|
||||
raise newException(ValueError, "No implementation available")
|
||||
|
||||
method removeGroupMembers*(self: AccessInterface, pubKeys: seq[string]) {.base.} =
|
||||
raise newException(ValueError, "No implementation available")
|
||||
|
|
|
@ -140,7 +140,7 @@ method addChatMember*(self: Module, member: ChatMember) =
|
|||
else:
|
||||
let statusUpdateDto = self.controller.getStatusForContact(member.id)
|
||||
status = toOnlineStatus(statusUpdateDto.statusType)
|
||||
|
||||
|
||||
self.view.model().addItem(initMemberItem(
|
||||
pubKey = member.id,
|
||||
displayName = contactDetails.details.displayName,
|
||||
|
@ -186,7 +186,7 @@ method onChatMembersAddedOrRemoved*(self: Module, ids: seq[string]) =
|
|||
self.onChatMembersAdded(membersAdded)
|
||||
for id in membersRemoved:
|
||||
self.onChatMemberRemoved(id)
|
||||
|
||||
|
||||
|
||||
method onChatMemberUpdated*(self: Module, publicKey: string, admin: bool, joined: bool) =
|
||||
let contactDetails = self.controller.getContactDetails(publicKey)
|
||||
|
@ -207,3 +207,8 @@ method getMembersPublicKeys*(self: Module): string =
|
|||
let publicKeys = self.controller.getMembersPublicKeys()
|
||||
return publicKeys.join(" ")
|
||||
|
||||
method addGroupMembers*(self: Module, pubKeys: seq[string]) =
|
||||
self.controller.addGroupMembers(pubKeys)
|
||||
|
||||
method removeGroupMembers*(self: Module, pubKeys: seq[string]) =
|
||||
self.controller.removeGroupMembers(pubKeys)
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import NimQml
|
||||
import ../../../../shared_models/member_model
|
||||
import NimQml, sequtils, sugar
|
||||
import ../../../../shared_models/[member_model, member_item]
|
||||
import io_interface
|
||||
|
||||
QtObject:
|
||||
|
@ -8,6 +8,8 @@ QtObject:
|
|||
delegate: io_interface.AccessInterface
|
||||
model: Model
|
||||
modelVariant: QVariant
|
||||
temporaryModel: Model # used for editing purposes
|
||||
temporaryModelVariant: QVariant
|
||||
|
||||
proc delete*(self: View) =
|
||||
self.model.delete
|
||||
|
@ -20,6 +22,8 @@ QtObject:
|
|||
result.delegate = delegate
|
||||
result.model = newModel()
|
||||
result.modelVariant = newQVariant(result.model)
|
||||
result.temporaryModel = newModel()
|
||||
result.temporaryModelVariant = newQVariant(result.temporaryModel)
|
||||
|
||||
proc model*(self: View): Model =
|
||||
return self.model
|
||||
|
@ -38,3 +42,39 @@ QtObject:
|
|||
|
||||
proc getMembersPublicKeys*(self: View): string {.slot.} =
|
||||
return self.delegate.getMembersPublicKeys()
|
||||
|
||||
proc temporaryModelChanged*(self: View) {.signal.}
|
||||
|
||||
proc getTemporaryModel(self: View): QVariant {.slot.} =
|
||||
return self.temporaryModelVariant
|
||||
|
||||
QtProperty[QVariant]temporaryModel:
|
||||
read = getTemporaryModel
|
||||
notify = temporaryModelChanged
|
||||
|
||||
proc resetTemporaryModel*(self: View) {.slot.} =
|
||||
self.temporaryModel.setItems(self.model.getItems())
|
||||
|
||||
proc appendTemporaryModel*(self: View, pubKey: string, displayName: string) {.slot.} =
|
||||
# for temporary model only pubKey and displayName is needed
|
||||
let userItem = initMemberItem(
|
||||
pubKey = pubKey,
|
||||
displayName = displayName,
|
||||
ensName = "",
|
||||
localNickname = "",
|
||||
alias = "",
|
||||
icon = "",
|
||||
colorId = 0,
|
||||
)
|
||||
self.temporaryModel.addItem(userItem)
|
||||
|
||||
proc removeFromTemporaryModel*(self: View, pubKey: string) {.slot.} =
|
||||
self.temporaryModel.removeItemById(pubKey)
|
||||
|
||||
proc updateGroupMembers*(self: View) {.slot.} =
|
||||
let modelIDs = self.model.getItemIds()
|
||||
let temporaryModelIDs = self.temporaryModel.getItemIds()
|
||||
let membersAdded = filter(temporaryModelIDs, id => not modelIDs.contains(id))
|
||||
let membersRemoved = filter(modelIDs, id => not temporaryModelIDs.contains(id))
|
||||
self.delegate.addGroupMembers(membersAdded)
|
||||
self.delegate.removeGroupMembers(membersRemoved)
|
||||
|
|
|
@ -52,6 +52,9 @@ QtObject:
|
|||
self.endResetModel()
|
||||
self.countChanged()
|
||||
|
||||
proc getItems*(self: Model): seq[MemberItem] =
|
||||
self.items
|
||||
|
||||
proc `$`*(self: Model): string =
|
||||
for i in 0 ..< self.items.len:
|
||||
result &= fmt"""Member Model:
|
||||
|
@ -142,27 +145,12 @@ QtObject:
|
|||
result = newQVariant(item.requestToJoinId)
|
||||
|
||||
proc addItem*(self: Model, item: MemberItem) =
|
||||
# we need to maintain online contact on top, that means
|
||||
# if we add an item online status we add it as the last online item (before the first offline item)
|
||||
# if we add an item with offline status we add it as the first offline item (after the last online item)
|
||||
var position = -1
|
||||
for i in 0 ..< self.items.len:
|
||||
if(self.items[i].onlineStatus == OnlineStatus.Inactive):
|
||||
position = i
|
||||
break
|
||||
|
||||
if(position == -1):
|
||||
position = self.items.len
|
||||
|
||||
let parentModelIndex = newQModelIndex()
|
||||
defer: parentModelIndex.delete
|
||||
|
||||
self.beginInsertRows(parentModelIndex, position, position)
|
||||
self.items.insert(item, position)
|
||||
self.beginInsertRows(newQModelIndex(), self.items.len, self.items.len)
|
||||
self.items.add(item)
|
||||
self.endInsertRows()
|
||||
self.countChanged()
|
||||
|
||||
proc findIndexForMessageId(self: Model, pubKey: string): int =
|
||||
proc findIndexForMember(self: Model, pubKey: string): int =
|
||||
for i in 0 ..< self.items.len:
|
||||
if(self.items[i].pubKey == pubKey):
|
||||
return i
|
||||
|
@ -179,11 +167,11 @@ QtObject:
|
|||
self.countChanged()
|
||||
|
||||
proc isContactWithIdAdded*(self: Model, id: string): bool =
|
||||
return self.findIndexForMessageId(id) != -1
|
||||
return self.findIndexForMember(id) != -1
|
||||
|
||||
proc setName*(self: Model, pubKey: string, displayName: string,
|
||||
ensName: string, localNickname: string) =
|
||||
let ind = self.findIndexForMessageId(pubKey)
|
||||
let ind = self.findIndexForMember(pubKey)
|
||||
if(ind == -1):
|
||||
return
|
||||
|
||||
|
@ -199,7 +187,7 @@ QtObject:
|
|||
])
|
||||
|
||||
proc setIcon*(self: Model, pubKey: string, icon: string) =
|
||||
let ind = self.findIndexForMessageId(pubKey)
|
||||
let ind = self.findIndexForMember(pubKey)
|
||||
if(ind == -1):
|
||||
return
|
||||
|
||||
|
@ -221,7 +209,7 @@ QtObject:
|
|||
joined: bool,
|
||||
isUntrustworthy: bool,
|
||||
) =
|
||||
let ind = self.findIndexForMessageId(pubKey)
|
||||
let ind = self.findIndexForMember(pubKey)
|
||||
if(ind == -1):
|
||||
return
|
||||
|
||||
|
@ -259,7 +247,7 @@ QtObject:
|
|||
isContact: bool,
|
||||
isUntrustworthy: bool,
|
||||
) =
|
||||
let ind = self.findIndexForMessageId(pubKey)
|
||||
let ind = self.findIndexForMember(pubKey)
|
||||
if(ind == -1):
|
||||
return
|
||||
|
||||
|
@ -284,7 +272,7 @@ QtObject:
|
|||
|
||||
proc setOnlineStatus*(self: Model, pubKey: string,
|
||||
onlineStatus: OnlineStatus) =
|
||||
let ind = self.findIndexForMessageId(pubKey)
|
||||
let ind = self.findIndexForMember(pubKey)
|
||||
if(ind == -1):
|
||||
return
|
||||
|
||||
|
@ -298,7 +286,7 @@ QtObject:
|
|||
|
||||
# TODO: rename me to removeItemByPubkey
|
||||
proc removeItemById*(self: Model, pubKey: string) =
|
||||
let ind = self.findIndexForMessageId(pubKey)
|
||||
let ind = self.findIndexForMember(pubKey)
|
||||
if(ind == -1):
|
||||
return
|
||||
|
||||
|
|
|
@ -0,0 +1,234 @@
|
|||
import QtQuick 2.14
|
||||
import QtQuick.Controls 2.14
|
||||
import QtQuick.Layouts 1.14
|
||||
import QtGraphicalEffects 1.0
|
||||
|
||||
import StatusQ.Core 0.1
|
||||
import StatusQ.Controls 0.1
|
||||
import StatusQ.Core.Theme 0.1
|
||||
import StatusQ.Core.Utils 0.1
|
||||
import StatusQ.Popups.Dialog 0.1
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property alias model: listView.model
|
||||
property alias delegate: listView.delegate
|
||||
|
||||
property alias suggestionsModel: suggestionsListView.model
|
||||
property alias suggestionsDelegate: suggestionsListView.delegate
|
||||
|
||||
readonly property alias label: label
|
||||
readonly property alias warningLabel: warningLabel
|
||||
readonly property alias edit: edit
|
||||
|
||||
signal confirmed()
|
||||
signal rejected()
|
||||
|
||||
signal entryAccepted(var suggestionsDelegate)
|
||||
signal entryRemoved(var delegate)
|
||||
|
||||
implicitWidth: mainLayout.implicitWidth
|
||||
implicitHeight: mainLayout.implicitHeight
|
||||
|
||||
RowLayout {
|
||||
id: mainLayout
|
||||
|
||||
spacing: 8
|
||||
|
||||
anchors.fill: parent
|
||||
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: 44
|
||||
color: Theme.palette.baseColor2
|
||||
radius: 8
|
||||
|
||||
RowLayout {
|
||||
anchors {
|
||||
fill: parent
|
||||
leftMargin: 16
|
||||
rightMargin: 16
|
||||
topMargin: 7
|
||||
bottomMargin: 7
|
||||
}
|
||||
|
||||
StatusBaseText {
|
||||
id: label
|
||||
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
visible: text !== ""
|
||||
font.pixelSize: 15
|
||||
color: Theme.palette.baseColor1
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
|
||||
StatusScrollView {
|
||||
id: scrollView
|
||||
|
||||
anchors.fill: parent
|
||||
|
||||
padding: 0
|
||||
|
||||
onContentWidthChanged: {
|
||||
if (scrollView.contentWidth > scrollView.width) {
|
||||
scrollView.contentX = scrollView.contentWidth - scrollView.width
|
||||
} else {
|
||||
scrollView.contentX = 0
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
height: scrollView.height
|
||||
|
||||
spacing: 8
|
||||
|
||||
StatusListView {
|
||||
id: listView
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
implicitWidth: contentWidth
|
||||
|
||||
orientation: ListView.Horizontal
|
||||
spacing: 8
|
||||
}
|
||||
|
||||
TextInput {
|
||||
id: edit
|
||||
|
||||
Layout.fillHeight: true
|
||||
Layout.minimumWidth: 4
|
||||
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
font.pixelSize: 15
|
||||
color: Theme.palette.directColor1
|
||||
|
||||
cursorDelegate: Rectangle {
|
||||
color: Theme.palette.primaryColor1
|
||||
implicitWidth: 2
|
||||
radius: 1
|
||||
visible: edit.cursorVisible
|
||||
|
||||
SequentialAnimation on visible {
|
||||
loops: Animation.Infinite
|
||||
running: edit.cursorVisible
|
||||
PropertyAnimation { to: false; duration: 600; }
|
||||
PropertyAnimation { to: true; duration: 600; }
|
||||
}
|
||||
}
|
||||
|
||||
Keys.onPressed: {
|
||||
if (suggestionsDialog.visible) {
|
||||
if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) {
|
||||
root.entryAccepted(suggestionsListView.itemAtIndex(suggestionsListView.currentIndex))
|
||||
} else if (event.key === Qt.Key_Up) {
|
||||
suggestionsListView.decrementCurrentIndex()
|
||||
} else if (event.key === Qt.Key_Down) {
|
||||
suggestionsListView.incrementCurrentIndex()
|
||||
}
|
||||
} else {
|
||||
if (event.key === Qt.Key_Backspace && edit.text === "") {
|
||||
root.entryRemoved(listView.itemAtIndex(listView.count - 1))
|
||||
} else if (event.key === Qt.Key_Return || event.key === Qt.Enter) {
|
||||
root.confirmed()
|
||||
} else if (event.key === Qt.Key_Escape) {
|
||||
root.rejected()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ensure edit cursor is visible
|
||||
Item {
|
||||
Layout.fillHeight: true
|
||||
implicitWidth: 1
|
||||
}
|
||||
}
|
||||
|
||||
ScrollBar.horizontal: StatusScrollBar {
|
||||
id: scrollBar
|
||||
parent: scrollView.parent
|
||||
anchors.top: scrollView.bottom
|
||||
anchors.left: scrollView.left
|
||||
anchors.right: scrollView.right
|
||||
policy: ScrollBar.AsNeeded
|
||||
visible: resolveVisibility(policy, scrollView.width, scrollView.contentWidth)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StatusBaseText {
|
||||
id: warningLabel
|
||||
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
visible: text !== ""
|
||||
font.pixelSize: 10
|
||||
color: Theme.palette.dangerColor1
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
propagateComposedEvents: true
|
||||
|
||||
onPressed: {
|
||||
edit.forceActiveFocus()
|
||||
mouse.accepted = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StatusButton {
|
||||
Layout.leftMargin: 10
|
||||
text: qsTr("Confirm")
|
||||
|
||||
onClicked: root.confirmed()
|
||||
}
|
||||
|
||||
StatusButton {
|
||||
text: qsTr("Reject")
|
||||
type: StatusBaseButton.Type.Danger
|
||||
|
||||
onClicked: root.rejected()
|
||||
}
|
||||
}
|
||||
|
||||
Popup {
|
||||
id: suggestionsDialog
|
||||
|
||||
visible: edit.text !== "" && root.suggestionsModel.count
|
||||
|
||||
parent: edit
|
||||
y: parent.height
|
||||
|
||||
padding: 8
|
||||
|
||||
background: StatusDialogBackground {
|
||||
id: bg
|
||||
layer.enabled: true
|
||||
layer.effect: DropShadow {
|
||||
source: bg
|
||||
horizontalOffset: 0
|
||||
verticalOffset: 4
|
||||
radius: 12
|
||||
samples: 25
|
||||
spread: 0.2
|
||||
color: Theme.palette.dropShadow
|
||||
}
|
||||
}
|
||||
|
||||
StatusListView {
|
||||
id: suggestionsListView
|
||||
|
||||
anchors.fill: parent
|
||||
implicitWidth: contentItem.childrenRect.width
|
||||
implicitHeight: contentItem.childrenRect.height
|
||||
|
||||
onVisibleChanged: currentIndex = 0
|
||||
}
|
||||
}
|
||||
}
|
|
@ -25,13 +25,26 @@ RowLayout {
|
|||
|
||||
signal searchButtonClicked()
|
||||
|
||||
QtObject {
|
||||
id: d
|
||||
|
||||
readonly property string stateInfoButtonContent: ""
|
||||
readonly property string stateMembersSelectorContent: "selectingMembers"
|
||||
|
||||
readonly property bool selectingMembers: root.state == stateMembersSelectorContent
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: loader
|
||||
sourceComponent: statusChatInfoButton
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
Layout.alignment: Qt.AlignLeft
|
||||
Layout.leftMargin: padding
|
||||
|
||||
sourceComponent: {
|
||||
if (d.selectingMembers) return membersSelector
|
||||
return statusChatInfoButton
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
|
@ -39,6 +52,7 @@ RowLayout {
|
|||
Layout.alignment: Qt.AlignRight
|
||||
Layout.rightMargin: padding
|
||||
spacing: 8
|
||||
visible: !d.selectingMembers
|
||||
|
||||
StatusFlatRoundButton {
|
||||
id: searchButton
|
||||
|
@ -209,7 +223,7 @@ RowLayout {
|
|||
)
|
||||
}
|
||||
onAddRemoveGroupMember: {
|
||||
loader.sourceComponent = contactsSelector
|
||||
root.state = d.stateMembersSelectorContent
|
||||
}
|
||||
onFetchMoreMessages: {
|
||||
root.rootStore.messageStore.requestMoreMessages();
|
||||
|
@ -237,8 +251,6 @@ RowLayout {
|
|||
}
|
||||
}
|
||||
|
||||
Keys.onEscapePressed: { loader.sourceComponent = statusChatInfoButton }
|
||||
|
||||
// Chat toolbar content option 1:
|
||||
Component {
|
||||
id: statusChatInfoButton
|
||||
|
@ -329,13 +341,15 @@ RowLayout {
|
|||
|
||||
// Chat toolbar content option 2:
|
||||
Component {
|
||||
id: contactsSelector
|
||||
GroupChatPanel {
|
||||
id: membersSelector
|
||||
|
||||
MembersEditSelectorView {
|
||||
sectionModule: root.chatSectionModule
|
||||
chatContentModule: root.chatContentModule
|
||||
rootStore: root.rootStore
|
||||
maxHeight: root.height
|
||||
onPanelClosed: loader.sourceComponent = statusChatInfoButton
|
||||
|
||||
onConfirmed: root.state = d.stateInfoButtonContent
|
||||
onRejected: root.state = d.stateInfoButtonContent
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,129 +11,88 @@ import StatusQ.Core.Theme 0.1
|
|||
|
||||
import utils 1.0
|
||||
import shared.status 1.0
|
||||
import shared.controls.delegates 1.0
|
||||
|
||||
Page {
|
||||
id: root
|
||||
Behavior on anchors.bottomMargin { NumberAnimation { duration: 30 }}
|
||||
|
||||
property ListModel contactsModel: ListModel { }
|
||||
property var rootStore
|
||||
property var emojiPopup: null
|
||||
|
||||
Keys.onEscapePressed: Global.closeCreateChatView()
|
||||
|
||||
StatusListView {
|
||||
id: contactsModelListView
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
model: root.rootStore.contactsModel
|
||||
delegate: Item {
|
||||
property string pubKey: model.pubKey
|
||||
property string displayName: model.displayName
|
||||
property string icon: model.icon
|
||||
}
|
||||
}
|
||||
|
||||
onVisibleChanged: {
|
||||
if (visible) {
|
||||
for (var i = 0; i < contactsModelListView.count; i ++) {
|
||||
var entry = contactsModelListView.itemAtIndex(i);
|
||||
contactsModel.insert(contactsModel.count,
|
||||
{"pubKey": entry.pubKey, "displayName": entry.displayName,
|
||||
"icon": entry.icon});
|
||||
}
|
||||
tagSelector.sortModel(root.contactsModel);
|
||||
} else {
|
||||
contactsModel.clear();
|
||||
tagSelector.namesModel.clear();
|
||||
}
|
||||
}
|
||||
|
||||
function createChat() {
|
||||
if (tagSelector.namesModel.count === 1) {
|
||||
var ensName = tagSelector.namesModel.get(0).name.includes(".eth") ? tagSelector.namesModel.get(0).name : "";
|
||||
root.rootStore.chatCommunitySectionModule.createOneToOneChat("", tagSelector.namesModel.get(0).pubKey, ensName);
|
||||
} else {
|
||||
var groupName = "";
|
||||
var pubKeys = [];
|
||||
for (var i = 0; i < tagSelector.namesModel.count; i++) {
|
||||
groupName += (tagSelector.namesModel.get(i).name + (i === tagSelector.namesModel.count - 1 ? "" : "&"));
|
||||
pubKeys.push(tagSelector.namesModel.get(i).pubKey);
|
||||
}
|
||||
root.rootStore.chatCommunitySectionModule.createGroupChat("", groupName, JSON.stringify(pubKeys));
|
||||
}
|
||||
|
||||
chatInput.textInput.clear();
|
||||
chatInput.textInput.textFormat = TextEdit.PlainText;
|
||||
chatInput.textInput.textFormat = TextEdit.RichText;
|
||||
Global.changeAppSectionBySectionType(Constants.appSection.chat)
|
||||
}
|
||||
padding: 0
|
||||
|
||||
Behavior on opacity { NumberAnimation {}}
|
||||
Behavior on anchors.bottomMargin { NumberAnimation { duration: 30 }}
|
||||
|
||||
background: Rectangle {
|
||||
anchors.fill: parent
|
||||
color: Theme.palette.statusAppLayout.rightPanelBackgroundColor
|
||||
}
|
||||
|
||||
// TODO: Could it be replaced to `GroupChatPanel`?
|
||||
header: RowLayout {
|
||||
id: headerRow
|
||||
width: parent.width
|
||||
height: tagSelector.height
|
||||
anchors.top: parent.top
|
||||
anchors.topMargin: Style.current.halfPadding
|
||||
clip: true
|
||||
spacing: Style.current.padding
|
||||
StatusTagSelector {
|
||||
id: tagSelector
|
||||
Layout.fillWidth: true
|
||||
Layout.alignment: Qt.AlignVCenter | Qt.AlignLeft
|
||||
Layout.leftMargin: 17
|
||||
maxHeight: root.height
|
||||
nameCountLimit: 20
|
||||
listLabel: contactsModel.count ? qsTr("Contacts") : ""
|
||||
textEdit.enabled: contactsModel.count
|
||||
toLabelText: qsTr("To: ")
|
||||
warningText: qsTr("USER LIMIT REACHED")
|
||||
ringSpecModelGetter: function(pubKey) {
|
||||
return Utils.getColorHashAsJson(pubKey);
|
||||
}
|
||||
compressedKeyGetter: function(pubKey) {
|
||||
return Utils.getCompressedPk(pubKey);
|
||||
}
|
||||
colorIdForPubkeyGetter: function (pubKey) {
|
||||
return Utils.colorIdForPubkey(pubKey);
|
||||
}
|
||||
onTextChanged: {
|
||||
sortModel(root.contactsModel);
|
||||
}
|
||||
}
|
||||
header: Item {
|
||||
implicitHeight: headerLayout.implicitHeight + headerLayout.anchors.topMargin + headerLayout.anchors.bottomMargin
|
||||
|
||||
StatusButton {
|
||||
id: confirmButton
|
||||
objectName: "createChatConfirmButton"
|
||||
implicitWidth: 106
|
||||
implicitHeight: 44
|
||||
Layout.alignment: Qt.AlignTop
|
||||
enabled: tagSelector.namesModel.count > 0
|
||||
text: qsTr("Confirm")
|
||||
onClicked: {
|
||||
root.rootStore.createChatInitMessage = chatInput.textInput.text;
|
||||
root.rootStore.createChatFileUrls = chatInput.fileUrls;
|
||||
root.createChat();
|
||||
RowLayout {
|
||||
id: headerLayout
|
||||
anchors {
|
||||
fill: parent
|
||||
topMargin: 8
|
||||
}
|
||||
|
||||
MembersSelectorView {
|
||||
id: membersSelector
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
|
||||
rootStore: root.rootStore
|
||||
enabled: root.rootStore.contactsModel.count > 0
|
||||
|
||||
function createChat() {
|
||||
if (model.count === 0) {
|
||||
console.warn("Can't create chat with no members")
|
||||
return
|
||||
}
|
||||
|
||||
if (model.count === 1) {
|
||||
const member = model.get(0)
|
||||
const ensName = member.displayName.includes(".eth") ? member.displayName : ""
|
||||
root.rootStore.chatCommunitySectionModule.createOneToOneChat("", member.pubKey, ensName)
|
||||
} else {
|
||||
var groupName = "";
|
||||
var pubKeys = [];
|
||||
for (var i = 0; i < model.count; i++) {
|
||||
const member = model.get(i)
|
||||
groupName += (member.displayName + (i === model.count - 1 ? "" : "&"))
|
||||
pubKeys.push(member.pubKey)
|
||||
}
|
||||
root.rootStore.chatCommunitySectionModule.createGroupChat("", groupName, JSON.stringify(pubKeys))
|
||||
}
|
||||
}
|
||||
|
||||
onConfirmed: {
|
||||
root.rootStore.createChatInitMessage = chatInput.textInput.text
|
||||
root.rootStore.createChatFileUrls = chatInput.fileUrls
|
||||
createChat()
|
||||
|
||||
cleanup()
|
||||
chatInput.textInput.clear()
|
||||
}
|
||||
|
||||
onRejected: {
|
||||
cleanup()
|
||||
Global.closeCreateChatView()
|
||||
}
|
||||
|
||||
onVisibleChanged: if (visible) edit.forceActiveFocus()
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.preferredWidth: 32
|
||||
Layout.preferredHeight: 32
|
||||
Layout.alignment: Qt.AlignTop
|
||||
Layout.topMargin: 5
|
||||
StatusActivityCenterButton {
|
||||
id: notificationButton
|
||||
width: parent.width
|
||||
height: parent.height
|
||||
tooltip.offset: width/2
|
||||
Layout.preferredWidth: 32
|
||||
Layout.preferredHeight: 32
|
||||
|
||||
unreadNotificationsCount: root.rootStore.unreadNotificationsCount
|
||||
onClicked: Global.openActivityCenterPopup()
|
||||
}
|
||||
|
@ -141,61 +100,91 @@ Page {
|
|||
}
|
||||
|
||||
contentItem: Item {
|
||||
StatusChatInput {
|
||||
id: chatInput
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
visible: tagSelector.namesModel.count > 0
|
||||
chatType: tagSelector.namesModel.count == 1? Constants.chatType.oneToOne : Constants.chatType.privateGroupChat
|
||||
|
||||
emojiPopup: root.emojiPopup
|
||||
recentStickers: root.rootStore.stickersModuleInst.recent
|
||||
stickerPackList: root.rootStore.stickersModuleInst.stickerPacks
|
||||
closeGifPopupAfterSelection: true
|
||||
|
||||
onSendTransactionCommandButtonClicked: {
|
||||
root.rootStore.createChatStartSendTransactionProcess = true;
|
||||
root.createChat();
|
||||
ColumnLayout {
|
||||
anchors {
|
||||
fill: parent
|
||||
topMargin: 32
|
||||
bottomMargin: 12
|
||||
leftMargin: 8
|
||||
}
|
||||
|
||||
onReceiveTransactionCommandButtonClicked: {
|
||||
root.rootStore.createChatStartReceiveTransactionProcess = true;
|
||||
root.createChat();
|
||||
StatusBaseText {
|
||||
Layout.alignment: Qt.AlignTop
|
||||
Layout.leftMargin: 8
|
||||
|
||||
visible: contactsList.visible
|
||||
|
||||
font.pixelSize: 15
|
||||
text: qsTr("Contacts")
|
||||
color: Theme.palette.baseColor1
|
||||
}
|
||||
|
||||
onStickerSelected: {
|
||||
root.rootStore.createChatStickerHashId = hashId;
|
||||
root.rootStore.createChatStickerPackId = packId;
|
||||
root.rootStore.createChatStickerUrl = url;
|
||||
root.createChat();
|
||||
StatusListView {
|
||||
id: contactsList
|
||||
|
||||
Layout.fillHeight: true
|
||||
|
||||
visible: membersSelector.suggestionsModel.count &&
|
||||
!(membersSelector.edit.text !== "")
|
||||
|
||||
implicitWidth: contentItem.childrenRect.width
|
||||
|
||||
model: membersSelector.suggestionsModel
|
||||
delegate: ContactListItemDelegate {
|
||||
onClicked: membersSelector.entryAccepted(this)
|
||||
}
|
||||
}
|
||||
|
||||
onSendMessage: {
|
||||
root.rootStore.createChatFileUrls = chatInput.fileUrls;
|
||||
root.rootStore.createChatInitMessage = chatInput.textInput.text;
|
||||
root.createChat();
|
||||
StatusChatInput {
|
||||
id: chatInput
|
||||
|
||||
Layout.alignment: Qt.AlignBottom
|
||||
Layout.fillWidth: true
|
||||
|
||||
visible: membersSelector.model.count > 0
|
||||
chatType: membersSelector.model.count === 1? Constants.chatType.oneToOne : Constants.chatType.privateGroupChat
|
||||
|
||||
emojiPopup: root.emojiPopup
|
||||
recentStickers: root.rootStore.stickersModuleInst.recent
|
||||
stickerPackList: root.rootStore.stickersModuleInst.stickerPacks
|
||||
closeGifPopupAfterSelection: true
|
||||
|
||||
onSendTransactionCommandButtonClicked: {
|
||||
root.rootStore.createChatStartSendTransactionProcess = true;
|
||||
root.createChat();
|
||||
}
|
||||
|
||||
onReceiveTransactionCommandButtonClicked: {
|
||||
root.rootStore.createChatStartReceiveTransactionProcess = true;
|
||||
root.createChat();
|
||||
}
|
||||
|
||||
onStickerSelected: {
|
||||
root.rootStore.createChatStickerHashId = hashId;
|
||||
root.rootStore.createChatStickerPackId = packId;
|
||||
root.rootStore.createChatStickerUrl = url;
|
||||
root.createChat();
|
||||
}
|
||||
|
||||
onSendMessage: {
|
||||
root.rootStore.createChatFileUrls = chatInput.fileUrls;
|
||||
root.rootStore.createChatInitMessage = chatInput.textInput.text;
|
||||
root.createChat();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StatusBaseText {
|
||||
anchors.centerIn: parent
|
||||
width: Math.min(553, parent.width - 2 * Style.current.padding)
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.verticalCenterOffset: -(headerRow.height/2)
|
||||
visible: root.rootStore.contactsModel.count === 0
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
visible: contactsModel.count === 0
|
||||
wrapMode: Text.WordWrap
|
||||
font.pixelSize: 15
|
||||
color: Theme.palette.baseColor1
|
||||
text: qsTr("You can only send direct messages to your Contacts.\n
|
||||
Send a contact request to the person you would like to chat with, you will be able to chat with them once they have accepted your contact request.")
|
||||
Component.onCompleted: {
|
||||
if (visible) {
|
||||
tagSelector.enabled = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,91 @@
|
|||
import QtQuick 2.14
|
||||
import QtQuick.Controls 2.14
|
||||
import QtQuick.Layouts 1.14
|
||||
|
||||
import StatusQ.Core 0.1
|
||||
import StatusQ.Core.Theme 0.1
|
||||
import StatusQ.Core.Utils 0.1
|
||||
import StatusQ.Controls 0.1
|
||||
import StatusQ.Components 0.1
|
||||
|
||||
import "../panels"
|
||||
import "../stores"
|
||||
import "private"
|
||||
|
||||
import utils 1.0
|
||||
|
||||
import SortFilterProxyModel 0.2
|
||||
|
||||
MembersSelectorBase {
|
||||
id: root
|
||||
|
||||
// TODO: use stores instead of modules
|
||||
property var sectionModule
|
||||
property var chatContentModule
|
||||
|
||||
onConfirmed: {
|
||||
d.updateGroupMembers()
|
||||
d.resetTemporaryModel()
|
||||
}
|
||||
|
||||
onRejected: {
|
||||
d.resetTemporaryModel()
|
||||
}
|
||||
|
||||
onEntryAccepted: {
|
||||
if (!root.limitReached) {
|
||||
d.appendTemporaryModel(suggestionsDelegate._pubKey, suggestionsDelegate.userName)
|
||||
root.edit.clear()
|
||||
}
|
||||
}
|
||||
|
||||
onEntryRemoved: {
|
||||
if (!delegate.isReadonly) {
|
||||
d.removeFromTemporaryModel(delegate._pubKey)
|
||||
}
|
||||
}
|
||||
|
||||
model: SortFilterProxyModel {
|
||||
sourceModel: root.chatContentModule.usersModule.temporaryModel
|
||||
sorters: RoleSorter {
|
||||
roleName: "isAdmin"
|
||||
sortOrder: Qt.DescendingOrder
|
||||
}
|
||||
}
|
||||
|
||||
delegate: StatusTagItem {
|
||||
readonly property string _pubKey: model.pubKey
|
||||
|
||||
height: ListView.view.height
|
||||
text: model.displayName
|
||||
isReadonly: {
|
||||
if (model.isAdmin) return true
|
||||
if (root.chatContentModule.amIChatAdmin()) return false
|
||||
return index < root.chatContentModule.usersModule.model.count
|
||||
}
|
||||
icon: model.isAdmin ? "crown" : ""
|
||||
|
||||
onClicked: root.entryRemoved(this)
|
||||
}
|
||||
|
||||
QtObject {
|
||||
id: d
|
||||
|
||||
function appendTemporaryModel(pubKey, displayName) {
|
||||
root.chatContentModule.usersModule.appendTemporaryModel(pubKey, displayName)
|
||||
}
|
||||
function removeFromTemporaryModel(pubKey) {
|
||||
root.chatContentModule.usersModule.removeFromTemporaryModel(pubKey)
|
||||
}
|
||||
function resetTemporaryModel() {
|
||||
root.chatContentModule.usersModule.resetTemporaryModel()
|
||||
}
|
||||
function updateGroupMembers() {
|
||||
root.chatContentModule.usersModule.updateGroupMembers()
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
d.resetTemporaryModel()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,65 @@
|
|||
import QtQuick 2.14
|
||||
import QtQuick.Controls 2.14
|
||||
import QtQml.Models 2.2
|
||||
|
||||
import StatusQ.Controls 0.1
|
||||
import StatusQ.Components 0.1
|
||||
|
||||
import "private"
|
||||
|
||||
import SortFilterProxyModel 0.2
|
||||
|
||||
MembersSelectorBase {
|
||||
id: root
|
||||
|
||||
function cleanup() {
|
||||
root.edit.clear()
|
||||
d.selectedMembers.clear()
|
||||
}
|
||||
|
||||
onEntryAccepted: {
|
||||
if (!root.limitReached) {
|
||||
d.addMember(suggestionsDelegate._pubKey, suggestionsDelegate.userName)
|
||||
root.edit.clear()
|
||||
}
|
||||
}
|
||||
|
||||
onEntryRemoved: {
|
||||
d.removeMember(delegate._pubKey)
|
||||
}
|
||||
|
||||
model: SortFilterProxyModel {
|
||||
sourceModel: d.selectedMembers
|
||||
}
|
||||
|
||||
delegate: StatusTagItem {
|
||||
readonly property string _pubKey: model.pubKey
|
||||
|
||||
height: ListView.view.height
|
||||
text: model.displayName
|
||||
|
||||
onClicked: root.entryRemoved(this)
|
||||
}
|
||||
|
||||
QtObject {
|
||||
id: d
|
||||
|
||||
property ListModel selectedMembers: ListModel {}
|
||||
|
||||
function addMember(pubKey, displayName) {
|
||||
d.selectedMembers.append({
|
||||
"pubKey": pubKey,
|
||||
"displayName": displayName
|
||||
})
|
||||
}
|
||||
function removeMember(pubKey) {
|
||||
for(var i = 0; i < d.selectedMembers.count; i++) {
|
||||
const obj = d.selectedMembers.get(i)
|
||||
if(obj.pubKey === pubKey) {
|
||||
d.selectedMembers.remove(i)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,79 @@
|
|||
import QtQuick 2.14
|
||||
import QtQuick.Controls 2.14
|
||||
import QtQuick.Layouts 1.14
|
||||
|
||||
import StatusQ.Core 0.1
|
||||
import StatusQ.Core.Theme 0.1
|
||||
import StatusQ.Core.Utils 0.1
|
||||
import StatusQ.Controls 0.1
|
||||
import StatusQ.Components 0.1
|
||||
|
||||
import "../../panels"
|
||||
|
||||
import utils 1.0
|
||||
import shared.controls.delegates 1.0
|
||||
|
||||
import SortFilterProxyModel 0.2
|
||||
|
||||
InlineSelectorPanel {
|
||||
id: root
|
||||
|
||||
property var rootStore
|
||||
readonly property bool limitReached: model.count >= d.membersLimit
|
||||
|
||||
label.text: qsTr("To:")
|
||||
warningLabel.text: qsTr("%1 USER LIMIT REACHED").arg(d.membersLimit)
|
||||
warningLabel.visible: limitReached
|
||||
|
||||
suggestionsModel: SortFilterProxyModel {
|
||||
id: _suggestionsModel
|
||||
|
||||
sourceModel: root.rootStore.contactsModel
|
||||
|
||||
function searchPredicate(displayName) {
|
||||
return displayName.toLowerCase().includes(root.edit.text.toLowerCase())
|
||||
}
|
||||
|
||||
function notAMemberPredicate(pubKey) {
|
||||
for(var i = 0; i < model.count; i++) {
|
||||
var item = model.get(i)
|
||||
if(item.pubKey === pubKey) return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
filters: [
|
||||
ExpressionFilter {
|
||||
enabled: root.edit.text !== ""
|
||||
expression: {
|
||||
root.edit.text // ensure expression is reevaluated when edit.text changes
|
||||
return _suggestionsModel.searchPredicate(model.displayName)
|
||||
}
|
||||
},
|
||||
ExpressionFilter {
|
||||
expression: {
|
||||
root.model.count // ensure expression is reevaluated when members model changes
|
||||
return _suggestionsModel.notAMemberPredicate(model.pubKey)
|
||||
}
|
||||
}
|
||||
]
|
||||
sorters: StringSorter {
|
||||
roleName: "displayName"
|
||||
}
|
||||
}
|
||||
|
||||
suggestionsDelegate: ContactListItemDelegate {
|
||||
highlighted: ListView.isCurrentItem
|
||||
onClicked: root.entryAccepted(this)
|
||||
}
|
||||
|
||||
QtObject {
|
||||
id: d
|
||||
|
||||
readonly property int membersLimit: 20 // see: https://github.com/status-im/status-mobile/issues/13066
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
root.edit.forceActiveFocus()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
import QtQuick 2.14
|
||||
import QtQuick.Controls 2.14
|
||||
|
||||
import StatusQ.Components 0.1
|
||||
import StatusQ.Core.Theme 0.1
|
||||
|
||||
import utils 1.0
|
||||
|
||||
/**
|
||||
attached model property should be compatible with nim's contact model
|
||||
*/
|
||||
|
||||
StatusMemberListItem {
|
||||
id: root
|
||||
|
||||
readonly property string _pubKey: model.pubKey // expose uncompressed pubkey
|
||||
|
||||
pubKey: Utils.getCompressedPk(model.pubKey)
|
||||
nickName: model.localNickname
|
||||
userName: model.displayName
|
||||
isVerified: model.isVerified
|
||||
isUntrustworthy: model.isUntrustworthy
|
||||
isContact: model.isContact
|
||||
asset.name: model.icon
|
||||
asset.color: Utils.colorForPubkey(model.pubKey)
|
||||
asset.isImage: (asset.name !== "")
|
||||
asset.isLetterIdenticon: (asset.name === "")
|
||||
status: model.onlineStatus
|
||||
statusListItemIcon.badge.border.color: sensor.containsMouse ? Theme.palette.baseColor2 : Theme.palette.baseColor4
|
||||
ringSettings.ringSpecModel: Utils.getColorHashAsJson(model.pubKey)
|
||||
color: (sensor.containsMouse || highlighted) ? Theme.palette.baseColor2 : "transparent"
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
ContactListItemDelegate 1.0 ContactListItemDelegate.qml
|
Loading…
Reference in New Issue