From 914c7b28391289c93890bb9b46dccfb19bd7a90e Mon Sep 17 00:00:00 2001 From: Patryk Osmaczko Date: Fri, 4 Feb 2022 14:07:48 +0100 Subject: [PATCH] fix(@desktop/sections): make ctrl+k display chats from communities Closes: #4059 --- src/app/modules/main/chat_search_item.nim | 35 ++++ src/app/modules/main/chat_search_model.nim | 66 +++++++ src/app/modules/main/chat_section/model.nim | 3 + src/app/modules/main/chat_section/module.nim | 6 +- .../module_access_interface.nim | 7 +- .../modules/main/chat_section/sub_model.nim | 3 + src/app/modules/main/controller.nim | 5 +- src/app/modules/main/controller_interface.nim | 5 +- src/app/modules/main/module.nim | 26 ++- .../module_view_delegate_interface.nim | 9 +- src/app/modules/main/view.nim | 29 ++- ui/StatusQ | 2 +- ui/app/AppLayouts/stores/RootStore.qml | 10 ++ ui/app/AppMain.qml | 80 ++++----- .../shared/status/StatusSearchListPopup.qml | 166 ++++++++++++++++++ ui/imports/shared/status/qmldir | 1 + 16 files changed, 400 insertions(+), 53 deletions(-) create mode 100644 src/app/modules/main/chat_search_item.nim create mode 100644 src/app/modules/main/chat_search_model.nim create mode 100644 ui/imports/shared/status/StatusSearchListPopup.qml diff --git a/src/app/modules/main/chat_search_item.nim b/src/app/modules/main/chat_search_item.nim new file mode 100644 index 0000000000..535e7dfde4 --- /dev/null +++ b/src/app/modules/main/chat_search_item.nim @@ -0,0 +1,35 @@ +type + Item* = ref object + chatId: string + name: string + color: string + icon: string + sectionId: string + sectionName: string + +proc initItem*(chatId, name, color, icon, sectionId, sectionName: string): Item = + result = Item() + result.chatId = chatId + result.name = name + result.color = color + result.icon = icon + result.sectionId = sectionId + result.sectionName = sectionName + +proc chatId*(self: Item): string = + self.chatId + +proc name*(self: Item): string = + self.name + +proc color*(self: Item): string = + self.color + +proc icon*(self: Item): string = + self.icon + +proc sectionId*(self: Item): string = + self.sectionId + +proc sectionName*(self: Item): string = + self.sectionName diff --git a/src/app/modules/main/chat_search_model.nim b/src/app/modules/main/chat_search_model.nim new file mode 100644 index 0000000000..bf170873e3 --- /dev/null +++ b/src/app/modules/main/chat_search_model.nim @@ -0,0 +1,66 @@ +import NimQml, Tables +import chat_search_item + +type + ModelRole {.pure.} = enum + ChatId = UserRole + 1 + Name + Color + Icon + SectionId + SectionName + +QtObject: + type Model* = ref object of QAbstractListModel + items: seq[Item] + + proc setup(self: Model) = + self.QAbstractListModel.setup + + proc delete(self: Model) = + self.items = @[] + self.QAbstractListModel.delete + + proc newModel*(): Model = + new(result, delete) + result.setup + + proc setItems*(self: Model, items: seq[Item]) = + self.beginResetModel() + self.items = items + self.endResetModel() + + method rowCount(self: Model, index: QModelIndex = nil): int = + return self.items.len + + method roleNames(self: Model): Table[int, string] = + { + ModelRole.ChatId.int:"chatId", + ModelRole.Name.int:"name", + ModelRole.Color.int:"color", + ModelRole.Icon.int:"icon", + ModelRole.SectionId.int:"sectionId", + ModelRole.SectionName.int:"sectionName", + }.toTable + + method data(self: Model, index: QModelIndex, role: int): QVariant = + if not index.isValid: + return + if index.row < 0 or index.row >= self.items.len: + return + let item = self.items[index.row] + let enumRole = role.ModelRole + + case enumRole: + of ModelRole.ChatId: + result = newQVariant(item.chatId) + of ModelRole.Name: + result = newQVariant(item.name) + of ModelRole.Color: + result = newQVariant(item.color) + of ModelRole.Icon: + result = newQVariant(item.icon) + of ModelRole.SectionId: + result = newQVariant(item.sectionId) + of ModelRole.SectionName: + result = newQVariant(item.sectionName) diff --git a/src/app/modules/main/chat_section/model.nim b/src/app/modules/main/chat_section/model.nim index f07c0ed578..a0e8d67611 100644 --- a/src/app/modules/main/chat_section/model.nim +++ b/src/app/modules/main/chat_section/model.nim @@ -64,6 +64,9 @@ QtObject: method rowCount(self: Model, index: QModelIndex = nil): int = return self.items.len + proc items*(self: Model): seq[Item] = + return self.items + method roleNames(self: Model): Table[int, string] = { ModelRole.Id.int:"itemId", diff --git a/src/app/modules/main/chat_section/module.nim b/src/app/modules/main/chat_section/module.nim index 1fc5692588..6af61e8e27 100644 --- a/src/app/modules/main/chat_section/module.nim +++ b/src/app/modules/main/chat_section/module.nim @@ -1,7 +1,8 @@ import NimQml, Tables, chronicles, json, sequtils import io_interface import ../io_interface as delegate_interface -import view, controller, item, sub_item, model, sub_model, base_item +import view, controller, item, sub_item, sub_model, base_item +import model as chats_model import ../../shared_models/contacts_item as contacts_item import ../../shared_models/contacts_model as contacts_model @@ -355,6 +356,9 @@ method onActiveSectionChange*(self: Module, sectionId: string) = self.updateNotifications(self.controller.getActiveChatId(), unviewedMessagesCount=0, unviewedMentionsCount=0) self.delegate.onActiveChatChange(self.controller.getMySectionId(), self.controller.getActiveChatId()) +method chatsModel*(self: Module): chats_model.Model = + return self.view.chatsModel() + method createPublicChat*(self: Module, chatId: string) = if(self.controller.isCommunity()): debug "creating public chat is not allowed for community, most likely it's an error in qml", methodName="createPublicChat" diff --git a/src/app/modules/main/chat_section/private_interfaces/module_access_interface.nim b/src/app/modules/main/chat_section/private_interfaces/module_access_interface.nim index 8e8665b43d..f67c8a6e77 100644 --- a/src/app/modules/main/chat_section/private_interfaces/module_access_interface.nim +++ b/src/app/modules/main/chat_section/private_interfaces/module_access_interface.nim @@ -8,6 +8,8 @@ import ../../../../../app_service/service/message/service as message_service import ../../../../../app_service/service/gif/service as gif_service import ../../../../../app_service/service/mailservers/service as mailservers_service +import ../model as chats_model + import ../../../../core/eventemitter method delete*(self: AccessInterface) {.base.} = @@ -30,4 +32,7 @@ method getModuleAsVariant*(self: AccessInterface): QVariant {.base.} = raise newException(ValueError, "No implementation available") method onActiveSectionChange*(self: AccessInterface, sectionId: string) {.base.} = - raise newException(ValueError, "No implementation available") \ No newline at end of file + raise newException(ValueError, "No implementation available") + +method chatsModel*(self: AccessInterface): chats_model.Model {.base.} = + raise newException(ValueError, "No implementation available") diff --git a/src/app/modules/main/chat_section/sub_model.nim b/src/app/modules/main/chat_section/sub_model.nim index ba38854644..fdaa268918 100644 --- a/src/app/modules/main/chat_section/sub_model.nim +++ b/src/app/modules/main/chat_section/sub_model.nim @@ -57,6 +57,9 @@ QtObject: method rowCount(self: SubModel, index: QModelIndex = nil): int = return self.items.len + proc items*(self: SubModel): seq[SubItem] = + return self.items + method roleNames(self: SubModel): Table[int, string] = { ModelRole.Id.int:"itemId", diff --git a/src/app/modules/main/controller.nim b/src/app/modules/main/controller.nim index 6192f66e69..87b9da60bf 100644 --- a/src/app/modules/main/controller.nim +++ b/src/app/modules/main/controller.nim @@ -251,4 +251,7 @@ method resolveENS*(self: Controller, ensName: string, uuid: string = "") = self.contactsService.resolveENS(ensName, uuid) method isMnemonicBackedUp*(self: Controller): bool = - result = self.privacyService.isMnemonicBackedUp() \ No newline at end of file + result = self.privacyService.isMnemonicBackedUp() + +method switchTo*(self: Controller, sectionId, chatId: string) = + self.messageService.switchTo(sectionId, chatId, "") diff --git a/src/app/modules/main/controller_interface.nim b/src/app/modules/main/controller_interface.nim index e79f4b5cd8..2e14d01e28 100644 --- a/src/app/modules/main/controller_interface.nim +++ b/src/app/modules/main/controller_interface.nim @@ -48,4 +48,7 @@ method resolveENS*(self: AccessInterface, ensName: string, uuid: string = "") {. raise newException(ValueError, "No implementation available") method isMnemonicBackedUp*(self: AccessInterface): bool {.base.} = - raise newException(ValueError, "No implementation available") \ No newline at end of file + raise newException(ValueError, "No implementation available") + +method switchTo*(self: AccessInterface, sectionId, chatId: string) {.base.} = + raise newException(ValueError, "No implementation available") diff --git a/src/app/modules/main/module.nim b/src/app/modules/main/module.nim index 27f2253c67..72e5efdffb 100644 --- a/src/app/modules/main/module.nim +++ b/src/app/modules/main/module.nim @@ -1,12 +1,15 @@ import NimQml, tables, json, sugar, sequtils -import io_interface, view, controller +import io_interface, view, controller, chat_search_item, chat_search_model import ./communities/models/[pending_request_item, pending_request_model] import ../shared_models/[user_item, user_model, section_item, section_model, active_section] import ../../global/app_sections_config as conf import ../../global/app_signals import ../../global/global_singleton +import chat_section/[model, sub_item, sub_model] +import chat_section/base_item as chat_section_base_item +import chat_section/item as chat_section_item import chat_section/module as chat_section_module import wallet_section/module as wallet_section_module import browser_section/module as browser_section_module @@ -508,6 +511,27 @@ method getCommunitySectionModule*[T](self: Module[T], communityId: string): QVar return self.communitySectionsModule[communityId].getModuleAsVariant() +method rebuildChatSearchModel*[T](self: Module[T]) = + let transformItem = proc(item: chat_section_base_item.BaseItem, sectionId, sectionName: string): chat_search_item.Item = + result = chat_search_item.initItem(item.id(), item.name(), item.color(), item.icon(), sectionId, sectionName) + + let transform = proc(items: seq[chat_section_item.Item], sectionId, sectionName: string): seq[chat_search_item.Item] = + for item in items: + if item.type() != ChatType.Unknown.int: + result.add(transformItem(item, sectionId, sectionName)) + else: + for subItem in item.subItems().items(): + result.add(transformItem(subItem, sectionId, sectionName)) + + var items = transform(self.chatSectionModule.chatsModel().items(), conf.CHAT_SECTION_ID, conf.CHAT_SECTION_NAME) + for cId in self.communitySectionsModule.keys: + items.add(transform(self.communitySectionsModule[cId].chatsModel().items(), cId, self.view.model().getItemById(cId).name())) + + self.view.chatSearchModel().setItems(items) + +method switchTo*[T](self: Module[T], sectionId, chatId: string) = + self.controller.switchTo(sectionId, chatId) + method onActiveChatChange*[T](self: Module[T], sectionId: string, chatId: string) = self.appSearchModule.onActiveChatChange(sectionId, chatId) diff --git a/src/app/modules/main/private_interfaces/module_view_delegate_interface.nim b/src/app/modules/main/private_interfaces/module_view_delegate_interface.nim index 424b52ad4c..6cc6f07b39 100644 --- a/src/app/modules/main/private_interfaces/module_view_delegate_interface.nim +++ b/src/app/modules/main/private_interfaces/module_view_delegate_interface.nim @@ -1,5 +1,6 @@ import NimQml import ../../shared_models/section_item +import ../chat_search_item method viewDidLoad*(self: AccessInterface) {.base.} = raise newException(ValueError, "No implementation available") @@ -26,4 +27,10 @@ method getContactDetailsAsJson*(self: AccessInterface, publicKey: string): strin raise newException(ValueError, "No implementation available") method resolveENS*(self: AccessInterface, ensName: string, uuid: string) {.base.} = - raise newException(ValueError, "No implementation available") \ No newline at end of file + raise newException(ValueError, "No implementation available") + +method rebuildChatSearchModel*(self: AccessInterface) {.base.} = + raise newException(ValueError, "No implementation available") + +method switchTo*(self: AccessInterface, sectionId, chatId: string) {.base.} = + raise newException(ValueError, "No implementation available") diff --git a/src/app/modules/main/view.nim b/src/app/modules/main/view.nim index 6d71a63d76..45dc44394e 100644 --- a/src/app/modules/main/view.nim +++ b/src/app/modules/main/view.nim @@ -3,15 +3,18 @@ import ../shared_models/section_model import ../shared_models/section_item import ../shared_models/active_section import io_interface +import chat_search_model QtObject: type View* = ref object of QObject delegate: io_interface.AccessInterface - model: SectionModel + model: section_model.SectionModel modelVariant: QVariant activeSection: ActiveSection activeSectionVariant: QVariant + chatSearchModel: chat_search_model.Model + chatSearchModelVariant: QVariant tmpCommunityId: string # shouldn't be used anywhere except in prepareCommunitySectionModuleForCommunityId/getCommunitySectionModule procs proc activeSectionChanged*(self:View) {.signal.} @@ -21,16 +24,20 @@ QtObject: self.modelVariant.delete self.activeSection.delete self.activeSectionVariant.delete + self.chatSearchModel.delete + self.chatSearchModelVariant.delete self.QObject.delete proc newView*(delegate: io_interface.AccessInterface): View = new(result, delete) result.QObject.setup result.delegate = delegate - result.model = newModel() + result.model = section_model.newModel() result.modelVariant = newQVariant(result.model) result.activeSection = newActiveSection() result.activeSectionVariant = newQVariant(result.activeSection) + result.chatSearchModel = chat_search_model.newModel() + result.chatSearchModelVariant = newQVariant(result.chatSearchModel) proc load*(self: View) = # In some point, here, we will setup some exposed main module related things. @@ -54,6 +61,21 @@ QtObject: read = getModel notify = modelChanged + proc chatSearchModel*(self: View): chat_search_model.Model = + return self.chatSearchModel + + proc chatSearchModelChanged*(self: View) {.signal.} + + proc getChatSearchModel(self: View): QVariant {.slot.} = + return self.chatSearchModelVariant + + proc rebuildChatSearchModel*(self: View) {.slot.} = + self.delegate.rebuildChatSearchModel() + + QtProperty[QVariant] chatSearchModel: + read = getChatSearchModel + notify = chatSearchModelChanged + proc openStoreToKeychainPopup*(self: View) {.signal.} proc offerToStorePassword*(self: View) = @@ -96,6 +118,9 @@ QtObject: let item = self.model.getItemBySectionType(sectionType.SectionType) self.delegate.setActiveSection(item) + proc switchTo*(self: View, sectionId: string, chatId: string) {.slot.} = + self.delegate.switchTo(sectionId, chatId) + proc setUserStatus*(self: View, status: bool) {.slot.} = self.delegate.setUserStatus(status) diff --git a/ui/StatusQ b/ui/StatusQ index d85ed4c3ed..5780f183c7 160000 --- a/ui/StatusQ +++ b/ui/StatusQ @@ -1 +1 @@ -Subproject commit d85ed4c3ed95fa1f32d8e50b2746691e5d9e5e4e +Subproject commit 5780f183c7b3cf63c3abbc584daa72078241e917 diff --git a/ui/app/AppLayouts/stores/RootStore.qml b/ui/app/AppLayouts/stores/RootStore.qml index ca75259802..1068c7733d 100644 --- a/ui/app/AppLayouts/stores/RootStore.qml +++ b/ui/app/AppLayouts/stores/RootStore.qml @@ -19,6 +19,16 @@ QtObject { property EmojiReactions emojiReactionsModel: EmojiReactions { } + property var chatSearchModel: mainModuleInst.chatSearchModel + + function rebuildChatSearchModel() { + mainModuleInst.rebuildChatSearchModel() + } + + function setActiveSectionChat(sectionId, chatId) { + mainModuleInst.switchTo(sectionId, chatId) + } + // Not Refactored Yet // property var chatsModelInst: chatsModel // Not Refactored Yet diff --git a/ui/app/AppMain.qml b/ui/app/AppMain.qml index c39e480cc0..708126a803 100644 --- a/ui/app/AppMain.qml +++ b/ui/app/AppMain.qml @@ -286,7 +286,7 @@ Item { // // that's why we're using it this way mainModule.prepareCommunitySectionModuleForCommunityId(model.id) communityContextMenu.chatCommunitySectionModule = mainModule.getCommunitySectionModule() - + } StatusMenuItem { @@ -723,12 +723,12 @@ Item { Global.toastMessage = this; } } - + // Add SendModal here as it is used by the Wallet as well as the Browser Loader { id: sendModal active: false - + function open() { this.active = true this.item.open() @@ -783,52 +783,44 @@ Item { } } - Component { - id: statusSmartIdenticonComponent - StatusSmartIdenticon { - property string imageSource: "" - image: StatusImageSettings { - width: channelPicker.imageWidth - height: channelPicker.imageHeight - source: imageSource - isIdenticon: true - } - icon: StatusIconSettings { - width: channelPicker.imageWidth - height: channelPicker.imageHeight - letterSize: 15 - color: Theme.palette.miscColor5 - } - } - } - - StatusInputListPopup { + StatusSearchListPopup { id: channelPicker - //% "Where do you want to go?" - title: qsTrId("where-do-you-want-to-go-") - showSearchBox: true - width: 350 + x: parent.width / 2 - width / 2 y: parent.height / 2 - height / 2 - // TODO improve this to work with community Chats as well - modelList: mainModule.getChatSectionModule().model - getText: function (modelData) { - return modelData.name - } - getId: function (modelData) { - return modelData.itemId - } - getImageComponent: function (parent, modelData) { - return statusSmartIdenticonComponent.createObject(parent, { - imageSource: modelData.identicon, - name: modelData.name - }); + + searchBoxPlaceholder: qsTr("Where do you want to go?") + model: rootStore.chatSearchModel + delegate: StatusListItem { + property var modelData + property bool isCurrentItem: true + function filterAccepts(searchText) { + return title.includes(searchText) + } + + title: modelData ? modelData.name : "" + label: modelData? modelData.sectionName : "" + highlighted: isCurrentItem + sensor.hoverEnabled: false + statusListItemIcon { + name: modelData ? modelData.name : "" + active: true + } + icon { + width: image.width + height: image.height + color: modelData ? modelData.color : "" + } + image { + source: modelData ? modelData.icon : "" + isIdenticon: true + } } - onClicked: function (index, id) { - Global.changeAppSectionBySectionType(Constants.appSection.chat) - mainModule.getChatSectionModule().setActiveItem(id, "") - channelPicker.close() + onAboutToShow: rootStore.rebuildChatSearchModel() + onSelected: { + rootStore.setActiveSectionChat(modelData.sectionId, modelData.chatId) + close() } } } diff --git a/ui/imports/shared/status/StatusSearchListPopup.qml b/ui/imports/shared/status/StatusSearchListPopup.qml new file mode 100644 index 0000000000..8084bac324 --- /dev/null +++ b/ui/imports/shared/status/StatusSearchListPopup.qml @@ -0,0 +1,166 @@ +import QtQuick 2.14 +import QtQuick.Controls 2.14 +import QtQuick.Layouts 1.14 +import QtGraphicalEffects 1.0 + +import StatusQ.Components 0.1 +import StatusQ.Controls 0.1 +import StatusQ.Core 0.1 +import StatusQ.Core.Theme 0.1 + +import utils 1.0 + +Popup { + id: root + + width: 400 + height: 300 + + property alias model: listView.model + + // delegate interface has to be fulfilled + property Component delegate: Item { + property var modelData + property bool isCurrentItem + function filterAccepts(searchText) { + return true + } + } + property string searchBoxPlaceholder: qsTr("Search...") + + signal selected(int index, var modelData) + + background: Rectangle { + radius: Style.current.radius + color: Style.current.background + border.color: Style.current.border + layer.enabled: true + layer.effect: DropShadow { + verticalOffset: 3 + radius: 8 + samples: 15 + fast: true + cached: true + color: "#22000000" + } + } + + ColumnLayout { + anchors.fill: parent + + StatusInput { + id: searchBox + + Layout.fillWidth: true + leftPadding: 0 + rightPadding: 0 + input.placeholderText: root.searchBoxPlaceholder + input.icon: StatusIconSettings { + width: 24 + height: 24 + name: "search" + color: Theme.palette.baseColor1 + } + + function goToNextAvailableIndex(up) { + var currentIndex = listView.currentIndex + for (var i = 0; i < listView.count; i++) { + currentIndex = up ? (currentIndex === 0 ? listView.count - 1 : currentIndex - 1) + : (currentIndex === listView.count - 1 ? 0 : currentIndex + 1) + listView.currentIndex = currentIndex + if (listView.currentItem.visible) { + return + } + } + listView.currentIndex = 0 + } + + Keys.onReleased: { + listView.selectByHover = false + + if (event.key === Qt.Key_Down) { + searchBox.goToNextAvailableIndex(false) + } + if (event.key === Qt.Key_Up) { + searchBox.goToNextAvailableIndex(true) + } + if (event.key === Qt.Key_Escape) { + return root.close() + } + if (event.key === Qt.Key_Enter || event.key === Qt.Key_Return) { + return root.selected(listView.currentIndex, + listView.currentItem.myData) + } + if (!listView.currentItem.visible) { + goToNextAvailableIndex(false) + } + } + + onTextChanged: if (text === "") listView.currentIndex = 0 + } + + ListView { + id: listView + + Layout.fillWidth: true + Layout.fillHeight: true + + property bool selectByHover: false + + clip: true + highlightMoveDuration: 200 + + delegate: Item { + id: delegateItem + + property var myData: typeof modelData === "undefined" ? model : modelData + + width: listView.width + height: visible ? delegateLoader.height : 0 + + Loader { + id: delegateLoader + + width: parent.width + sourceComponent: root.delegate + + onLoaded: { + item.modelData = delegateItem.myData + item.isCurrentItem = Qt.binding(() => delegateItem.ListView.isCurrentItem) + delegateItem.visible = Qt.binding(() => item.filterAccepts(searchBox.text)) + } + } + + MouseArea { + anchors.fill: parent + + hoverEnabled: true + onClicked: (mouse) => { + listView.currentIndex = index + root.selected(index, delegateItem.myData) + mouse.accepted = false + } + onContainsMouseChanged: if (containsMouse) listView.currentIndex = index + cursorShape: Qt.PointingHandCursor + } + } + + Loader { + anchors.fill: parent + active: !listView.selectByHover + + sourceComponent: MouseArea { + hoverEnabled: true + onPositionChanged: listView.selectByHover = true + } + } + } + } + + onAboutToShow: { + listView.currentIndex = 0 + listView.selectByHover = false + searchBox.text = "" + searchBox.input.edit.forceActiveFocus() + } +} diff --git a/ui/imports/shared/status/qmldir b/ui/imports/shared/status/qmldir index f596185ac2..506a1c6f38 100644 --- a/ui/imports/shared/status/qmldir +++ b/ui/imports/shared/status/qmldir @@ -21,6 +21,7 @@ StatusImageModal 1.0 StatusImageModal.qml StatusImageRadioButton 1.0 StatusImageRadioButton.qml StatusInputListPopup 1.0 StatusInputListPopup.qml StatusNotification 1.0 StatusNotification.qml +StatusSearchListPopup 1.0 StatusSearchListPopup.qml StatusSectionDescItem 1.0 StatusSectionDescItem.qml StatusSectionHeadline 1.0 StatusSectionHeadline.qml StatusSettingsLineButton 1.0 StatusSettingsLineButton.qml