From 8ac6eb891629620be09f79944c88d325f927e651 Mon Sep 17 00:00:00 2001 From: Alex Jbanca Date: Mon, 23 Oct 2023 19:21:35 +0300 Subject: [PATCH] fix(LinkPreviews): Fixing gif hyperlink detection in StatusChatInput The link preview model can be filtered, but the hyperlink detection needs an unfiltered model to properly highlight all URLs. This brought in the need to separate the urls model and the link previews model. --- .../chat_content/input_area/controller.nim | 9 +- .../chat_content/input_area/io_interface.nim | 5 +- .../chat_content/input_area/module.nim | 7 +- .../chat_content/input_area/urls_model.nim | 94 +++++++++++++++++++ .../chat_content/input_area/view.nim | 26 ++++- storybook/pages/StatusChatInputPage.qml | 1 + .../AppLayouts/Chat/views/ChatColumnView.qml | 1 + ui/imports/shared/status/StatusChatInput.qml | 4 +- 8 files changed, 136 insertions(+), 11 deletions(-) create mode 100644 src/app/modules/main/chat_section/chat_content/input_area/urls_model.nim diff --git a/src/app/modules/main/chat_section/chat_content/input_area/controller.nim b/src/app/modules/main/chat_section/chat_content/input_area/controller.nim index 877b49fa18..516a15462b 100644 --- a/src/app/modules/main/chat_section/chat_content/input_area/controller.nim +++ b/src/app/modules/main/chat_section/chat_content/input_area/controller.nim @@ -111,7 +111,7 @@ proc setLinkPreviewEnabledForThisMessage*(self: Controller, enabled: bool) = self.delegate.setAskToEnableLinkPreview(false) proc resetLinkPreviews(self: Controller) = - self.delegate.setUrls(@[]) + self.delegate.setLinkPreviewUrls(@[]) self.linkPreviewCache.clear() self.linkPreviewCurrentMessageSetting = self.linkPreviewPersistentSetting self.delegate.setAskToEnableLinkPreview(false) @@ -201,12 +201,15 @@ proc canAskToEnableLinkPreview(self: Controller): bool = proc setText*(self: Controller, text: string, unfurlNewUrls: bool) = if text == "": self.resetLinkPreviews() + self.delegate.setUrls(@[]) return let urls = self.messageService.getTextUrls(text) + self.delegate.setUrls(urls) + let supportedUrls = urls.filter(x => not x.endsWith(".gif")) # GIFs are currently unfurled by receiver - self.delegate.setUrls(supportedUrls) - let newUrls = self.linkPreviewCache.unknownUrls(urls) + self.delegate.setLinkPreviewUrls(supportedUrls) + let newUrls = self.linkPreviewCache.unknownUrls(supportedUrls) let askToEnableLinkPreview = len(newUrls) > 0 and self.canAskToEnableLinkPreview() self.delegate.setAskToEnableLinkPreview(askToEnableLinkPreview) diff --git a/src/app/modules/main/chat_section/chat_content/input_area/io_interface.nim b/src/app/modules/main/chat_section/chat_content/input_area/io_interface.nim index e7e6118041..44b9376c74 100644 --- a/src/app/modules/main/chat_section/chat_content/input_area/io_interface.nim +++ b/src/app/modules/main/chat_section/chat_content/input_area/io_interface.nim @@ -102,7 +102,7 @@ method setText*(self: AccessInterface, text: string, unfurlUrls: bool) {.base.} method getPlainText*(self: AccessInterface): string = raise newException(ValueError, "No implementation available") -method setUrls*(self: AccessInterface, urls: seq[string]) {.base.} = +method setLinkPreviewUrls*(self: AccessInterface, urls: seq[string]) {.base.} = raise newException(ValueError, "No implementation available") method updateLinkPreviewsFromCache*(self: AccessInterface, urls: seq[string]) {.base.} = @@ -128,3 +128,6 @@ method setAskToEnableLinkPreview*(self: AccessInterface, value: bool) = method setLinkPreviewEnabledForThisMessage*(self: AccessInterface, enabled: bool) = raise newException(ValueError, "No implementation available") + +method setUrls*(self: AccessInterface, urls: seq[string]) {.base.} = + raise newException(ValueError, "No implementation available") diff --git a/src/app/modules/main/chat_section/chat_content/input_area/module.nim b/src/app/modules/main/chat_section/chat_content/input_area/module.nim index 926c50a382..84dfdb22c6 100644 --- a/src/app/modules/main/chat_section/chat_content/input_area/module.nim +++ b/src/app/modules/main/chat_section/chat_content/input_area/module.nim @@ -166,8 +166,8 @@ method clearLinkPreviewCache*(self: Module) {.slot.} = method updateLinkPreviewsFromCache*(self: Module, urls: seq[string]) = self.view.updateLinkPreviewsFromCache(urls) -method setUrls*(self: Module, urls: seq[string]) = - self.view.setUrls(urls) +method setLinkPreviewUrls*(self: Module, urls: seq[string]) = + self.view.setLinkPreviewUrls(urls) method linkPreviewsFromCache*(self: Module, urls: seq[string]): Table[string, LinkPreview] = return self.controller.linkPreviewsFromCache(urls) @@ -186,3 +186,6 @@ method setAskToEnableLinkPreview*(self: Module, value: bool) = method setLinkPreviewEnabledForThisMessage*(self: Module, value: bool) = self.controller.setLinkPreviewEnabledForThisMessage(value) + +method setUrls*(self: Module, urls: seq[string]) = + self.view.setUrls(urls) diff --git a/src/app/modules/main/chat_section/chat_content/input_area/urls_model.nim b/src/app/modules/main/chat_section/chat_content/input_area/urls_model.nim new file mode 100644 index 0000000000..add326c73b --- /dev/null +++ b/src/app/modules/main/chat_section/chat_content/input_area/urls_model.nim @@ -0,0 +1,94 @@ +import NimQml, tables, sequtils + +type + ModelRole {.pure.} = enum + Url = UserRole + 1 + +QtObject: + type + Model* = ref object of QAbstractListModel + items: seq[string] + + proc delete*(self: Model) = + self.items = @[] + self.QAbstractListModel.delete + + proc setup(self: Model) = + self.QAbstractListModel.setup + + proc newUrlsModel*(): Model = + new(result, delete) + result.setup + + proc countChanged(self: Model) {.signal.} + + proc getCount*(self: Model): int {.slot.} = + self.items.len + + QtProperty[int] count: + read = getCount + notify = countChanged + + method rowCount(self: Model, index: QModelIndex = nil): int = + return self.items.len + + method roleNames(self: Model): Table[int, string] = + { + ModelRole.Url.int:"url" + }.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 enumRole = role.ModelRole + + case enumRole: + of ModelRole.Url: + let item = self.items[index.row] + result = newQVariant(item) + else: + result = newQVariant() + + proc removeItemWithIndex(self: Model, ind: int) = + if(ind < 0 or ind >= self.items.len): + return + + let parentModelIndex = newQModelIndex() + defer: parentModelIndex.delete + + self.beginRemoveRows(parentModelIndex, ind, ind) + self.items.delete(ind) + self.endRemoveRows() + + proc setUrls*(self: Model, urls: seq[string]) = + var itemsToInsert: seq[string] + var indexesToRemove: seq[int] + + #remove + for i in 0 ..< self.items.len: + if not urls.anyIt(it == self.items[i]): + indexesToRemove.add(i) + + while indexesToRemove.len > 0: + let index = pop(indexesToRemove) + self.removeItemWithIndex(index) + + # Move or insert + for i in 0 ..< urls.len: + if self.items.anyIt(it == urls[i]): + continue + itemsToInsert.add(urls[i]) + + + if itemsToInsert.len > 0: + let parentModelIndex = newQModelIndex() + defer: parentModelIndex.delete + self.beginInsertRows(parentModelIndex, self.items.len, self.items.len + itemsToInsert.len - 1) + self.items = self.items & itemsToInsert + self.endInsertRows() + + self.countChanged() diff --git a/src/app/modules/main/chat_section/chat_content/input_area/view.nim b/src/app/modules/main/chat_section/chat_content/input_area/view.nim index bbf4d77fc3..87b5329534 100644 --- a/src/app/modules/main/chat_section/chat_content/input_area/view.nim +++ b/src/app/modules/main/chat_section/chat_content/input_area/view.nim @@ -2,6 +2,7 @@ import NimQml import ./io_interface import ./gif_column_model import ./preserved_properties +import ./urls_model import ../../../../../../app/modules/shared_models/link_preview_model as link_preview_model import ../../../../../../app_service/service/gif/dto @@ -17,6 +18,8 @@ QtObject: preservedPropertiesVariant: QVariant linkPreviewModel: link_preview_model.Model linkPreviewModelVariant: QVariant + urlsModel: urls_model.Model + urlsModelVariant: QVariant askToEnableLinkPreview: bool proc delete*(self: View) = @@ -24,10 +27,12 @@ QtObject: self.gifColumnAModel.delete self.gifColumnBModel.delete self.gifColumnCModel.delete - self.preservedProperties.delete self.preservedPropertiesVariant.delete - self.linkPreviewModel.delete + self.preservedProperties.delete self.linkPreviewModelVariant.delete + self.linkPreviewModel.delete + self.urlsModelVariant.delete + self.urlsModel.delete proc newView*(delegate: io_interface.AccessInterface): View = new(result, delete) @@ -41,6 +46,8 @@ QtObject: result.preservedPropertiesVariant = newQVariant(result.preservedProperties) result.linkPreviewModel = newLinkPreviewModel() result.linkPreviewModelVariant = newQVariant(result.linkPreviewModel) + result.urlsModel = newUrlsModel() + result.urlsModelVariant = newQVariant(result.urlsModel) result.askToEnableLinkPreview = false proc load*(self: View) = @@ -223,7 +230,7 @@ QtObject: let linkPreviews = self.delegate.linkPreviewsFromCache(urls) self.linkPreviewModel.updateLinkPreviews(linkPreviews) - proc setUrls*(self: View, urls: seq[string]) = + proc setLinkPreviewUrls*(self: View, urls: seq[string]) = self.linkPreviewModel.setUrls(urls) if(self.delegate.getLinkPreviewEnabled()): self.updateLinkPreviewsFromCache(urls) @@ -253,8 +260,19 @@ QtObject: self.delegate.setLinkPreviewEnabledForThisMessage(enabled) let links = self.linkPreviewModel.getLinks() self.linkPreviewModel.clearItems() - self.setUrls(links) + self.setLinkPreviewUrls(links) self.loadLinkPreviews(links) proc removeLinkPreviewData*(self: View, index: int) {.slot.} = self.linkPreviewModel.removePreviewData(index) + + proc urlsModelChanged(self: View) {.signal.} + proc getUrlsModel*(self: View): QVariant {.slot.} = + return self.urlsModelVariant + + proc setUrls*(self: View, urls: seq[string]) = + self.urlsModel.setUrls(urls) + + QtProperty[QVariant] urlsModel: + read = getUrlsModel + notify = urlsModelChanged diff --git a/storybook/pages/StatusChatInputPage.qml b/storybook/pages/StatusChatInputPage.qml index 46435bfc43..15f0b14896 100644 --- a/storybook/pages/StatusChatInputPage.qml +++ b/storybook/pages/StatusChatInputPage.qml @@ -112,6 +112,7 @@ SplitView { enabled: enabledCheckBox.checked linkPreviewModel: fakeLinksModel + urlsModel: fakeLinksModel askToEnableLinkPreview: askToEnableLinkPreviewSwitch.checked onAskToEnableLinkPreviewChanged: { if(askToEnableLinkPreview) { diff --git a/ui/app/AppLayouts/Chat/views/ChatColumnView.qml b/ui/app/AppLayouts/Chat/views/ChatColumnView.qml index 5ae3407d14..b880d25f41 100644 --- a/ui/app/AppLayouts/Chat/views/ChatColumnView.qml +++ b/ui/app/AppLayouts/Chat/views/ChatColumnView.qml @@ -255,6 +255,7 @@ Item { store: root.rootStore usersStore: d.activeUsersStore linkPreviewModel: d.activeChatContentModule.inputAreaModule.linkPreviewModel + urlsModel: d.activeChatContentModule.inputAreaModule.urlsModel askToEnableLinkPreview: { if(!d.activeChatContentModule || !d.activeChatContentModule.inputAreaModule || !d.activeChatContentModule.inputAreaModule.preservedProperties) return false diff --git a/ui/imports/shared/status/StatusChatInput.qml b/ui/imports/shared/status/StatusChatInput.qml index a8f3c50a04..880809f80e 100644 --- a/ui/imports/shared/status/StatusChatInput.qml +++ b/ui/imports/shared/status/StatusChatInput.qml @@ -65,6 +65,8 @@ Rectangle { property var linkPreviewModel: null + property var urlsModel: null + property bool askToEnableLinkPreview: false property var imageErrorMessageLocation: StatusChatInput.ImageErrorMessageLocation.Top // TODO: Remove this property? @@ -1366,7 +1368,7 @@ Rectangle { TextEditHyperlinksFormatter { id: hyperlinksFormatter textEdit: messageInputField - urlModel: control.linkPreviewModel + urlModel: control.urlsModel highlightUrl: linkPreviewArea.hoveredUrl enabled: messageInputField.enabled && messageInputField.textFormat == TextEdit.RichText }