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:
Patryk Osmaczko 2022-09-06 15:19:50 +02:00 committed by osmaczko
parent 2d5c69bc6c
commit 6760870dc9
13 changed files with 731 additions and 181 deletions

View File

@ -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)

View File

@ -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")

View File

@ -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)

View File

@ -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)

View File

@ -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

View File

@ -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
}
}
}

View File

@ -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
}
}
}

View File

@ -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;
}
}
}
}
}

View File

@ -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()
}
}

View File

@ -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
}
}
}
}
}

View File

@ -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()
}
}

View File

@ -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"
}

View File

@ -0,0 +1 @@
ContactListItemDelegate 1.0 ContactListItemDelegate.qml