feat(GroupChat/Add-Remove-Contacts): Implement "Add / remove from group" button for group chats
Added new option add / remove contacts in dropdown and created navigation to modify the loaded component in the toolbar. Enabled addition of new members into a group chat by the admin. Enabled removal of members of a group chat by the admin. Added into `ChatContentView` components related to chat toolbar: - `StatusTagSelector` and its corresponding logic - Moved `StatusChatInfoButton` from toolbar to content view. Added `esc` key event to leave the group chat add / remove panel. Updated `StatusQ` link. Closes #5522
This commit is contained in:
parent
78f985e484
commit
7c92d39359
|
@ -293,6 +293,9 @@ proc addGroupMembers*(self: Controller, chatId: string, pubKeys: seq[string]) =
|
|||
proc removeMemberFromGroupChat*(self: Controller, communityID: string, chatId: string, pubKey: string) =
|
||||
self.chatService.removeMemberFromGroupChat(communityID, chatId, pubKey)
|
||||
|
||||
proc removeMembersFromGroupChat*(self: Controller, communityID: string, chatId: string, pubKeys: seq[string]) =
|
||||
self.chatService.removeMembersFromGroupChat(communityID, chatId, pubKeys)
|
||||
|
||||
proc renameGroupChat*(self: Controller, chatId: string, newName: string) =
|
||||
let communityId = if self.isCommunitySection: self.sectionId else: ""
|
||||
self.chatService.renameGroupChat(communityId, chatId, newName)
|
||||
|
|
|
@ -205,6 +205,9 @@ method addGroupMembers*(self: AccessInterface, chatId: string, pubKeys: string)
|
|||
method removeMemberFromGroupChat*(self: AccessInterface, communityID: string, chatId: string, pubKey: string) {.base.} =
|
||||
raise newException(ValueError, "No implementation available")
|
||||
|
||||
method removeMembersFromGroupChat*(self: AccessInterface, communityID: string, chatId: string, pubKeys: string) {.base.} =
|
||||
raise newException(ValueError, "No implementation available")
|
||||
|
||||
method renameGroupChat*(self: AccessInterface, chatId: string, newName: string) {.base.} =
|
||||
raise newException(ValueError, "No implementation available")
|
||||
|
||||
|
|
|
@ -686,6 +686,9 @@ method addGroupMembers*(self: Module, chatId: string, pubKeys: string) =
|
|||
method removeMemberFromGroupChat*(self: Module, communityID: string, chatId: string, pubKey: string) =
|
||||
self.controller.removeMemberFromGroupChat(communityID, chatId, pubKey)
|
||||
|
||||
method removeMembersFromGroupChat*(self: Module, communityID: string, chatId: string, pubKeys: string) =
|
||||
self.controller.removeMembersFromGroupChat(communityID, chatId, self.convertPubKeysToJson(pubKeys))
|
||||
|
||||
method renameGroupChat*(self: Module, chatId: string, newName: string) =
|
||||
self.controller.renameGroupChat(chatId, newName)
|
||||
|
||||
|
|
|
@ -186,6 +186,9 @@ QtObject:
|
|||
|
||||
proc removeMemberFromGroupChat*(self: View, communityID: string, chatId: string, pubKey: string) {.slot.} =
|
||||
self.delegate.removeMemberFromGroupChat(communityID, chatId, pubKey)
|
||||
|
||||
proc removeMembersFromGroupChat*(self: View, communityID: string, chatId: string, pubKeys: string) {.slot.} =
|
||||
self.delegate.removeMembersFromGroupChat(communityID, chatId, pubKeys)
|
||||
|
||||
proc renameGroupChat*(self: View, chatId: string, newName: string) {.slot.} =
|
||||
self.delegate.renameGroupChat(chatId, newName)
|
||||
|
|
|
@ -464,6 +464,12 @@ QtObject:
|
|||
except Exception as e:
|
||||
error "error while removing member from group: ", msg = e.msg
|
||||
|
||||
proc removeMembersFromGroupChat*(self: Service, communityID: string, chatID: string, members: seq[string]) =
|
||||
try:
|
||||
for member in members:
|
||||
self.removeMemberFromGroupChat(communityID, chatID, member)
|
||||
except Exception as e:
|
||||
error "error while removing members from group: ", msg = e.msg
|
||||
|
||||
proc renameGroupChat*(self: Service, communityID: string, chatID: string, name: string) =
|
||||
try:
|
||||
|
|
|
@ -1 +1 @@
|
|||
Subproject commit be0f49a8d80a8489e9f26f765167c3239c0c92c1
|
||||
Subproject commit a4d0c1366256d4e552990bde63734b7ceea0ad80
|
|
@ -0,0 +1,180 @@
|
|||
import QtQuick 2.13
|
||||
import QtQuick.Layouts 1.13
|
||||
|
||||
import StatusQ.Components 0.1
|
||||
import StatusQ.Controls 0.1
|
||||
|
||||
import utils 1.0
|
||||
|
||||
|
||||
RowLayout {
|
||||
id: root
|
||||
|
||||
property var sectionModule
|
||||
property var chatContentModule
|
||||
property var rootStore
|
||||
property int maxHeight
|
||||
|
||||
signal panelClosed()
|
||||
|
||||
QtObject {
|
||||
id: d
|
||||
|
||||
property ListModel groupUsersModel: ListModel { }
|
||||
property ListModel contactsModel: ListModel { }
|
||||
property var addedMembersIds: []
|
||||
property var removedMembersIds: []
|
||||
|
||||
function initialize () {
|
||||
groupUsersModel.clear()
|
||||
contactsModel.clear()
|
||||
addedMembersIds = []
|
||||
removedMembersIds = []
|
||||
tagSelector.namesModel.clear()
|
||||
}
|
||||
}
|
||||
|
||||
ListView {
|
||||
id: groupUsersModelListView
|
||||
visible: false
|
||||
model: root.chatContentModule.usersModule.model
|
||||
delegate: Item {
|
||||
property string publicId: model.id
|
||||
property string name: model.name
|
||||
property string icon: model.icon
|
||||
property bool isAdmin: model.isAdmin
|
||||
}
|
||||
}
|
||||
|
||||
ListView {
|
||||
id: contactsModelListView
|
||||
visible: false
|
||||
model: root.rootStore.contactsModel
|
||||
delegate: Item {
|
||||
property string publicId: model.pubKey
|
||||
property string name: model.name
|
||||
property string icon: model.icon
|
||||
}
|
||||
}
|
||||
|
||||
clip: true
|
||||
|
||||
Component.onCompleted: {
|
||||
d.initialize()
|
||||
|
||||
// Build groupUsersModel type from model type (to fit with expected StatusTagSelector format
|
||||
for (var i = 0; i < groupUsersModelListView.count; i ++) {
|
||||
var entry = groupUsersModelListView.itemAtIndex(i)
|
||||
|
||||
// Add all group users different than me
|
||||
if(!entry.isAdmin) {
|
||||
d.groupUsersModel.insert(d.groupUsersModel.count,
|
||||
{"publicId": entry.publicId,
|
||||
"name": entry.name,
|
||||
"icon": entry.icon})
|
||||
}
|
||||
}
|
||||
|
||||
// Build contactsModel type from model type (to fit with expected StatusTagSelector format
|
||||
for (var j = 0; j < contactsModelListView.count; j ++) {
|
||||
var entry2 = contactsModelListView.itemAtIndex(j)
|
||||
d.contactsModel.insert(d.contactsModel.count,
|
||||
{"publicId": entry2.publicId,
|
||||
"name": entry2.name,
|
||||
"icon": entry2.icon,
|
||||
"isIdenticon": false,
|
||||
"onlineStatus": false})
|
||||
}
|
||||
|
||||
// Update contacts list used by StatusTagSelector
|
||||
tagSelector.sortModel(d.contactsModel)
|
||||
}
|
||||
|
||||
StatusTagSelector {
|
||||
id: tagSelector
|
||||
|
||||
function memberExists(memberId) {
|
||||
var exists = false
|
||||
for (var i = 0; i < groupUsersModelListView.count; i ++) {
|
||||
var entry = groupUsersModelListView.itemAtIndex(i)
|
||||
if(entry.publicId === memberId) {
|
||||
exists = true
|
||||
break
|
||||
}
|
||||
}
|
||||
return exists
|
||||
}
|
||||
|
||||
function find(val, array) {
|
||||
for(var i = 0; i < array.length; i++) {
|
||||
if(array[i] === val) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
function addNewMember(memberId) {
|
||||
if(find(memberId, d.addedMembersIds)) {
|
||||
return
|
||||
}
|
||||
|
||||
if(!memberExists(memberId)) {
|
||||
d.addedMembersIds.push(memberId)
|
||||
}
|
||||
|
||||
if(memberExists(memberId) && find(memberId, d.removedMembersIds)) {
|
||||
d.removedMembersIds.pop(memberId)
|
||||
}
|
||||
}
|
||||
|
||||
function removeExistingMember(memberId) {
|
||||
if(find(memberId, d.removedMembersIds)) {
|
||||
return
|
||||
}
|
||||
|
||||
if(memberExists(memberId)) {
|
||||
d.removedMembersIds.push(memberId)
|
||||
}
|
||||
|
||||
if(!memberExists(memberId) && find(memberId, d.addedMembersIds)) {
|
||||
d.addedMembersIds.pop(memberId)
|
||||
}
|
||||
}
|
||||
|
||||
namesModel: d.groupUsersModel
|
||||
Layout.fillWidth: true
|
||||
Layout.alignment: Qt.AlignTop | Qt.AlignLeft
|
||||
maxHeight: root.maxHeight
|
||||
nameCountLimit: 20
|
||||
showSortedListOnlyWhenText: true
|
||||
toLabelText: qsTr("To: ")
|
||||
warningText: qsTr("USER LIMIT REACHED")
|
||||
onTextChanged: sortModel(d.contactsModel)
|
||||
onAddMember: addNewMember(memberId)
|
||||
onRemoveMember: removeExistingMember(memberId)
|
||||
ringSpecModelGetter: function(pubKey) {
|
||||
return Utils.getColorHashAsJson(pubKey);
|
||||
}
|
||||
compressedKeyGetter: function(pubKey) {
|
||||
return Utils.getCompressedPk(pubKey);
|
||||
}
|
||||
}
|
||||
|
||||
StatusButton {
|
||||
id: confirmButton
|
||||
implicitHeight: 44
|
||||
Layout.alignment: Qt.AlignTop
|
||||
text: "Confirm"
|
||||
onClicked: {
|
||||
if(root.chatContentModule.chatDetails.id &&((d.addedMembersIds.length > 0) || (d.removedMembersIds.length > 0))) {
|
||||
// Add request:
|
||||
root.sectionModule.addGroupMembers(root.chatContentModule.chatDetails.id, JSON.stringify(d.addedMembersIds))
|
||||
|
||||
// Remove request:
|
||||
root.sectionModule.removeMembersFromGroupChat("", root.chatContentModule.chatDetails.id, JSON.stringify(d.removedMembersIds))
|
||||
}
|
||||
root.panelClosed()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -57,95 +57,120 @@ ColumnLayout {
|
|||
if(chatContentRoot.height > 0) {
|
||||
chatInput.forceInputActiveFocus()
|
||||
}
|
||||
}
|
||||
|
||||
Keys.onEscapePressed: { topBar.toolbarComponent = statusChatInfoButton }
|
||||
|
||||
// Chat toolbar content option 1:
|
||||
Component {
|
||||
id: statusChatInfoButton
|
||||
|
||||
StatusChatInfoButton {
|
||||
width: Math.min(implicitWidth, parent.width)
|
||||
title: chatContentModule? chatContentModule.chatDetails.name : ""
|
||||
subTitle: {
|
||||
if(!chatContentModule)
|
||||
return ""
|
||||
|
||||
// In some moment in future this should be part of the backend logic.
|
||||
// (once we add transaltion on the backend side)
|
||||
switch (chatContentModule.chatDetails.type) {
|
||||
case Constants.chatType.oneToOne:
|
||||
return (chatContentModule.isMyContact(chatContentModule.chatDetails.id) ?
|
||||
//% "Contact"
|
||||
qsTrId("chat-is-a-contact") :
|
||||
//% "Not a contact"
|
||||
qsTrId("chat-is-not-a-contact"))
|
||||
case Constants.chatType.publicChat:
|
||||
//% "Public chat"
|
||||
return qsTrId("public-chat")
|
||||
case Constants.chatType.privateGroupChat:
|
||||
let cnt = chatContentRoot.usersStore.usersModule.model.count
|
||||
//% "%1 members"
|
||||
if(cnt > 1) return qsTrId("-1-members").arg(cnt);
|
||||
//% "1 member"
|
||||
return qsTrId("1-member");
|
||||
case Constants.chatType.communityChat:
|
||||
return Utils.linkifyAndXSS(chatContentModule.chatDetails.description).trim()
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
image.source: chatContentModule? chatContentModule.chatDetails.icon : ""
|
||||
ringSettings.ringSpecModel: chatContentModule && chatContentModule.chatDetails.type === Constants.chatType.oneToOne ?
|
||||
Utils.getColorHashAsJson(chatContentModule.chatDetails.id) : ""
|
||||
icon.color: chatContentModule?
|
||||
chatContentModule.chatDetails.type === Constants.chatType.oneToOne ?
|
||||
Utils.colorForPubkey(chatContentModule.chatDetails.id)
|
||||
: chatContentModule.chatDetails.color
|
||||
: ""
|
||||
icon.emoji: chatContentModule? chatContentModule.chatDetails.emoji : ""
|
||||
icon.emojiSize: "24x24"
|
||||
type: chatContentModule? chatContentModule.chatDetails.type : Constants.chatType.unknown
|
||||
pinnedMessagesCount: chatContentModule? chatContentModule.pinnedMessagesModel.count : 0
|
||||
muted: chatContentModule? chatContentModule.chatDetails.muted : false
|
||||
|
||||
onPinnedMessagesCountClicked: {
|
||||
if(!chatContentModule) {
|
||||
console.debug("error on open pinned messages - chat content module is not set")
|
||||
return
|
||||
}
|
||||
Global.openPopup(pinnedMessagesPopupComponent, {
|
||||
store: rootStore,
|
||||
messageStore: messageStore,
|
||||
pinnedMessagesModel: chatContentModule.pinnedMessagesModel,
|
||||
messageToPin: ""
|
||||
})
|
||||
}
|
||||
onUnmute: {
|
||||
if(!chatContentModule) {
|
||||
console.debug("error on unmute chat - chat content module is not set")
|
||||
return
|
||||
}
|
||||
chatContentModule.unmuteChat()
|
||||
}
|
||||
|
||||
sensor.enabled: {
|
||||
if(!chatContentModule)
|
||||
return false
|
||||
|
||||
return chatContentModule.chatDetails.type !== Constants.chatType.publicChat &&
|
||||
chatContentModule.chatDetails.type !== Constants.chatType.communityChat
|
||||
}
|
||||
onClicked: {
|
||||
switch (chatContentModule.chatDetails.type) {
|
||||
case Constants.chatType.privateGroupChat:
|
||||
Global.openPopup(groupInfoPopupComponent, {
|
||||
chatContentModule: chatContentModule,
|
||||
chatDetails: chatContentModule.chatDetails
|
||||
})
|
||||
break;
|
||||
case Constants.chatType.oneToOne:
|
||||
Global.openProfilePopup(chatContentModule.chatDetails.id)
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Chat toolbar content option 2:
|
||||
Component {
|
||||
id: contactsSelector
|
||||
GroupChatPanel {
|
||||
sectionModule: chatSectionModule
|
||||
chatContentModule: chatContentRoot.chatContentModule
|
||||
rootStore: chatContentRoot.rootStore
|
||||
maxHeight: chatContentRoot.height
|
||||
|
||||
onPanelClosed: topBar.toolbarComponent = statusChatInfoButton
|
||||
}
|
||||
}
|
||||
|
||||
StatusChatToolBar {
|
||||
id: topBar
|
||||
z: parent.z + 1
|
||||
Layout.fillWidth: true
|
||||
|
||||
chatInfoButton.title: chatContentModule? chatContentModule.chatDetails.name : ""
|
||||
chatInfoButton.subTitle: {
|
||||
if(!chatContentModule)
|
||||
return ""
|
||||
|
||||
// In some moment in future this should be part of the backend logic.
|
||||
// (once we add transaltion on the backend side)
|
||||
switch (chatContentModule.chatDetails.type) {
|
||||
case Constants.chatType.oneToOne:
|
||||
return (chatContentModule.isMyContact(chatContentModule.chatDetails.id) ?
|
||||
//% "Contact"
|
||||
qsTrId("chat-is-a-contact") :
|
||||
//% "Not a contact"
|
||||
qsTrId("chat-is-not-a-contact"))
|
||||
case Constants.chatType.publicChat:
|
||||
//% "Public chat"
|
||||
return qsTrId("public-chat")
|
||||
case Constants.chatType.privateGroupChat:
|
||||
let cnt = chatContentRoot.usersStore.usersModule.model.count
|
||||
//% "%1 members"
|
||||
if(cnt > 1) return qsTrId("-1-members").arg(cnt);
|
||||
//% "1 member"
|
||||
return qsTrId("1-member");
|
||||
case Constants.chatType.communityChat:
|
||||
return Utils.linkifyAndXSS(chatContentModule.chatDetails.description).trim()
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
chatInfoButton.image.source: chatContentModule? chatContentModule.chatDetails.icon : ""
|
||||
chatInfoButton.ringSettings.ringSpecModel: chatContentModule && chatContentModule.chatDetails.type === Constants.chatType.oneToOne ?
|
||||
Utils.getColorHashAsJson(chatContentModule.chatDetails.id) : ""
|
||||
chatInfoButton.icon.color: chatContentModule?
|
||||
chatContentModule.chatDetails.type === Constants.chatType.oneToOne ?
|
||||
Utils.colorForPubkey(chatContentModule.chatDetails.id)
|
||||
: chatContentModule.chatDetails.color
|
||||
: ""
|
||||
chatInfoButton.icon.emoji: chatContentModule? chatContentModule.chatDetails.emoji : ""
|
||||
chatInfoButton.icon.emojiSize: "24x24"
|
||||
chatInfoButton.type: chatContentModule? chatContentModule.chatDetails.type : Constants.chatType.unknown
|
||||
chatInfoButton.pinnedMessagesCount: chatContentModule? chatContentModule.pinnedMessagesModel.count : 0
|
||||
chatInfoButton.muted: chatContentModule? chatContentModule.chatDetails.muted : false
|
||||
|
||||
chatInfoButton.onPinnedMessagesCountClicked: {
|
||||
if(!chatContentModule) {
|
||||
console.debug("error on open pinned messages - chat content module is not set")
|
||||
return
|
||||
}
|
||||
Global.openPopup(pinnedMessagesPopupComponent, {
|
||||
store: rootStore,
|
||||
messageStore: messageStore,
|
||||
pinnedMessagesModel: chatContentModule.pinnedMessagesModel,
|
||||
messageToPin: ""
|
||||
})
|
||||
}
|
||||
chatInfoButton.onUnmute: {
|
||||
if(!chatContentModule) {
|
||||
console.debug("error on unmute chat - chat content module is not set")
|
||||
return
|
||||
}
|
||||
chatContentModule.unmuteChat()
|
||||
}
|
||||
|
||||
chatInfoButton.sensor.enabled: {
|
||||
if(!chatContentModule)
|
||||
return false
|
||||
|
||||
return chatContentModule.chatDetails.type !== Constants.chatType.publicChat &&
|
||||
chatContentModule.chatDetails.type !== Constants.chatType.communityChat
|
||||
}
|
||||
chatInfoButton.onClicked: {
|
||||
switch (chatContentModule.chatDetails.type) {
|
||||
case Constants.chatType.privateGroupChat:
|
||||
Global.openPopup(groupInfoPopupComponent, {
|
||||
chatContentModule: chatContentModule,
|
||||
chatDetails: chatContentModule.chatDetails
|
||||
})
|
||||
break;
|
||||
case Constants.chatType.oneToOne:
|
||||
Global.openProfilePopup(chatContentModule.chatDetails.id)
|
||||
break;
|
||||
}
|
||||
}
|
||||
toolbarComponent: statusChatInfoButton
|
||||
|
||||
membersButton.visible: {
|
||||
if(!chatContentModule || chatContentModule.chatDetails.type === Constants.chatType.publicChat)
|
||||
|
@ -258,7 +283,7 @@ ColumnLayout {
|
|||
)
|
||||
}
|
||||
onAddRemoveGroupMember: {
|
||||
chatContentRoot.rootStore.addRemoveGroupMember();
|
||||
topBar.toolbarComponent = contactsSelector
|
||||
}
|
||||
onFetchMoreMessages: {
|
||||
chatContentRoot.rootStore.messageStore.requestMoreMessages();
|
||||
|
@ -298,7 +323,7 @@ ColumnLayout {
|
|||
Connections {
|
||||
target: mainModule
|
||||
onOnlineStatusChanged: {
|
||||
if (connected == isConnected) return;
|
||||
if (connected === isConnected) return;
|
||||
isConnected = connected;
|
||||
if(isConnected){
|
||||
timer.setTimeout(function(){
|
||||
|
|
|
@ -46,6 +46,8 @@ StatusPopupMenu {
|
|||
signal fetchMoreMessages(int timeFrame)
|
||||
signal addRemoveGroupMember()
|
||||
|
||||
width: root.amIChatAdmin && (root.chatType === Constants.chatType.privateGroupChat) ? 207 : implicitWidth
|
||||
|
||||
StatusMenuItem {
|
||||
id: viewProfileMenuItem
|
||||
text: {
|
||||
|
@ -72,18 +74,12 @@ StatusPopupMenu {
|
|||
}
|
||||
}
|
||||
|
||||
// TODO needs to be implemented
|
||||
// This should open up the ad-hoc group chat creation view
|
||||
// Design https://www.figma.com/file/17fc13UBFvInrLgNUKJJg5/Kuba%E2%8E%9CDesktop?node-id=417%3A243810
|
||||
// StatusMenuItem {
|
||||
// text: qsTr("Add / remove from group")
|
||||
// icon.name: "notification"
|
||||
// enabled: (root.chatType === Constants.chatType.privateGroupChat &&
|
||||
// amIChatAdmin)
|
||||
// onTriggered: {
|
||||
// root.addRemoveGroupMember();
|
||||
// }
|
||||
// }
|
||||
StatusMenuItem {
|
||||
text: qsTr("Add / remove from group")
|
||||
icon.name: "add-to-dm"
|
||||
enabled: root.amIChatAdmin && (root.chatType === Constants.chatType.privateGroupChat)
|
||||
onTriggered: { root.addRemoveGroupMember() }
|
||||
}
|
||||
|
||||
StatusMenuSeparator {
|
||||
visible: viewProfileMenuItem.enabled
|
||||
|
|
|
@ -88,6 +88,7 @@ Page {
|
|||
color: Theme.palette.statusAppLayout.rightPanelBackgroundColor
|
||||
}
|
||||
|
||||
// TODO: Could it be replaced to `GroupChatPanel`?
|
||||
header: RowLayout {
|
||||
id: headerRow
|
||||
width: parent.width
|
||||
|
|
Loading…
Reference in New Issue