From fcd956767702ccd6a82bf9bcd1d538755e7b6764 Mon Sep 17 00:00:00 2001 From: Alex Jbanca Date: Mon, 9 Oct 2023 11:45:16 +0300 Subject: [PATCH] feat: Add settings card to control link previews settings in chat input This commit adds the link preview settings card in the chat input area and connects the settings to the controller. Not included in this commit: Backend for the preserving the settings, syncing the settings and enforcing the settings on the backend side. Whenever an url is detected in the chat input area, the link preview settings card is presented. This card enables the user to choose one of the following options: 1. `Show for this message` - All the link previews in the current message will be loaded without asking again. The current message can be defined as the message currently typed/pasted in the chat input. Deleting or sending the current content is resetting this setting and the link preview settings card will be presented again when a new url is detected. 2. `Always show previews` - All the link previews will be loaded automatically. The link preview settings card will not be presented again (in the current state, this settings is enabled for the lifetime of the controller. This will change once the settings are preserved and synced) 3. `Never show previews` - No link preview will be loaded. Same as the `Always show previews` option, this will be preserved for the lifetime of the controller for now. 4. Dismiss (x button) - The link preview settings card will be dismissed. It will be loaded again when a new link preview is detected The same options can be loaded as a context menu on the link preview card. Changes: 1. Adding `LinkPreviewSettingsCard` 2. Adding the settings context menu to `LinkPreviewSettingsCard` and `LinkPreviewMiniCard` 3. Connect settings events to the nim controller 4. Adding the controller logic for settings change 5. Adding the link preview dismiss settings flag to the preserverd properties and use it as a condition to load the settings. 6. Adding/Updating corresponding storybook pages --- .../chat_content/input_area/controller.nim | 61 +++++++- .../chat_content/input_area/io_interface.nim | 14 +- .../chat_content/input_area/module.nim | 16 +- .../input_area/preserved_properties.nim | 1 + .../chat_content/input_area/view.nim | 43 +++++- .../shared_models/link_preview_model.nim | 11 +- .../pages/ChatInputLinksPreviewAreaPage.qml | 83 ++++++++-- .../pages/LinkPreviewSettingsCardPage.qml | 30 ++++ storybook/pages/StatusChatInputPage.qml | 95 +++++++++--- .../AppLayouts/Chat/views/ChatColumnView.qml | 23 ++- .../chat/ChatInputLinksPreviewArea.qml | 54 ++++++- .../controls/chat/LinkPreviewMiniCard.qml | 6 +- .../controls/chat/LinkPreviewSettingsCard.qml | 144 ++++++++++++++++++ ui/imports/shared/controls/chat/qmldir | 1 + ui/imports/shared/status/StatusChatInput.qml | 21 ++- .../status/StatusChatInputImageArea.qml | 2 +- ui/imports/utils/UndoStackManager.qml | 2 - 17 files changed, 540 insertions(+), 67 deletions(-) create mode 100644 storybook/pages/LinkPreviewSettingsCardPage.qml create mode 100644 ui/imports/shared/controls/chat/LinkPreviewSettingsCard.qml 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 fd0864482c..1505bbb480 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 @@ -10,6 +10,12 @@ import ../../../../../core/eventemitter import ../../../../../core/unique_event_emitter import ./link_preview_cache +type + LinkPreviewSetting* {.pure.} = enum + AlwaysAsk + Enabled + Disabled + type Controller* = ref object of RootObj delegate: io_interface.AccessInterface @@ -22,6 +28,8 @@ type gifService: gif_service.Service messageService: message_service.Service linkPreviewCache: LinkPreviewCache + linkPreviewPersistentSetting: LinkPreviewSetting + linkPreviewCurrentMessageSetting: LinkPreviewSetting proc newController*( delegate: io_interface.AccessInterface, @@ -45,6 +53,8 @@ proc newController*( result.gifService = gifService result.messageService = messageService result.linkPreviewCache = newLinkPreiewCache() + result.linkPreviewPersistentSetting = LinkPreviewSetting.AlwaysAsk + result.linkPreviewCurrentMessageSetting = LinkPreviewSetting.AlwaysAsk proc onUrlsUnfurled(self: Controller, args: LinkPreviewV2DataArgs) @@ -89,6 +99,16 @@ proc getChatId*(self: Controller): string = proc belongsToCommunity*(self: Controller): bool = return self.belongsToCommunity + +proc setLinkPreviewEnabledForThisMessage*(self: Controller, enabled: bool) = + self.linkPreviewCurrentMessageSetting = if enabled: LinkPreviewSetting.Enabled else: LinkPreviewSetting.Disabled + self.delegate.setAskToEnableLinkPreview(false) + +proc resetLinkPreviews(self: Controller) = + self.delegate.setUrls(@[]) + self.linkPreviewCache.clear() + self.linkPreviewCurrentMessageSetting = LinkPreviewSetting.AlwaysAsk + self.delegate.setAskToEnableLinkPreview(false) proc sendImages*(self: Controller, imagePathsAndDataJson: string, @@ -96,6 +116,7 @@ proc sendImages*(self: Controller, replyTo: string, preferredUsername: string = "", linkPreviews: seq[LinkPreview]): string = + self.resetLinkPreviews() self.chatService.sendImages( self.chatId, imagePathsAndDataJson, @@ -111,8 +132,8 @@ proc sendChatMessage*(self: Controller, contentType: int, preferredUsername: string = "", linkPreviews: seq[LinkPreview]) = - self.chatService.sendChatMessage( - self.chatId, + self.resetLinkPreviews() + self.chatService.sendChatMessage(self.chatId, msg, replyTo, contentType, @@ -165,11 +186,25 @@ proc addToRecentsGif*(self: Controller, item: GifDto) = proc isFavorite*(self: Controller, item: GifDto): bool = return self.gifService.isFavorite(item) +proc getLinkPreviewEnabled*(self: Controller): bool = + return self.linkPreviewPersistentSetting == LinkPreviewSetting.Enabled or self.linkPreviewCurrentMessageSetting == LinkPreviewSetting.Enabled + +proc canAskToEnableLinkPreview(self: Controller): bool = + return self.linkPreviewPersistentSetting == LinkPreviewSetting.AlwaysAsk and self.linkPreviewCurrentMessageSetting == LinkPreviewSetting.AlwaysAsk + proc setText*(self: Controller, text: string) = + if(text == ""): + self.resetLinkPreviews() + return + let urls = self.messageService.getTextUrls(text) self.delegate.setUrls(urls) - if len(urls) > 0: - let newUrls = self.linkPreviewCache.unknownUrls(urls) + let newUrls = self.linkPreviewCache.unknownUrls(urls) + + let askToEnableLinkPreview = len(newUrls) > 0 and self.canAskToEnableLinkPreview() + self.delegate.setAskToEnableLinkPreview(askToEnableLinkPreview) + + if self.getLinkPreviewEnabled() and len(urls) > 0: self.messageService.asyncUnfurlUrls(newUrls) proc linkPreviewsFromCache*(self: Controller, urls: seq[string]): Table[string, LinkPreview] = @@ -179,8 +214,22 @@ proc clearLinkPreviewCache*(self: Controller) = self.linkPreviewCache.clear() proc onUrlsUnfurled(self: Controller, args: LinkPreviewV2DataArgs) = + if not self.getLinkPreviewEnabled(): + return + let urls = self.linkPreviewCache.add(args.linkPreviews) self.delegate.updateLinkPreviewsFromCache(urls) -proc reloadLinkPreview*(self: Controller, url: string) = - self.messageService.asyncUnfurlUrls(@[url]) +proc loadLinkPreviews*(self: Controller, urls: seq[string]) = + if self.getLinkPreviewEnabled(): + self.messageService.asyncUnfurlUrls(urls) + +proc setLinkPreviewEnabled*(self: Controller, enabled: bool) = + if(enabled): + self.linkPreviewPersistentSetting = LinkPreviewSetting.Enabled + self.linkPreviewCurrentMessageSetting = LinkPreviewSetting.Enabled + else: + self.linkPreviewPersistentSetting = LinkPreviewSetting.Disabled + self.linkPreviewCurrentMessageSetting = LinkPreviewSetting.Disabled + + self.delegate.setAskToEnableLinkPreview(false) \ No newline at end of file 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 8cebf96b14..1408c9f2bf 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 @@ -111,5 +111,17 @@ method clearLinkPreviewCache*(self: AccessInterface) {.base.} = method linkPreviewsFromCache*(self: AccessInterface, urls: seq[string]): Table[string, LinkPreview] {.base.} = raise newException(ValueError, "No implementation available") -method reloadLinkPreview*(self: AccessInterface, url: string) {.base.} = +method loadLinkPreviews*(self: AccessInterface, urls: seq[string]) {.base.} = + raise newException(ValueError, "No implementation available") + +method getLinkPreviewEnabled*(self: AccessInterface): bool = + raise newException(ValueError, "No implementation available") + +method setLinkPreviewEnabled*(self: AccessInterface, enabled: bool) = + raise newException(ValueError, "No implementation available") + +method setAskToEnableLinkPreview*(self: AccessInterface, value: bool) = + raise newException(ValueError, "No implementation available") + +method setLinkPreviewEnabledForThisMessage*(self: AccessInterface, enabled: bool) = 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 f9dddc44c7..6672af604f 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 @@ -167,5 +167,17 @@ method setUrls*(self: Module, urls: seq[string]) = method linkPreviewsFromCache*(self: Module, urls: seq[string]): Table[string, LinkPreview] = return self.controller.linkPreviewsFromCache(urls) -method reloadLinkPreview*(self: Module, url: string) = - self.controller.reloadLinkPreview(url) \ No newline at end of file +method loadLinkPreviews*(self: Module, urls: seq[string]) = + self.controller.loadLinkPreviews(urls) + +method getLinkPreviewEnabled*(self: Module): bool = + return self.controller.getLinkPreviewEnabled() + +method setLinkPreviewEnabled*(self: Module, enabled: bool) = + self.controller.setLinkPreviewEnabled(enabled) + +method setAskToEnableLinkPreview*(self: Module, value: bool) = + self.view.setAskToEnableLinkPreview(value) + +method setLinkPreviewEnabledForThisMessage*(self: Module, value: bool) = + self.controller.setLinkPreviewEnabledForThisMessage(value) diff --git a/src/app/modules/main/chat_section/chat_content/input_area/preserved_properties.nim b/src/app/modules/main/chat_section/chat_content/input_area/preserved_properties.nim index 4f8b5a2ffa..ebaf8b4d5d 100644 --- a/src/app/modules/main/chat_section/chat_content/input_area/preserved_properties.nim +++ b/src/app/modules/main/chat_section/chat_content/input_area/preserved_properties.nim @@ -56,3 +56,4 @@ QtObject: read = getFileUrlsAndSourcesJson write = setFileUrlsAndSourcesJson notify = fileUrlsAndSourcesJsonChanged + 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 644cb893be..167efda3c0 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 @@ -17,6 +17,7 @@ QtObject: preservedPropertiesVariant: QVariant linkPreviewModel: link_preview_model.Model linkPreviewModelVariant: QVariant + askToEnableLinkPreview: bool proc delete*(self: View) = self.QObject.delete @@ -40,6 +41,7 @@ QtObject: result.preservedPropertiesVariant = newQVariant(result.preservedProperties) result.linkPreviewModel = newLinkPreviewModel() result.linkPreviewModelVariant = newQVariant(result.linkPreviewModel) + result.askToEnableLinkPreview = false proc load*(self: View) = self.delegate.viewDidLoad() @@ -197,6 +199,17 @@ QtObject: QtProperty[QVariant] linkPreviewModel: read = getLinkPreviewModel + proc askToEnableLinkPreviewChanged(self: View) {.signal.} + proc getAskToEnableLinkPreview(self: View): bool {.slot.} = + return self.askToEnableLinkPreview + proc setAskToEnableLinkPreview*(self: View, value: bool) {.slot.} = + self.askToEnableLinkPreview = value + self.askToEnableLinkPreviewChanged() + + QtProperty[bool] askToEnableLinkPreview: + read = getAskToEnableLinkPreview + notify = askToEnableLinkPreviewChanged + # Currently used to fetch link previews, but could be used elsewhere proc setText*(self: View, text: string) {.slot.} = self.delegate.setText(text) @@ -207,10 +220,36 @@ QtObject: proc setUrls*(self: View, urls: seq[string]) = self.linkPreviewModel.setUrls(urls) - self.updateLinkPreviewsFromCache(urls) + if(self.delegate.getLinkPreviewEnabled()): + self.updateLinkPreviewsFromCache(urls) + else: + self.linkPreviewModel.removeAllPreviewData() proc clearLinkPreviewCache*(self: View) {.slot.} = self.delegate.clearLinkPreviewCache() proc reloadLinkPreview(self: View, link: string) {.slot.} = - self.delegate.reloadLinkPreview(link) + self.delegate.loadLinkPreviews(@[link]) + + proc loadLinkPreviews(self: View, links: seq[string]) = + self.delegate.loadLinkPreviews(links) + + proc enableLinkPreview(self: View) {.slot.} = + self.delegate.setLinkPreviewEnabled(true) + let links = self.linkPreviewModel.getLinks() + self.linkPreviewModel.clearItems() + self.loadLinkPreviews(links) + + proc disableLinkPreview(self: View) {.slot.} = + self.delegate.setLinkPreviewEnabled(false) + self.linkPreviewModel.removeAllPreviewData() + + proc setLinkPreviewEnabledForCurrentMessage(self: View, enabled: bool) {.slot.} = + self.delegate.setLinkPreviewEnabledForThisMessage(enabled) + let links = self.linkPreviewModel.getLinks() + self.linkPreviewModel.clearItems() + self.setUrls(links) + self.loadLinkPreviews(links) + + proc removeLinkPreviewData*(self: View, index: int) {.slot.} = + self.linkPreviewModel.removePreviewData(index) \ No newline at end of file diff --git a/src/app/modules/shared_models/link_preview_model.nim b/src/app/modules/shared_models/link_preview_model.nim index bef27335fd..691b0ddf56 100644 --- a/src/app/modules/shared_models/link_preview_model.nim +++ b/src/app/modules/shared_models/link_preview_model.nim @@ -213,8 +213,17 @@ QtObject: defer: modelIndex.delete self.dataChanged(modelIndex, modelIndex) + proc removeAllPreviewData*(self: Model) {.slot.} = + for i in 0 ..< self.items.len: + self.removePreviewData(i) + proc getUnfuledLinkPreviews*(self: Model): seq[LinkPreview] = result = @[] for item in self.items: if item.unfurled and item.linkPreview.hostName != "": - result.add(item.linkPreview) \ No newline at end of file + result.add(item.linkPreview) + + proc getLinks*(self: Model): seq[string] = + result = @[] + for item in self.items: + result.add(item.linkPreview.url) \ No newline at end of file diff --git a/storybook/pages/ChatInputLinksPreviewAreaPage.qml b/storybook/pages/ChatInputLinksPreviewAreaPage.qml index 5a7d750d05..cb5074f812 100644 --- a/storybook/pages/ChatInputLinksPreviewAreaPage.qml +++ b/storybook/pages/ChatInputLinksPreviewAreaPage.qml @@ -1,24 +1,81 @@ import QtQuick 2.15 import QtQuick.Controls 2.15 +import QtQuick.Layouts 1.15 import QtGraphicalEffects 1.15 +import Storybook 1.0 + import StatusQ.Core.Theme 0.1 import shared.controls.chat 1.0 -Page { - Rectangle { - id: wrapper - anchors.fill: parent - color: Theme.palette.statusChatInput.secondaryBackgroundColor +SplitView { - ChatInputLinksPreviewArea { - id: chatInputLinkPreviewsArea - anchors.centerIn: parent - width: parent.width - imagePreviewModel: ["https://picsum.photos/200/300?random=1", "https://picsum.photos/200/300?random=1"] - linkPreviewModel: linkPreviewListModel - onLinkRemoved: linkPreviewListModel.remove(index) + Logs { id: logs } + orientation: Qt.Vertical + + SplitView { + + SplitView.fillWidth: true + SplitView.fillHeight: true + + Pane { + SplitView.fillWidth: true + Rectangle { + id: wrapper + anchors.fill: parent + color: Theme.palette.statusChatInput.secondaryBackgroundColor + + ChatInputLinksPreviewArea { + id: chatInputLinkPreviewsArea + anchors.centerIn: parent + width: parent.width + imagePreviewArray: ["https://picsum.photos/200/300?random=1", "https://picsum.photos/200/300?random=1"] + linkPreviewModel: showLinkPreviewSettings ? emptyModel : linkPreviewListModel + showLinkPreviewSettings: !linkPreviewEnabledSwitch.checked + visible: hasContent + + onImageRemoved: (index) => logs.logEvent("ChatInputLinksPreviewArea::onImageRemoved: " + index) + onImageClicked: (chatImage) => logs.logEvent("ChatInputLinksPreviewArea::onImageClicked: " + chatImage) + onLinkReload: (link) => logs.logEvent("ChatInputLinksPreviewArea::onLinkReload: " + link) + onLinkClicked: (link) => logs.logEvent("ChatInputLinksPreviewArea::onLinkClicked: " + link) + + onEnableLinkPreview: () => { + linkPreviewEnabledSwitch.checked = true + logs.logEvent("ChatInputLinksPreviewArea::onEnableLinkPreview") + } + onEnableLinkPreviewForThisMessage: () => logs.logEvent("ChatInputLinksPreviewArea::onEnableLinkPreviewForThisMessage") + onDisableLinkPreview: () => logs.logEvent("ChatInputLinksPreviewArea::onDisableLinkPreview") + onDismissLinkPreviewSettings: () => logs.logEvent("ChatInputLinksPreviewArea::onDismissLinkPreviewSettings") + onDismissLinkPreview: (index) => logs.logEvent("ChatInputLinksPreviewArea::onDismissLinkPreview: " + index) + } + } } + + Pane { + SplitView.preferredWidth: 300 + SplitView.fillHeight: true + ColumnLayout { + Label { + text: "Links preview enabled" + } + Switch { + id: linkPreviewEnabledSwitch + } + } + } + } + + LogsAndControlsPanel { + id: logsAndControlsPanel + + SplitView.minimumHeight: 100 + SplitView.preferredHeight: 200 + + logsView.logText: logs.logText + } + + ListModel { + id: emptyModel } ListModel { @@ -143,4 +200,4 @@ Page { // category: Panels -// https://www.figma.com/file/Mr3rqxxgKJ2zMQ06UAKiWL/💬-Chat⎜Desktop?type=design&node-id=22341-184809&mode=design&t=VWBVK4DOUxr1BmTp-0 \ No newline at end of file +// https://www.figma.com/file/Mr3rqxxgKJ2zMQ06UAKiWL/💬-Chat⎜Desktop?type=design&node-id=22341-184809&mode=design&t=VWBVK4DOUxr1BmTp-0 diff --git a/storybook/pages/LinkPreviewSettingsCardPage.qml b/storybook/pages/LinkPreviewSettingsCardPage.qml new file mode 100644 index 0000000000..cca13274b3 --- /dev/null +++ b/storybook/pages/LinkPreviewSettingsCardPage.qml @@ -0,0 +1,30 @@ +import QtQuick 2.15 +import QtQuick.Controls 2.15 + +import StatusQ.Core.Theme 0.1 + +import shared.controls.chat 1.0 + +Pane { + id: root + + layer.enabled: true + layer.samples: 4 + background: Rectangle { + color: Theme.palette.statusChatInput.secondaryBackgroundColor + } + + LinkPreviewSettingsCard { + id: previewMiniCard + anchors.centerIn: parent + onDismiss: ToolTip.show(qsTr("Link previews disabled for this message"), 1000) + onEnableLinkPreviewForThisMessage: ToolTip.show(qsTr("Link previews enabled for this message"), 1000) + onEnableLinkPreview: ToolTip.show(qsTr("Link previews enabled"), 1000) + onDisableLinkPreview: ToolTip.show(qsTr("Link previews disabled"), 1000) + } +} + + +//category: Controls + +//https://www.figma.com/file/Mr3rqxxgKJ2zMQ06UAKiWL/💬-Chat⎜Desktop?type=design&node-id=22341-184809&mode=design&t=91pnQgUZAqFJLcqM-0 diff --git a/storybook/pages/StatusChatInputPage.qml b/storybook/pages/StatusChatInputPage.qml index 25aa99f692..891cf43def 100644 --- a/storybook/pages/StatusChatInputPage.qml +++ b/storybook/pages/StatusChatInputPage.qml @@ -94,31 +94,11 @@ SplitView { sourceComponent: StatusChatInput { id: chatInput property var globalUtils: globalUtilsMock.globalUtils - property string unformattedText: textInput.getText(0, textInput.length) + property string unformattedText: chatInput.textInput.getText(0, chatInput.textInput.length) onUnformattedTextChanged: { textEditConnection.enabled = false - - var words = unformattedText.split(" ") - - fakeLinksModel.clear() - words.forEach(function(word){ - if(Utils.isURL(word)) { - fakeLinksModel.append({ - url: encodeURI(word), - unfurled: Math.floor(Math.random() * 2), - immutable: false, - hostname: Math.floor(Math.random() * 2) ? "youtube.com" : "", - title: "PSY - GANGNAM STYLE(강남스타일) M/V", - description: "This is the description of the link", - linkType: Math.floor(Math.random() * 3), - thumbnailWidth: 480, - thumbnailHeight: 360, - thumbnailUrl: "https://picsum.photos/480/360?random=1", - thumbnailDataUri: "" - }) - } - }) + d.loadLinkPreviews(unformattedText) textEditConnection.enabled = true } @@ -133,6 +113,13 @@ SplitView { enabled: enabledCheckBox.checked linkPreviewModel: fakeLinksModel + askToEnableLinkPreview: askToEnableLinkPreviewSwitch.checked + onAskToEnableLinkPreviewChanged: { + if(askToEnableLinkPreview) { + fakeLinksModel.clear() + d.loadLinkPreviews(unformattedText) + } + } usersStore: QtObject { readonly property var usersModel: fakeUsersModel } @@ -141,6 +128,26 @@ SplitView { logs.logEvent("StatusChatInput::sendMessage", ["PlainText"], [globalUtilsMock.globalUtils.plainText(chatInput.getTextWithPublicKeys())]) logs.logEvent("StatusChatInput::sendMessage", ["RawText"], [chatInput.textInput.text]) } + onEnableLinkPreviewForThisMessage: { + linkPreviewSwitch.checked = true + askToEnableLinkPreviewSwitch.checked = false + } + onEnableLinkPreview: { + linkPreviewSwitch.checked = true + askToEnableLinkPreviewSwitch.checked = false + } + onDisableLinkPreview: { + linkPreviewSwitch.checked = false + askToEnableLinkPreviewSwitch.checked = false + } + onDismissLinkPreviewSettings: { + askToEnableLinkPreviewSwitch.checked = false + linkPreviewSwitch.checked = false + } + onDismissLinkPreview: (index) => { + fakeLinksModel.setProperty(index, "unfurled", false) + fakeLinksModel.setProperty(index, "immutable", true) + } } } @@ -152,6 +159,36 @@ SplitView { logsView.logText: logs.logText } + + QtObject { + id: d + property bool linkPreviewsEnabled: linkPreviewSwitch.checked && !askToEnableLinkPreviewSwitch.checked + onLinkPreviewsEnabledChanged: { + loadLinkPreviews(chatInputLoader.item ? chatInputLoader.item.unformattedText : "") + } + function loadLinkPreviews(text) { + var words = text.split(" ") + + fakeLinksModel.clear() + words.forEach(function(word){ + if(Utils.isURL(word)) { + fakeLinksModel.append({ + url: encodeURI(word), + unfurled: d.linkPreviewsEnabled, + immutable: !d.linkPreviewsEnabled, + hostname: Math.floor(Math.random() * 2) ? "youtube.com" : "", + title: "PSY - GANGNAM STYLE(강남스타일) M/V", + description: "This is the description of the link", + linkType: Math.floor(Math.random() * 3), + thumbnailWidth: 480, + thumbnailHeight: 360, + thumbnailUrl: "https://picsum.photos/480/360?random=1", + thumbnailDataUri: "" + }) + } + }) + } + } } Pane { @@ -207,6 +244,18 @@ SplitView { text: "Links" Layout.fillWidth: true } + + Switch { + id: linkPreviewSwitch + text: "Link Preview enabled" + } + + Switch { + id: askToEnableLinkPreviewSwitch + text: "Ask to enable Link Preview" + checked: true + } + ComboBox { id: linksNb editable: true @@ -245,4 +294,4 @@ SplitView { // category: Components -// https://www.figma.com/file/Mr3rqxxgKJ2zMQ06UAKiWL/💬-Chat⎜Desktop?type=design&node-id=23155-66084&mode=design&t=VWBVK4DOUxr1BmTp-0 \ No newline at end of file +// https://www.figma.com/file/Mr3rqxxgKJ2zMQ06UAKiWL/💬-Chat⎜Desktop?type=design&node-id=23155-66084&mode=design&t=VWBVK4DOUxr1BmTp-0 diff --git a/ui/app/AppLayouts/Chat/views/ChatColumnView.qml b/ui/app/AppLayouts/Chat/views/ChatColumnView.qml index 1f29548f73..17441cd519 100644 --- a/ui/app/AppLayouts/Chat/views/ChatColumnView.qml +++ b/ui/app/AppLayouts/Chat/views/ChatColumnView.qml @@ -130,17 +130,17 @@ Item { chatInput.validateImagesAndShowImageArea(filesList) } - function restoreInputState() { + function restoreInputState(textInput) { if (!d.activeChatContentModule) { - chatInput.setText("") + chatInput.clear() chatInput.resetReplyArea() chatInput.resetImageArea() return } // Restore message text - chatInput.setText(d.activeChatContentModule.inputAreaModule.preservedProperties.text) + chatInput.setText(textInput) d.restoreInputReply() d.restoreInputAttachments() @@ -154,9 +154,14 @@ Item { } onActiveChatContentModuleChanged: { + let preservedText = "" + if (d.activeChatContentModule) { + preservedText = d.activeChatContentModule.inputAreaModule.preservedProperties.text + } + d.activeChatContentModule.inputAreaModule.clearLinkPreviewCache() // Call later to make sure activeUsersStore and activeMessagesStore bindings are updated - Qt.callLater(d.restoreInputState) + Qt.callLater(d.restoreInputState, preservedText) } } @@ -243,7 +248,12 @@ Item { store: root.rootStore usersStore: d.activeUsersStore linkPreviewModel: d.activeChatContentModule.inputAreaModule.linkPreviewModel + askToEnableLinkPreview: { + if(!d.activeChatContentModule || !d.activeChatContentModule.inputAreaModule || !d.activeChatContentModule.inputAreaModule.preservedProperties) + return false + return d.activeChatContentModule.inputAreaModule.askToEnableLinkPreview + } textInput.placeholderText: { if (!channelPostRestrictions.visible) { if (d.activeChatContentModule.chatDetails.blocked) @@ -315,6 +325,11 @@ Item { } onLinkPreviewReloaded: (link) => d.activeChatContentModule.inputAreaModule.reloadLinkPreview(link) + onEnableLinkPreview: () => d.activeChatContentModule.inputAreaModule.enableLinkPreview() + onDisableLinkPreview: () => d.activeChatContentModule.inputAreaModule.disableLinkPreview() + onEnableLinkPreviewForThisMessage: () => d.activeChatContentModule.inputAreaModule.setLinkPreviewEnabledForCurrentMessage(true) + onDismissLinkPreviewSettings: () => d.activeChatContentModule.inputAreaModule.setLinkPreviewEnabledForCurrentMessage(false) + onDismissLinkPreview: (index) => d.activeChatContentModule.inputAreaModule.removeLinkPreviewData(index) } ChatPermissionQualificationPanel { diff --git a/ui/imports/shared/controls/chat/ChatInputLinksPreviewArea.qml b/ui/imports/shared/controls/chat/ChatInputLinksPreviewArea.qml index cedb724386..2b1c0fcdd0 100644 --- a/ui/imports/shared/controls/chat/ChatInputLinksPreviewArea.qml +++ b/ui/imports/shared/controls/chat/ChatInputLinksPreviewArea.qml @@ -8,22 +8,44 @@ import StatusQ.Core 0.1 import shared.status 1.0 import shared.controls.chat 1.0 +import utils 1.0 + import SortFilterProxyModel 0.2 Control { id: root - required property var imagePreviewModel + required property var imagePreviewArray + /* + Expected roles: + string title + string url + bool unfurled + bool immutable + string hostname + string description + int linkType + int thumbnailWidth + int thumbnailHeight + string thumbnailUrl + string thumbnailDataUri + */ required property var linkPreviewModel + required property bool showLinkPreviewSettings readonly property alias hoveredUrl: d.hoveredUrl - readonly property int contentItemsCount: imagePreviewModel.length + d.filteredModel.count + readonly property bool hasContent: imagePreviewArray.length > 0 || showLinkPreviewSettings || linkPreviewRepeater.count > 0 signal imageRemoved(int index) signal imageClicked(var chatImage) signal linkReload(string link) signal linkClicked(string link) - signal linkRemoved(string link) + + signal enableLinkPreview() + signal enableLinkPreviewForThisMessage() + signal disableLinkPreview() + signal dismissLinkPreviewSettings() + signal dismissLinkPreview(int index) horizontalPadding: 12 topPadding: 12 @@ -60,6 +82,8 @@ Control { contentWidth: layout.width contentHeight: layout.height + onFlickStarted: settingsContextMenu.close() + RowLayout { id: layout spacing: 8 @@ -67,12 +91,13 @@ Control { id: imageArea Layout.preferredHeight: 64 spacing: layout.spacing - imageSource: imagePreviewModel + imageSource: imagePreviewArray onImageClicked: root.imageClicked(chatImage) onImageRemoved: root.imageRemoved(index) - visible: !!imagePreviewModel && imagePreviewModel.length > 0 + visible: !!imagePreviewArray && imagePreviewArray.length > 0 } Repeater { + id: linkPreviewRepeater model: d.filteredModel delegate: LinkPreviewMiniCard { // Model properties @@ -104,9 +129,10 @@ Control { unfurled && hostname === "" ? LinkPreviewMiniCard.State.LoadingFailed : !unfurled ? LinkPreviewMiniCard.State.Loading : LinkPreviewMiniCard.State.Invalid - onClose: root.linkPreviewModel.removePreviewData(d.filteredModel.mapToSource(index)) + onClose: root.dismissLinkPreview(d.filteredModel.mapToSource(index)) onRetry: root.linkReload(url) onClicked: root.linkClicked(url) + onRightClicked: settingsContextMenu.popup() onContainsMouseChanged: { if (containsMouse) { d.hoveredUrl = url @@ -121,6 +147,14 @@ Control { } } } + LinkPreviewSettingsCard { + id: settingsCard + visible: root.showLinkPreviewSettings + onDismiss: root.dismissLinkPreviewSettings() + onEnableLinkPreviewForThisMessage: root.enableLinkPreviewForThisMessage() + onEnableLinkPreview: root.enableLinkPreview() + onDisableLinkPreview: root.disableLinkPreview() + } } } } @@ -157,4 +191,12 @@ Control { ] } } + + LinkPreviewSettingsCard.ContextMenu { + id: settingsContextMenu + + onEnableLinkPreviewForThisMessage: root.enableLinkPreviewForThisMessage() + onEnableLinkPreview: root.enableLinkPreview() + onDisableLinkPreview: root.disableLinkPreview() + } } diff --git a/ui/imports/shared/controls/chat/LinkPreviewMiniCard.qml b/ui/imports/shared/controls/chat/LinkPreviewMiniCard.qml index 37d8329020..a956d467dc 100644 --- a/ui/imports/shared/controls/chat/LinkPreviewMiniCard.qml +++ b/ui/imports/shared/controls/chat/LinkPreviewMiniCard.qml @@ -42,6 +42,7 @@ CalloutCard { signal close() signal retry() signal clicked(var eventPoint) + signal rightClicked(var eventPoint) implicitWidth: 260 implicitHeight: 64 @@ -181,7 +182,6 @@ CalloutCard { icon: "tiny/chevron-right" color: Theme.palette.baseColor1 visible: secondTitleText.visible - } StatusBaseText { id: secondTitleText @@ -246,4 +246,8 @@ CalloutCard { target: background onTapped: root.clicked(eventPoint) } + TapHandler { + acceptedButtons: Qt.RightButton + onTapped: root.rightClicked(eventPoint) + } } diff --git a/ui/imports/shared/controls/chat/LinkPreviewSettingsCard.qml b/ui/imports/shared/controls/chat/LinkPreviewSettingsCard.qml new file mode 100644 index 0000000000..b6599d4e25 --- /dev/null +++ b/ui/imports/shared/controls/chat/LinkPreviewSettingsCard.qml @@ -0,0 +1,144 @@ +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import QtQuick.Layouts 1.15 + +import StatusQ.Controls 0.1 +import StatusQ.Core 0.1 +import StatusQ.Core.Theme 0.1 +import StatusQ.Popups 0.1 + +import utils 1.0 + +CalloutCard { + id: root + + signal dismiss() + signal enableLinkPreviewForThisMessage() + signal enableLinkPreview() + signal disableLinkPreview() + + implicitHeight: 64 + borderWidth: 0 + topPadding: 13 + bottomPadding: 13 + horizontalPadding: Style.current.padding + + contentItem: RowLayout { + spacing: Style.current.halfPadding + ColumnLayout { + spacing: 0 + Layout.fillHeight: true + Layout.fillWidth: true + StatusBaseText { + Layout.alignment: Qt.AlignTop + Layout.fillWidth: true + Layout.fillHeight: true + font.pixelSize: Style.current.additionalTextSize + font.weight: Font.Medium + wrapMode: Text.Wrap + elide: Text.ElideRight + maximumLineCount: 1 + text: qsTr("Show link previews?") + } + StatusBaseText { + Layout.fillWidth: true + Layout.fillHeight: true + font.pixelSize: Style.current.additionalTextSize + color: Theme.palette.baseColor1 + wrapMode: Text.Wrap + elide: Text.ElideRight + maximumLineCount: 1 + text: qsTr("A preview of your link will be shown here before you send it") + } + } + ComboBox { + id: optionsComboBox + Layout.leftMargin: 12 + Layout.preferredHeight: 38 + leftPadding: 12 + rightPadding: 12 + hoverEnabled: true + flat: true + contentItem: RowLayout { + spacing: Style.current.halfPadding + StatusBaseText { + Layout.fillWidth: true + Layout.fillHeight: true + verticalAlignment: Text.AlignVCenter + font.pixelSize: Style.current.additionalTextSize + elide: Text.ElideRight + text: qsTr("Options") + color: Theme.palette.baseColor1 + } + StatusIcon { + Layout.preferredWidth: 16 + Layout.preferredHeight: 16 + icon: "chevron-down" + color: Theme.palette.baseColor1 + } + } + background: Rectangle { + border.width: 1 + border.color: Theme.palette.directColor7 + color: optionsComboBox.popup.visible ? Theme.palette.baseColor2 : "transparent" + radius: Style.current.radius + HoverHandler { + cursorShape: Qt.PointingHandCursor + enabled: optionsComboBox.enabled + } + } + popup: ContextMenu { + y: - (height + 4) + onEnableLinkPreviewForThisMessage: root.enableLinkPreviewForThisMessage() + onEnableLinkPreview: root.enableLinkPreview() + onDisableLinkPreview: root.disableLinkPreview() + } + indicator: null + } + + StatusFlatRoundButton { + id: closeButton + Layout.preferredHeight: 38 + Layout.preferredWidth: 38 + type: StatusFlatRoundButton.Type.Secondary + icon.name: "close" + icon.color: Theme.palette.directColor1 + onClicked: root.dismiss() + } + } + + component ContextMenu: StatusMenu { + id: contextMenu + + signal enableLinkPreviewForThisMessage() + signal enableLinkPreview() + signal disableLinkPreview() + + hideDisabledItems: false + StatusAction { + text: qsTr("Link previews") + enabled: false + } + + StatusAction { + text: qsTr("Show for this message") + icon.name: "show" + onTriggered: contextMenu.enableLinkPreviewForThisMessage() + } + + StatusAction { + text: qsTr("Always show previews") + icon.name: "show" + onTriggered: contextMenu.enableLinkPreview() + } + + StatusMenuSeparator { } + + StatusAction { + text: qsTr("Never show previews") + icon.name: "hide" + type: StatusAction.Type.Danger + onTriggered: contextMenu.disableLinkPreview() + } + } +} diff --git a/ui/imports/shared/controls/chat/qmldir b/ui/imports/shared/controls/chat/qmldir index 1440fa3e11..a65337c403 100644 --- a/ui/imports/shared/controls/chat/qmldir +++ b/ui/imports/shared/controls/chat/qmldir @@ -5,6 +5,7 @@ FetchMoreMessagesButton 1.0 FetchMoreMessagesButton.qml GapComponent 1.0 GapComponent.qml LinkPreviewCard 1.0 LinkPreviewCard.qml LinkPreviewMiniCard 1.0 LinkPreviewMiniCard.qml +LinkPreviewSettingsCard 1.0 LinkPreviewSettingsCard.qml UsernameLabel 1.0 UsernameLabel.qml UserProfileCard 1.0 UserProfileCard.qml DateGroup 1.0 DateGroup.qml diff --git a/ui/imports/shared/status/StatusChatInput.qml b/ui/imports/shared/status/StatusChatInput.qml index 877b4a0a4f..7f13edb54f 100644 --- a/ui/imports/shared/status/StatusChatInput.qml +++ b/ui/imports/shared/status/StatusChatInput.qml @@ -28,9 +28,13 @@ Rectangle { signal stickerSelected(string hashId, string packId, string url) signal sendMessage(var event) signal keyUpPress() - signal linkPreviewRemoved(string link) signal linkPreviewReloaded(string link) - + signal enableLinkPreview() + signal enableLinkPreviewForThisMessage() + signal disableLinkPreview() + signal dismissLinkPreviewSettings() + signal dismissLinkPreview(int index) + property var usersStore property var store @@ -61,6 +65,8 @@ Rectangle { property var linkPreviewModel: null + property bool askToEnableLinkPreview: false + property var imageErrorMessageLocation: StatusChatInput.ImageErrorMessageLocation.Top // TODO: Remove this property? property alias suggestions: suggestionsBox @@ -1179,11 +1185,12 @@ Rectangle { ChatInputLinksPreviewArea { id: linkPreviewArea Layout.fillWidth: true - visible: contentItemsCount > 0 + visible: hasContent horizontalPadding: 12 topPadding: 12 - imagePreviewModel: control.fileUrlsAndSources + imagePreviewArray: control.fileUrlsAndSources linkPreviewModel: control.linkPreviewModel + showLinkPreviewSettings: control.askToEnableLinkPreview onImageRemoved: (index) => { //Just do a copy and replace the whole thing because it's a plain JS array and thre's no signal when a single item is removed let urls = control.fileUrlsAndSources @@ -1194,8 +1201,12 @@ Rectangle { } onImageClicked: (chatImage) => Global.openImagePopup(chatImage) onLinkReload: (link) => control.linkPreviewReloaded(link) - onLinkRemoved: (link) => control.linkPreviewRemoved(link) onLinkClicked: (link) => Global.openLink(link) + onEnableLinkPreview: () => control.enableLinkPreview() + onEnableLinkPreviewForThisMessage: () => control.enableLinkPreviewForThisMessage() + onDisableLinkPreview: () => control.disableLinkPreview() + onDismissLinkPreviewSettings: () => control.dismissLinkPreviewSettings() + onDismissLinkPreview: (index) => control.dismissLinkPreview(index) } RowLayout { diff --git a/ui/imports/shared/status/StatusChatInputImageArea.qml b/ui/imports/shared/status/StatusChatInputImageArea.qml index 1b3f065ad1..8a9bb6f057 100644 --- a/ui/imports/shared/status/StatusChatInputImageArea.qml +++ b/ui/imports/shared/status/StatusChatInputImageArea.qml @@ -20,7 +20,7 @@ Row { Repeater { id: rptImages - + Item { height: chatImage.height width: chatImage.width diff --git a/ui/imports/utils/UndoStackManager.qml b/ui/imports/utils/UndoStackManager.qml index 68ccd7bbd1..fbe566f19d 100644 --- a/ui/imports/utils/UndoStackManager.qml +++ b/ui/imports/utils/UndoStackManager.qml @@ -117,9 +117,7 @@ Item { } const newStackSize = Math.ceil(root.maxStackSize / 2) - print("Reducing undo stack to " + newStackSize + " items") for(var i = 1; i <= newStackSize; i++) { - print("Removing " + Math.ceil(root.maxStackSize / newStackSize) + " items from index " + i) d.undoStack.splice(i, Math.ceil(root.maxStackSize / newStackSize)) } }