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
This commit is contained in:
Alex Jbanca 2023-10-09 11:45:16 +03:00 committed by Alex Jbanca
parent dd8c3173f6
commit fcd9567677
17 changed files with 540 additions and 67 deletions

View File

@ -10,6 +10,12 @@ import ../../../../../core/eventemitter
import ../../../../../core/unique_event_emitter import ../../../../../core/unique_event_emitter
import ./link_preview_cache import ./link_preview_cache
type
LinkPreviewSetting* {.pure.} = enum
AlwaysAsk
Enabled
Disabled
type type
Controller* = ref object of RootObj Controller* = ref object of RootObj
delegate: io_interface.AccessInterface delegate: io_interface.AccessInterface
@ -22,6 +28,8 @@ type
gifService: gif_service.Service gifService: gif_service.Service
messageService: message_service.Service messageService: message_service.Service
linkPreviewCache: LinkPreviewCache linkPreviewCache: LinkPreviewCache
linkPreviewPersistentSetting: LinkPreviewSetting
linkPreviewCurrentMessageSetting: LinkPreviewSetting
proc newController*( proc newController*(
delegate: io_interface.AccessInterface, delegate: io_interface.AccessInterface,
@ -45,6 +53,8 @@ proc newController*(
result.gifService = gifService result.gifService = gifService
result.messageService = messageService result.messageService = messageService
result.linkPreviewCache = newLinkPreiewCache() result.linkPreviewCache = newLinkPreiewCache()
result.linkPreviewPersistentSetting = LinkPreviewSetting.AlwaysAsk
result.linkPreviewCurrentMessageSetting = LinkPreviewSetting.AlwaysAsk
proc onUrlsUnfurled(self: Controller, args: LinkPreviewV2DataArgs) proc onUrlsUnfurled(self: Controller, args: LinkPreviewV2DataArgs)
@ -89,6 +99,16 @@ proc getChatId*(self: Controller): string =
proc belongsToCommunity*(self: Controller): bool = proc belongsToCommunity*(self: Controller): bool =
return self.belongsToCommunity 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, proc sendImages*(self: Controller,
imagePathsAndDataJson: string, imagePathsAndDataJson: string,
@ -96,6 +116,7 @@ proc sendImages*(self: Controller,
replyTo: string, replyTo: string,
preferredUsername: string = "", preferredUsername: string = "",
linkPreviews: seq[LinkPreview]): string = linkPreviews: seq[LinkPreview]): string =
self.resetLinkPreviews()
self.chatService.sendImages( self.chatService.sendImages(
self.chatId, self.chatId,
imagePathsAndDataJson, imagePathsAndDataJson,
@ -111,8 +132,8 @@ proc sendChatMessage*(self: Controller,
contentType: int, contentType: int,
preferredUsername: string = "", preferredUsername: string = "",
linkPreviews: seq[LinkPreview]) = linkPreviews: seq[LinkPreview]) =
self.chatService.sendChatMessage( self.resetLinkPreviews()
self.chatId, self.chatService.sendChatMessage(self.chatId,
msg, msg,
replyTo, replyTo,
contentType, contentType,
@ -165,11 +186,25 @@ proc addToRecentsGif*(self: Controller, item: GifDto) =
proc isFavorite*(self: Controller, item: GifDto): bool = proc isFavorite*(self: Controller, item: GifDto): bool =
return self.gifService.isFavorite(item) 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) = proc setText*(self: Controller, text: string) =
if(text == ""):
self.resetLinkPreviews()
return
let urls = self.messageService.getTextUrls(text) let urls = self.messageService.getTextUrls(text)
self.delegate.setUrls(urls) 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) self.messageService.asyncUnfurlUrls(newUrls)
proc linkPreviewsFromCache*(self: Controller, urls: seq[string]): Table[string, LinkPreview] = proc linkPreviewsFromCache*(self: Controller, urls: seq[string]): Table[string, LinkPreview] =
@ -179,8 +214,22 @@ proc clearLinkPreviewCache*(self: Controller) =
self.linkPreviewCache.clear() self.linkPreviewCache.clear()
proc onUrlsUnfurled(self: Controller, args: LinkPreviewV2DataArgs) = proc onUrlsUnfurled(self: Controller, args: LinkPreviewV2DataArgs) =
if not self.getLinkPreviewEnabled():
return
let urls = self.linkPreviewCache.add(args.linkPreviews) let urls = self.linkPreviewCache.add(args.linkPreviews)
self.delegate.updateLinkPreviewsFromCache(urls) self.delegate.updateLinkPreviewsFromCache(urls)
proc reloadLinkPreview*(self: Controller, url: string) = proc loadLinkPreviews*(self: Controller, urls: seq[string]) =
self.messageService.asyncUnfurlUrls(@[url]) 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)

View File

@ -111,5 +111,17 @@ method clearLinkPreviewCache*(self: AccessInterface) {.base.} =
method linkPreviewsFromCache*(self: AccessInterface, urls: seq[string]): Table[string, LinkPreview] {.base.} = method linkPreviewsFromCache*(self: AccessInterface, urls: seq[string]): Table[string, LinkPreview] {.base.} =
raise newException(ValueError, "No implementation available") 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") raise newException(ValueError, "No implementation available")

View File

@ -167,5 +167,17 @@ method setUrls*(self: Module, urls: seq[string]) =
method linkPreviewsFromCache*(self: Module, urls: seq[string]): Table[string, LinkPreview] = method linkPreviewsFromCache*(self: Module, urls: seq[string]): Table[string, LinkPreview] =
return self.controller.linkPreviewsFromCache(urls) return self.controller.linkPreviewsFromCache(urls)
method reloadLinkPreview*(self: Module, url: string) = method loadLinkPreviews*(self: Module, urls: seq[string]) =
self.controller.reloadLinkPreview(url) 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)

View File

@ -56,3 +56,4 @@ QtObject:
read = getFileUrlsAndSourcesJson read = getFileUrlsAndSourcesJson
write = setFileUrlsAndSourcesJson write = setFileUrlsAndSourcesJson
notify = fileUrlsAndSourcesJsonChanged notify = fileUrlsAndSourcesJsonChanged

View File

@ -17,6 +17,7 @@ QtObject:
preservedPropertiesVariant: QVariant preservedPropertiesVariant: QVariant
linkPreviewModel: link_preview_model.Model linkPreviewModel: link_preview_model.Model
linkPreviewModelVariant: QVariant linkPreviewModelVariant: QVariant
askToEnableLinkPreview: bool
proc delete*(self: View) = proc delete*(self: View) =
self.QObject.delete self.QObject.delete
@ -40,6 +41,7 @@ QtObject:
result.preservedPropertiesVariant = newQVariant(result.preservedProperties) result.preservedPropertiesVariant = newQVariant(result.preservedProperties)
result.linkPreviewModel = newLinkPreviewModel() result.linkPreviewModel = newLinkPreviewModel()
result.linkPreviewModelVariant = newQVariant(result.linkPreviewModel) result.linkPreviewModelVariant = newQVariant(result.linkPreviewModel)
result.askToEnableLinkPreview = false
proc load*(self: View) = proc load*(self: View) =
self.delegate.viewDidLoad() self.delegate.viewDidLoad()
@ -197,6 +199,17 @@ QtObject:
QtProperty[QVariant] linkPreviewModel: QtProperty[QVariant] linkPreviewModel:
read = getLinkPreviewModel 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 # Currently used to fetch link previews, but could be used elsewhere
proc setText*(self: View, text: string) {.slot.} = proc setText*(self: View, text: string) {.slot.} =
self.delegate.setText(text) self.delegate.setText(text)
@ -207,10 +220,36 @@ QtObject:
proc setUrls*(self: View, urls: seq[string]) = proc setUrls*(self: View, urls: seq[string]) =
self.linkPreviewModel.setUrls(urls) self.linkPreviewModel.setUrls(urls)
self.updateLinkPreviewsFromCache(urls) if(self.delegate.getLinkPreviewEnabled()):
self.updateLinkPreviewsFromCache(urls)
else:
self.linkPreviewModel.removeAllPreviewData()
proc clearLinkPreviewCache*(self: View) {.slot.} = proc clearLinkPreviewCache*(self: View) {.slot.} =
self.delegate.clearLinkPreviewCache() self.delegate.clearLinkPreviewCache()
proc reloadLinkPreview(self: View, link: string) {.slot.} = 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)

View File

@ -213,8 +213,17 @@ QtObject:
defer: modelIndex.delete defer: modelIndex.delete
self.dataChanged(modelIndex, modelIndex) 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] = proc getUnfuledLinkPreviews*(self: Model): seq[LinkPreview] =
result = @[] result = @[]
for item in self.items: for item in self.items:
if item.unfurled and item.linkPreview.hostName != "": if item.unfurled and item.linkPreview.hostName != "":
result.add(item.linkPreview) result.add(item.linkPreview)
proc getLinks*(self: Model): seq[string] =
result = @[]
for item in self.items:
result.add(item.linkPreview.url)

View File

@ -1,24 +1,81 @@
import QtQuick 2.15 import QtQuick 2.15
import QtQuick.Controls 2.15 import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15
import QtGraphicalEffects 1.15 import QtGraphicalEffects 1.15
import Storybook 1.0
import StatusQ.Core.Theme 0.1 import StatusQ.Core.Theme 0.1
import shared.controls.chat 1.0 import shared.controls.chat 1.0
Page { SplitView {
Rectangle {
id: wrapper
anchors.fill: parent
color: Theme.palette.statusChatInput.secondaryBackgroundColor
ChatInputLinksPreviewArea { Logs { id: logs }
id: chatInputLinkPreviewsArea orientation: Qt.Vertical
anchors.centerIn: parent
width: parent.width SplitView {
imagePreviewModel: ["https://picsum.photos/200/300?random=1", "https://picsum.photos/200/300?random=1"]
linkPreviewModel: linkPreviewListModel SplitView.fillWidth: true
onLinkRemoved: linkPreviewListModel.remove(index) 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 { ListModel {
@ -143,4 +200,4 @@ Page {
// category: Panels // category: Panels
// https://www.figma.com/file/Mr3rqxxgKJ2zMQ06UAKiWL/💬-ChatDesktop?type=design&node-id=22341-184809&mode=design&t=VWBVK4DOUxr1BmTp-0 // https://www.figma.com/file/Mr3rqxxgKJ2zMQ06UAKiWL/💬-ChatDesktop?type=design&node-id=22341-184809&mode=design&t=VWBVK4DOUxr1BmTp-0

View File

@ -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/💬-ChatDesktop?type=design&node-id=22341-184809&mode=design&t=91pnQgUZAqFJLcqM-0

View File

@ -94,31 +94,11 @@ SplitView {
sourceComponent: StatusChatInput { sourceComponent: StatusChatInput {
id: chatInput id: chatInput
property var globalUtils: globalUtilsMock.globalUtils property var globalUtils: globalUtilsMock.globalUtils
property string unformattedText: textInput.getText(0, textInput.length) property string unformattedText: chatInput.textInput.getText(0, chatInput.textInput.length)
onUnformattedTextChanged: { onUnformattedTextChanged: {
textEditConnection.enabled = false textEditConnection.enabled = false
d.loadLinkPreviews(unformattedText)
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: ""
})
}
})
textEditConnection.enabled = true textEditConnection.enabled = true
} }
@ -133,6 +113,13 @@ SplitView {
enabled: enabledCheckBox.checked enabled: enabledCheckBox.checked
linkPreviewModel: fakeLinksModel linkPreviewModel: fakeLinksModel
askToEnableLinkPreview: askToEnableLinkPreviewSwitch.checked
onAskToEnableLinkPreviewChanged: {
if(askToEnableLinkPreview) {
fakeLinksModel.clear()
d.loadLinkPreviews(unformattedText)
}
}
usersStore: QtObject { usersStore: QtObject {
readonly property var usersModel: fakeUsersModel readonly property var usersModel: fakeUsersModel
} }
@ -141,6 +128,26 @@ SplitView {
logs.logEvent("StatusChatInput::sendMessage", ["PlainText"], [globalUtilsMock.globalUtils.plainText(chatInput.getTextWithPublicKeys())]) logs.logEvent("StatusChatInput::sendMessage", ["PlainText"], [globalUtilsMock.globalUtils.plainText(chatInput.getTextWithPublicKeys())])
logs.logEvent("StatusChatInput::sendMessage", ["RawText"], [chatInput.textInput.text]) 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 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 { Pane {
@ -207,6 +244,18 @@ SplitView {
text: "Links" text: "Links"
Layout.fillWidth: true Layout.fillWidth: true
} }
Switch {
id: linkPreviewSwitch
text: "Link Preview enabled"
}
Switch {
id: askToEnableLinkPreviewSwitch
text: "Ask to enable Link Preview"
checked: true
}
ComboBox { ComboBox {
id: linksNb id: linksNb
editable: true editable: true
@ -245,4 +294,4 @@ SplitView {
// category: Components // category: Components
// https://www.figma.com/file/Mr3rqxxgKJ2zMQ06UAKiWL/💬-ChatDesktop?type=design&node-id=23155-66084&mode=design&t=VWBVK4DOUxr1BmTp-0 // https://www.figma.com/file/Mr3rqxxgKJ2zMQ06UAKiWL/💬-ChatDesktop?type=design&node-id=23155-66084&mode=design&t=VWBVK4DOUxr1BmTp-0

View File

@ -130,17 +130,17 @@ Item {
chatInput.validateImagesAndShowImageArea(filesList) chatInput.validateImagesAndShowImageArea(filesList)
} }
function restoreInputState() { function restoreInputState(textInput) {
if (!d.activeChatContentModule) { if (!d.activeChatContentModule) {
chatInput.setText("") chatInput.clear()
chatInput.resetReplyArea() chatInput.resetReplyArea()
chatInput.resetImageArea() chatInput.resetImageArea()
return return
} }
// Restore message text // Restore message text
chatInput.setText(d.activeChatContentModule.inputAreaModule.preservedProperties.text) chatInput.setText(textInput)
d.restoreInputReply() d.restoreInputReply()
d.restoreInputAttachments() d.restoreInputAttachments()
@ -154,9 +154,14 @@ Item {
} }
onActiveChatContentModuleChanged: { onActiveChatContentModuleChanged: {
let preservedText = ""
if (d.activeChatContentModule) {
preservedText = d.activeChatContentModule.inputAreaModule.preservedProperties.text
}
d.activeChatContentModule.inputAreaModule.clearLinkPreviewCache() d.activeChatContentModule.inputAreaModule.clearLinkPreviewCache()
// Call later to make sure activeUsersStore and activeMessagesStore bindings are updated // 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 store: root.rootStore
usersStore: d.activeUsersStore usersStore: d.activeUsersStore
linkPreviewModel: d.activeChatContentModule.inputAreaModule.linkPreviewModel 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: { textInput.placeholderText: {
if (!channelPostRestrictions.visible) { if (!channelPostRestrictions.visible) {
if (d.activeChatContentModule.chatDetails.blocked) if (d.activeChatContentModule.chatDetails.blocked)
@ -315,6 +325,11 @@ Item {
} }
onLinkPreviewReloaded: (link) => d.activeChatContentModule.inputAreaModule.reloadLinkPreview(link) 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 { ChatPermissionQualificationPanel {

View File

@ -8,22 +8,44 @@ import StatusQ.Core 0.1
import shared.status 1.0 import shared.status 1.0
import shared.controls.chat 1.0 import shared.controls.chat 1.0
import utils 1.0
import SortFilterProxyModel 0.2 import SortFilterProxyModel 0.2
Control { Control {
id: root 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 var linkPreviewModel
required property bool showLinkPreviewSettings
readonly property alias hoveredUrl: d.hoveredUrl 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 imageRemoved(int index)
signal imageClicked(var chatImage) signal imageClicked(var chatImage)
signal linkReload(string link) signal linkReload(string link)
signal linkClicked(string link) signal linkClicked(string link)
signal linkRemoved(string link)
signal enableLinkPreview()
signal enableLinkPreviewForThisMessage()
signal disableLinkPreview()
signal dismissLinkPreviewSettings()
signal dismissLinkPreview(int index)
horizontalPadding: 12 horizontalPadding: 12
topPadding: 12 topPadding: 12
@ -60,6 +82,8 @@ Control {
contentWidth: layout.width contentWidth: layout.width
contentHeight: layout.height contentHeight: layout.height
onFlickStarted: settingsContextMenu.close()
RowLayout { RowLayout {
id: layout id: layout
spacing: 8 spacing: 8
@ -67,12 +91,13 @@ Control {
id: imageArea id: imageArea
Layout.preferredHeight: 64 Layout.preferredHeight: 64
spacing: layout.spacing spacing: layout.spacing
imageSource: imagePreviewModel imageSource: imagePreviewArray
onImageClicked: root.imageClicked(chatImage) onImageClicked: root.imageClicked(chatImage)
onImageRemoved: root.imageRemoved(index) onImageRemoved: root.imageRemoved(index)
visible: !!imagePreviewModel && imagePreviewModel.length > 0 visible: !!imagePreviewArray && imagePreviewArray.length > 0
} }
Repeater { Repeater {
id: linkPreviewRepeater
model: d.filteredModel model: d.filteredModel
delegate: LinkPreviewMiniCard { delegate: LinkPreviewMiniCard {
// Model properties // Model properties
@ -104,9 +129,10 @@ Control {
unfurled && hostname === "" ? LinkPreviewMiniCard.State.LoadingFailed : unfurled && hostname === "" ? LinkPreviewMiniCard.State.LoadingFailed :
!unfurled ? LinkPreviewMiniCard.State.Loading : LinkPreviewMiniCard.State.Invalid !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) onRetry: root.linkReload(url)
onClicked: root.linkClicked(url) onClicked: root.linkClicked(url)
onRightClicked: settingsContextMenu.popup()
onContainsMouseChanged: { onContainsMouseChanged: {
if (containsMouse) { if (containsMouse) {
d.hoveredUrl = url 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()
}
} }

View File

@ -42,6 +42,7 @@ CalloutCard {
signal close() signal close()
signal retry() signal retry()
signal clicked(var eventPoint) signal clicked(var eventPoint)
signal rightClicked(var eventPoint)
implicitWidth: 260 implicitWidth: 260
implicitHeight: 64 implicitHeight: 64
@ -181,7 +182,6 @@ CalloutCard {
icon: "tiny/chevron-right" icon: "tiny/chevron-right"
color: Theme.palette.baseColor1 color: Theme.palette.baseColor1
visible: secondTitleText.visible visible: secondTitleText.visible
} }
StatusBaseText { StatusBaseText {
id: secondTitleText id: secondTitleText
@ -246,4 +246,8 @@ CalloutCard {
target: background target: background
onTapped: root.clicked(eventPoint) onTapped: root.clicked(eventPoint)
} }
TapHandler {
acceptedButtons: Qt.RightButton
onTapped: root.rightClicked(eventPoint)
}
} }

View File

@ -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()
}
}
}

View File

@ -5,6 +5,7 @@ FetchMoreMessagesButton 1.0 FetchMoreMessagesButton.qml
GapComponent 1.0 GapComponent.qml GapComponent 1.0 GapComponent.qml
LinkPreviewCard 1.0 LinkPreviewCard.qml LinkPreviewCard 1.0 LinkPreviewCard.qml
LinkPreviewMiniCard 1.0 LinkPreviewMiniCard.qml LinkPreviewMiniCard 1.0 LinkPreviewMiniCard.qml
LinkPreviewSettingsCard 1.0 LinkPreviewSettingsCard.qml
UsernameLabel 1.0 UsernameLabel.qml UsernameLabel 1.0 UsernameLabel.qml
UserProfileCard 1.0 UserProfileCard.qml UserProfileCard 1.0 UserProfileCard.qml
DateGroup 1.0 DateGroup.qml DateGroup 1.0 DateGroup.qml

View File

@ -28,9 +28,13 @@ Rectangle {
signal stickerSelected(string hashId, string packId, string url) signal stickerSelected(string hashId, string packId, string url)
signal sendMessage(var event) signal sendMessage(var event)
signal keyUpPress() signal keyUpPress()
signal linkPreviewRemoved(string link)
signal linkPreviewReloaded(string link) signal linkPreviewReloaded(string link)
signal enableLinkPreview()
signal enableLinkPreviewForThisMessage()
signal disableLinkPreview()
signal dismissLinkPreviewSettings()
signal dismissLinkPreview(int index)
property var usersStore property var usersStore
property var store property var store
@ -61,6 +65,8 @@ Rectangle {
property var linkPreviewModel: null property var linkPreviewModel: null
property bool askToEnableLinkPreview: false
property var imageErrorMessageLocation: StatusChatInput.ImageErrorMessageLocation.Top // TODO: Remove this property? property var imageErrorMessageLocation: StatusChatInput.ImageErrorMessageLocation.Top // TODO: Remove this property?
property alias suggestions: suggestionsBox property alias suggestions: suggestionsBox
@ -1179,11 +1185,12 @@ Rectangle {
ChatInputLinksPreviewArea { ChatInputLinksPreviewArea {
id: linkPreviewArea id: linkPreviewArea
Layout.fillWidth: true Layout.fillWidth: true
visible: contentItemsCount > 0 visible: hasContent
horizontalPadding: 12 horizontalPadding: 12
topPadding: 12 topPadding: 12
imagePreviewModel: control.fileUrlsAndSources imagePreviewArray: control.fileUrlsAndSources
linkPreviewModel: control.linkPreviewModel linkPreviewModel: control.linkPreviewModel
showLinkPreviewSettings: control.askToEnableLinkPreview
onImageRemoved: (index) => { 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 //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 let urls = control.fileUrlsAndSources
@ -1194,8 +1201,12 @@ Rectangle {
} }
onImageClicked: (chatImage) => Global.openImagePopup(chatImage) onImageClicked: (chatImage) => Global.openImagePopup(chatImage)
onLinkReload: (link) => control.linkPreviewReloaded(link) onLinkReload: (link) => control.linkPreviewReloaded(link)
onLinkRemoved: (link) => control.linkPreviewRemoved(link)
onLinkClicked: (link) => Global.openLink(link) onLinkClicked: (link) => Global.openLink(link)
onEnableLinkPreview: () => control.enableLinkPreview()
onEnableLinkPreviewForThisMessage: () => control.enableLinkPreviewForThisMessage()
onDisableLinkPreview: () => control.disableLinkPreview()
onDismissLinkPreviewSettings: () => control.dismissLinkPreviewSettings()
onDismissLinkPreview: (index) => control.dismissLinkPreview(index)
} }
RowLayout { RowLayout {

View File

@ -20,7 +20,7 @@ Row {
Repeater { Repeater {
id: rptImages id: rptImages
Item { Item {
height: chatImage.height height: chatImage.height
width: chatImage.width width: chatImage.width

View File

@ -117,9 +117,7 @@ Item {
} }
const newStackSize = Math.ceil(root.maxStackSize / 2) const newStackSize = Math.ceil(root.maxStackSize / 2)
print("Reducing undo stack to " + newStackSize + " items")
for(var i = 1; i <= newStackSize; i++) { 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)) d.undoStack.splice(i, Math.ceil(root.maxStackSize / newStackSize))
} }
} }