From 23b103795ad9d0ee53e097922fc78e15130ab809 Mon Sep 17 00:00:00 2001 From: Pascal Precht Date: Wed, 30 Jun 2021 16:17:05 +0200 Subject: [PATCH] refactor(ContactsColumn): replace `ChannelList` with `StatusChatList` Closes #2745 --- src/app/chat/views/channel.nim | 57 ++++ ui/StatusQ | 2 +- ui/app/AppLayouts/Chat/ChatColumn.qml | 9 + ui/app/AppLayouts/Chat/ContactsColumn.qml | 318 +++++++++++++----- .../Chat/ContactsColumn/EmptyView.qml | 1 - ui/app/AppMain.qml | 17 + 6 files changed, 317 insertions(+), 87 deletions(-) diff --git a/src/app/chat/views/channel.nim b/src/app/chat/views/channel.nim index af2b455d1a..4b25645ff6 100644 --- a/src/app/chat/views/channel.nim +++ b/src/app/chat/views/channel.nim @@ -19,6 +19,7 @@ QtObject: activeChannel*: ChatItemView previousActiveChannelIndex*: int contextChannel*: ChatItemView + chatItemViews: Table[string, ChatItemView] proc setup(self: ChannelView) = self.QObject.setup proc delete*(self: ChannelView) = @@ -84,12 +85,21 @@ QtObject: proc contextChannelChanged*(self: ChannelView) {.signal.} + # TODO(pascal): replace with `markChatItemAsRead`, which is id based + # instead of index based, when refactoring/removing `CommunityColumn` and `ChannelContextMenu` + # (they still make use of this) proc markAllChannelMessagesReadByIndex*(self: ChannelView, channelIndex: int) {.slot.} = if (self.chats.chats.len == 0): return let selectedChannel = self.getChannel(channelIndex) if (selectedChannel == nil): return discard self.status.chat.markAllChannelMessagesRead(selectedChannel.id) + proc markChatItemAsRead*(self: ChannelView, id: string) {.slot.} = + if (self.chats.chats.len == 0): return + let selectedChannel = self.getChannelById(id) + if (selectedChannel == nil): return + discard self.status.chat.markAllChannelMessagesRead(selectedChannel.id) + proc clearUnreadIfNeeded*(self: ChannelView, channel: var Chat) = if (not channel.isNil and (channel.unviewedMessagesCount > 0 or channel.mentionsCount > 0)): var response = self.status.chat.markAllChannelMessagesRead(channel.id) @@ -188,6 +198,9 @@ QtObject: self.setActiveChannel(pubKey) ChatType.OneToOne.int + # TODO(pascal): replace with `leaveChat`, which is id based + # instead of index based, when refactoring/removing `CommunityColumn` and `ChannelContextMenu` + # (they still make use of this) proc leaveChatByIndex*(self: ChannelView, channelIndex: int) {.slot.} = if (self.chats.chats.len == 0): return let selectedChannel = self.getChannel(channelIndex) @@ -196,6 +209,14 @@ QtObject: self.activeChannel.chatItem = nil self.status.chat.leave(selectedChannel.id) + proc leaveChat*(self: ChannelView, id: string) {.slot.} = + if (self.chats.chats.len == 0): return + let selectedChannel = self.getChannelById(id) + if (selectedChannel == nil): return + if (self.activeChannel.id == selectedChannel.id): + self.activeChannel.chatItem = nil + self.status.chat.leave(selectedChannel.id) + proc leaveActiveChat*(self: ChannelView) {.slot.} = self.status.chat.leave(self.activeChannel.id) @@ -220,6 +241,9 @@ QtObject: channel.muted = false self.updateChannelInRightList(channel) + # TODO(pascal): replace with `muteChatItem`, which is id based + # instead of index based, when refactoring/removing `CommunityColumn` and `ChannelContextMenu` + # (they still make use of this) proc muteChannel*(self: ChannelView, channelIndex: int) {.slot.} = if (self.chats.chats.len == 0): return let selectedChannel = self.getChannel(channelIndex) @@ -231,6 +255,20 @@ QtObject: self.status.chat.muteChat(selectedChannel) self.updateChannelInRightList(selectedChannel) + proc muteChatItem*(self: ChannelView, id: string) {.slot.} = + if (self.chats.chats.len == 0): return + let selectedChannel = self.getChannelById(id) + if (selectedChannel == nil): return + if (selectedChannel.id == self.activeChannel.id): + self.muteCurrentChannel() + return + selectedChannel.muted = true + self.status.chat.muteChat(selectedChannel) + self.updateChannelInRightList(selectedChannel) + + # TODO(pascal): replace with `unmuteChatItem`, which is id based + # instead of index based, when refactoring/removing `CommunityColumn` and `ChannelContextMenu` + # (they still make use of this) proc unmuteChannel*(self: ChannelView, channelIndex: int) {.slot.} = if (self.chats.chats.len == 0): return let selectedChannel = self.getChannel(channelIndex) @@ -242,8 +280,27 @@ QtObject: self.status.chat.unmuteChat(selectedChannel) self.updateChannelInRightList(selectedChannel) + proc unmuteChatItem*(self: ChannelView, id: string) {.slot.} = + if (self.chats.chats.len == 0): return + let selectedChannel = self.getChannelById(id) + if (selectedChannel == nil): return + if (selectedChannel.id == self.activeChannel.id): + self.unmuteCurrentChannel() + return + selectedChannel.muted = false + self.status.chat.unmuteChat(selectedChannel) + self.updateChannelInRightList(selectedChannel) + proc channelIsMuted*(self: ChannelView, channelIndex: int): bool {.slot.} = if (self.chats.chats.len == 0): return false let selectedChannel = self.getChannel(channelIndex) if (selectedChannel == nil): return false result = selectedChannel.muted + + proc getChatItemById*(self: ChannelView, id: string): QObject {.slot.} = + if self.chatItemViews.hasKey(id): return self.chatItemViews[id] + let chat = self.getChannelById(id) + let chatItemView = newChatItemView(self.status) + chatItemView.setChatItem(chat) + self.chatItemViews[id] = chatItemView + return chatItemView diff --git a/ui/StatusQ b/ui/StatusQ index fd7a5530cf..7136968b3a 160000 --- a/ui/StatusQ +++ b/ui/StatusQ @@ -1 +1 @@ -Subproject commit fd7a5530cf6c4395eec616dbf4622266793cd2b3 +Subproject commit 7136968b3ad50d6d222849d53c793ecf82566786 diff --git a/ui/app/AppLayouts/Chat/ChatColumn.qml b/ui/app/AppLayouts/Chat/ChatColumn.qml index c677ac545f..67ce34a693 100644 --- a/ui/app/AppLayouts/Chat/ChatColumn.qml +++ b/ui/app/AppLayouts/Chat/ChatColumn.qml @@ -183,6 +183,15 @@ StackLayout { } } + Connections { + target: chatsModel.channelView + onActiveChannelChanged: { + chatsModel.messageView.hideLoadingIndicator() + SelectedMessage.reset(); + chatColumn.isReply = false; + } + } + function clickOnNotification() { applicationWindow.show() applicationWindow.raise() diff --git a/ui/app/AppLayouts/Chat/ContactsColumn.qml b/ui/app/AppLayouts/Chat/ContactsColumn.qml index 7ecddadd0f..4686d54401 100644 --- a/ui/app/AppLayouts/Chat/ContactsColumn.qml +++ b/ui/app/AppLayouts/Chat/ContactsColumn.qml @@ -10,23 +10,233 @@ import "./components" import "./ContactsColumn" import "./CommunityComponents" -Rectangle { - property alias chatGroupsListViewCount: channelList.channelListCount +import StatusQ.Core 0.1 +import StatusQ.Core.Theme 0.1 +import StatusQ.Components 0.1 +import StatusQ.Popups 0.1 + +Item { + property int chatGroupsListViewCount: channelList.chatListItems.count property alias searchStr: searchBox.text id: contactsColumn - Layout.fillHeight: true - color: Style.current.secondaryMenuBackground - StyledText { - id: title - //% "Chat" - text: qsTrId("chat") + Layout.fillHeight: true + width: 304 + + StatusNavigationPanelHeadline { + id: headline anchors.top: parent.top - anchors.topMargin: Style.current.padding + anchors.topMargin: 16 anchors.horizontalCenter: parent.horizontalCenter - font.weight: Font.Bold - font.pixelSize: 17 + text: qsTr("Chat") + } + + SearchBox { + id: searchBox + anchors.top: headline.bottom + anchors.topMargin: Style.current.padding + anchors.right: addChat.left + anchors.rightMargin: Style.current.padding + anchors.left: parent.left + anchors.leftMargin: Style.current.padding + } + + AddChat { + id: addChat + anchors.right: parent.right + anchors.rightMargin: Style.current.padding + anchors.top: headline.bottom + anchors.topMargin: Style.current.padding + } + + StatusContactRequestsIndicatorListItem { + id: contactRequests + + property int nbRequests: profileModel.contacts.contactRequests.count + + anchors.top: searchBox.bottom + anchors.topMargin: visible ? Style.current.padding : 0 + anchors.horizontalCenter: parent.horizontalCenter + + visible: nbRequests > 0 + height: visible ? implicitHeight : 0 + + title: qsTr("Contact requests") + requestsCount: nbRequests + + sensor.onClicked: openPopup(contactRequestsPopup) + } + + ScrollView { + id: chatGroupsContainer + + anchors.top: contactRequests.bottom + anchors.topMargin: Style.current.padding + anchors.bottom: parent.bottom + anchors.horizontalCenter: parent.horizontalCenter + + width: parent.width + + leftPadding: Style.current.halfPadding + rightPadding: Style.current.halfPadding + + ScrollBar.horizontal.policy: ScrollBar.AlwaysOff + + contentHeight: channelList.height + 2 * Style.current.padding + emptyViewAndSuggestions.height + emptyViewAndSuggestions.anchors.topMargin + clip: true + + Item { + id: noSearchResults + anchors.top: parent.top + anchors.horizontalCenter: parent.horizontalCenter + width: parent.width + visible: !!!channelList.height && contactsColumn.searchStr !== "" + height: visible ? 300 : 0 + + StatusBaseText { + font.pixelSize: 15 + color: Theme.palette.directColor5 + anchors.centerIn: parent + text: qsTr("No search results") + } + } + + StatusChatList { + id: channelList + + chatNameFn: function (chatItem) { + return chatItem.chatType !== Constants.chatTypePublic ? + Emoji.parse(Utils.removeStatusEns(Utils.filterXSS(chatItem.name))) : + Utils.filterXSS(chatItem.name) + } + + profileImageFn: function (id) { + return appMain.getProfileImage(id) + } + + filterFn: function (chatListItem) { + return !!!contactsColumn.searchStr || chatListItem.name.toLowerCase().includes(contactsColumn.searchStr.toLowerCase()) + } + + Connections { + target: profileModel.contacts.list + onContactChanged: { + for (var i = 0; i < channelList.chatListItems.count; i++) { + let chatItem = channelList.chatListItems.itemAt(i); + if (chatItem.chatId === pubkey) { + let profileImage = appMain.getProfileImage(pubkey) + if (!!profileImage) { + chatItem.image.isIdenticon = false + chatItem.image.source = profileImage + } + break; + } + } + } + } + + chatListItems.model: chatsModel.channelView.chats + selectedChatId: chatsModel.channelView.activeChannel.id + + onChatItemSelected: chatsModel.channelView.setActiveChannel(id) + onChatItemUnmuted: chatsModel.channelView.unmuteChatItem(id) + + popupMenu: StatusPopupMenu { + + id: chatListContextMenu + + property var chatItem + + openHandler: function (id) { + chatItem = chatsModel.channelView.getChatItemById(id) + } + + StatusMenuItem { + id: viewProfileMenuItem + text: { + if (chatItem) { + switch (chatItem.chatType) { + case Constants.chatTypeOneToOne: + return qsTr("View Profile") + break; + case Constants.chatTypePrivateGroupChat: + return qsTr("View Group") + break; + default: + return qsTr("Share Chat") + break; + } + } + return "" + } + icon.name: "group-chat" + enabled: chatItem && chatItem.chatType !== Constants.chatTypePublic + onTriggered: { + if (chatItem.chatType === Constants.chatTypeOneToOne) { + const userProfileImage = appMain.getProfileImage(chatItem.id) + return openProfilePopup( + chatItem.name, + chatItem.id, + userProfileImage || chatItem.identicon + ) + } + if (chatItem.chatType === Constants.chatTypePrivateGroupChat) { + return openPopup(groupInfoPopupComponent, {channelType: GroupInfoPopup.ChannelType.ContextChannel}) + } + } + } + + StatusMenuSeparator { + visible: viewProfileMenuItem.enabled + } + + StatusMenuItem { + text: chatItem && chatItem.muted ? + qsTr("Unmute chat") : + qsTr("Mute chat") + icon.name: "notification" + onTriggered: { + if (chatItem && chatItem.muted) { + return chatsModel.channelView.unmuteChatItem(chatItem.id) + } + chatsModel.channelView.muteChatItem(chatItem.id) + } + } + + StatusMenuItem { + text: "Mark as Read" + icon.name: "checkmark-circle" + onTriggered: chatsModel.channelView.markChatItemAsRead(chatItem.id) + } + + StatusMenuItem { + text: "Clear history" + icon.name: "close-circle" + onTriggered: chatsModel.channelView.clearChatHistory(chatItem.id) + } + + StatusMenuSeparator {} + + StatusMenuItem { + text: chatItem && chatItem.chatType === Constants.chatTypeOneToOne ? "Delete chat" : "Leave chat" + icon.name: chatItem && chatItem.chatType === Constants.chatTypeOneToOne ? "delete" : "arrow-right" + icon.width: chatItem && chatItem.chatType === Constants.chatTypeOneToOne ? 18 : 14 + iconRotation: chatItem && chatItem.chatType === Constants.chatTypeOneToOne ? 0 : 180 + + type: StatusMenuItem.Type.Danger + onTriggered: openPopup(deleteChatConfirmationDialogComponent, { chatId: chatItem.id }) + } + } + } + + EmptyView { + id: emptyViewAndSuggestions + width: parent.width + visible: !appSettings.hideChannelSuggestions && !noSearchResults.visible + anchors.top: noSearchResults.visible ? noSearchResults.bottom : channelList.bottom + anchors.topMargin: 32 + } } Component { @@ -101,84 +311,22 @@ Rectangle { } } - SearchBox { - id: searchBox - anchors.top: title.bottom - anchors.topMargin: Style.current.padding - anchors.right: addChat.left - anchors.rightMargin: Style.current.padding - anchors.left: parent.left - anchors.leftMargin: Style.current.padding - } - - AddChat { - id: addChat - anchors.right: parent.right - anchors.rightMargin: Style.current.padding - anchors.top: title.bottom - anchors.topMargin: Style.current.padding - } - - Connections { - target: profileModel.contacts - onContactRequestAdded: { - if (!appSettings.notifyOnNewRequests) { - return + Component { + id: deleteChatConfirmationDialogComponent + ConfirmationDialog { + property string chatId + btnType: "warn" + confirmationText: qsTr("Are you sure you want to leave this chat?") + onClosed: { + destroy() + } + onConfirmButtonClicked: { + chatsModel.channelView.leaveChat(chatId) + close(); } - const isContact = profileModel.contacts.isAdded(address) - systemTray.showMessage(isContact ? qsTr("Contact request accepted") : - qsTr("New contact request"), - isContact ? qsTr("You can now chat with %1").arg(Utils.removeStatusEns(name)) : - qsTr("%1 requests to become contacts").arg(Utils.removeStatusEns(name)), - SystemTrayIcon.NoIcon, - Constants.notificationPopupTTL) } } - StatusSettingsLineButton { - property int nbRequests: profileModel.contacts.contactRequests.count - - id: contactRequest - anchors.top: searchBox.bottom - anchors.topMargin: visible ? Style.current.padding : 0 - anchors.left: parent.left - anchors.leftMargin: Style.current.halfPadding - anchors.right: parent.right - anchors.rightMargin: Style.current.halfPadding - visible: nbRequests > 0 - height: visible ? implicitHeight : 0 - text: qsTr("Contact requests") - isBadge: true - badgeText: nbRequests.toString() - onClicked: openPopup(contactRequestsPopup) - } - - ScrollView { - id: chatGroupsContainer - anchors.top: contactRequest.bottom - anchors.topMargin: Style.current.padding - anchors.bottom: parent.bottom - anchors.left: parent.left - anchors.right: parent.right - leftPadding: Style.current.halfPadding - rightPadding: Style.current.halfPadding - ScrollBar.horizontal.policy: ScrollBar.AlwaysOff - contentHeight: channelList.height + 2 * Style.current.padding + emptyViewAndSuggestions.height - clip: true - - ChannelList { - id: channelList - searchStr: contactsColumn.searchStr.toLowerCase() - channelModel: chatsModel.channelView.chats - } - - EmptyView { - id: emptyViewAndSuggestions - width: parent.width - anchors.top: channelList.bottom - anchors.topMargin: Style.current.smallPadding - } - } } /*##^## diff --git a/ui/app/AppLayouts/Chat/ContactsColumn/EmptyView.qml b/ui/app/AppLayouts/Chat/ContactsColumn/EmptyView.qml index c655c8cc27..6571102d8f 100644 --- a/ui/app/AppLayouts/Chat/ContactsColumn/EmptyView.qml +++ b/ui/app/AppLayouts/Chat/ContactsColumn/EmptyView.qml @@ -12,7 +12,6 @@ Rectangle { id: emptyView Layout.fillHeight: true Layout.fillWidth: true - visible: !appSettings.hideChannelSuggestions height: suggestionContainer.height + inviteFriendsContainer.height + Style.current.padding * 2 border.color: Style.current.secondaryMenuBorder diff --git a/ui/app/AppMain.qml b/ui/app/AppMain.qml index 9169d1a83a..361f540531 100644 --- a/ui/app/AppMain.qml +++ b/ui/app/AppMain.qml @@ -11,6 +11,7 @@ import "./AppLayouts/Timeline" import "./AppLayouts/Wallet" import "./AppLayouts/Chat/components" import "./AppLayouts/Chat/CommunityComponents" +import Qt.labs.platform 1.1 import Qt.labs.settings 1.0 import StatusQ.Core.Theme 0.1 @@ -458,6 +459,22 @@ StatusAppLayout { } } + Connections { + target: profileModel.contacts + onContactRequestAdded: { + if (!appSettings.notifyOnNewRequests) { + return + } + const isContact = profileModel.contacts.isAdded(address) + systemTray.showMessage(isContact ? qsTr("Contact request accepted") : + qsTr("New contact request"), + isContact ? qsTr("You can now chat with %1").arg(Utils.removeStatusEns(name)) : + qsTr("%1 requests to become contacts").arg(Utils.removeStatusEns(name)), + SystemTrayIcon.NoIcon, + Constants.notificationPopupTTL) + } + } + Component { id: chooseBrowserPopupComponent ChooseBrowserPopup {