From 4a30d13bdcc61c6a172771ea21ac345528eadaa2 Mon Sep 17 00:00:00 2001 From: Alex Jbanca <47811206+alexjba@users.noreply.github.com> Date: Wed, 25 Oct 2023 18:20:02 +0300 Subject: [PATCH] feat(LinkPreviews): Integrate Link previews with the backend (#12523) * feat(StatusQ): Adding numberToLocaleStringInCompactForm function to LocaleUtils This function will format the number in a compact form E.g: 1000 -> 1K; 1000000 -> 1M; 1100000 -> 1.1M + adding tests fix(statusQ): Update numberToLocaleStringInCompactForm to return the locale number when greter than 999T fix(StatusQ): extend the test_numberToLocaleStringInCompactForm with new data * feat(LinkPreviews): Update the link preview area in StatusChatInput to use the new model Changes: 1. Create a new component `LinkPreviewMiniCardDelegate.qml` that filters the model data to properly fill the link preview card with the needed data based on the preview type 2. Update storybook pages 3. Small updates to LinkPreviewMiniCard * feat(LinkPreviews): Update the link previews in message history to use the new backend Changes: 1. Create delegate items for LinkPreviewCard and gif link preview to clean the LinksMessageView component and filter the model data based on the preview type 2. Remove UserProfileCard and reuse the LinkPreviewCard to display contacts link previews 3. Update LinkPreviewCard so that it can accommodate status link previews (communities, channels, contacts). The generic properties (title, description, footer) have been dropped and replaced with specialised properties for each preview type. 4. Fix LinkPreviewCard layout to better accommodate different content variants (missing properties, long/short title, missing description, missing icon) 5. Fixing the link preview context menu and click actions fix: Move inline components to separate files Fixing the linux builds using Qt 5.15.2 affected by this bug: https://bugreports.qt.io/browse/QTBUG-89180 * fix: Align LinkPreviewMiniCard implementation with LinkPreviewCard and remove state based model filtering --- .../pages/ChatInputLinksPreviewAreaPage.qml | 122 +------ storybook/pages/LinkPreviewCardPage.qml | 324 +++++++++++++----- storybook/pages/LinkPreviewMiniCardPage.qml | 44 ++- storybook/pages/LinksMessageViewPage.qml | 125 ++----- storybook/pages/StatusChatInputPage.qml | 21 +- storybook/pages/UserProfileCardPage.qml | 86 ----- storybook/src/Models/LinkPreviewModel.qml | 136 ++++++++ storybook/src/Models/qmldir | 1 + ui/StatusQ/src/StatusQ/Core/LocaleUtils.qml | 24 ++ .../src/assets/img/icons/active-members.svg | 3 + .../tests/TestUtils/tst_test-LocaleUtils.qml | 164 +++++++++ ui/app/mainui/AppMain.qml | 4 + .../chat/ChatInputLinksPreviewArea.qml | 53 +-- .../shared/controls/chat/LinkPreviewCard.qml | 251 +++++++++++--- .../controls/chat/LinkPreviewMiniCard.qml | 142 ++++---- .../shared/controls/chat/UserProfileCard.qml | 78 ----- .../controls/chat/private/ChannelData.qml | 9 + .../controls/chat/private/CommunityData.qml | 11 + .../shared/controls/chat/private/LinkData.qml | 10 + .../shared/controls/chat/private/UserData.qml | 9 + ui/imports/shared/controls/chat/qmldir | 1 - .../delegates/LinkPreviewCardDelegate.qml | 91 +++++ .../delegates/LinkPreviewGifDelegate.qml | 80 +++++ .../delegates/LinkPreviewMiniCardDelegate.qml | 86 +++++ ui/imports/shared/controls/delegates/qmldir | 3 + ui/imports/shared/status/StatusChatInput.qml | 2 +- .../shared/views/chat/ImageContextMenu.qml | 4 +- .../shared/views/chat/LinksMessageView.qml | 195 +++-------- ui/imports/shared/views/chat/MessageView.qml | 13 +- ui/imports/utils/Global.qml | 1 + 30 files changed, 1272 insertions(+), 821 deletions(-) delete mode 100644 storybook/pages/UserProfileCardPage.qml create mode 100644 storybook/src/Models/LinkPreviewModel.qml create mode 100644 ui/StatusQ/src/assets/img/icons/active-members.svg delete mode 100644 ui/imports/shared/controls/chat/UserProfileCard.qml create mode 100644 ui/imports/shared/controls/chat/private/ChannelData.qml create mode 100644 ui/imports/shared/controls/chat/private/CommunityData.qml create mode 100644 ui/imports/shared/controls/chat/private/LinkData.qml create mode 100644 ui/imports/shared/controls/chat/private/UserData.qml create mode 100644 ui/imports/shared/controls/delegates/LinkPreviewCardDelegate.qml create mode 100644 ui/imports/shared/controls/delegates/LinkPreviewGifDelegate.qml create mode 100644 ui/imports/shared/controls/delegates/LinkPreviewMiniCardDelegate.qml diff --git a/storybook/pages/ChatInputLinksPreviewAreaPage.qml b/storybook/pages/ChatInputLinksPreviewAreaPage.qml index cb5074f812..adde059d23 100644 --- a/storybook/pages/ChatInputLinksPreviewAreaPage.qml +++ b/storybook/pages/ChatInputLinksPreviewAreaPage.qml @@ -4,6 +4,7 @@ import QtQuick.Layouts 1.15 import QtGraphicalEffects 1.15 import Storybook 1.0 +import Models 1.0 import StatusQ.Core.Theme 0.1 import shared.controls.chat 1.0 @@ -30,7 +31,7 @@ SplitView { 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 + linkPreviewModel: showLinkPreviewSettings ? emptyModel : mockedLinkPreviewModel showLinkPreviewSettings: !linkPreviewEnabledSwitch.checked visible: hasContent @@ -78,123 +79,8 @@ SplitView { id: emptyModel } - ListModel { - id: linkPreviewListModel - ListElement { - url: "https://www.youtube.com/watch?v=9bZkp7q19f0" - unfurled: true - immutable: false - hostname: "youtube.com" - title: "PSY - GANGNAM STYLE(강남스타일) M/V" - description: "" - linkType: 0 - thumbnailWidth: 480 - thumbnailHeight: 360 - thumbnailUrl: "https://picsum.photos/480/360?random=1" - thumbnailDataUri: "" - } - ListElement { - url: "https://www.youtube.com/watch?v=9bZkp7q19f0" - unfurled: false - immutable: false - hostname: "youtube.com" - title: "PSY - GANGNAM STYLE(강남스타일) M/V" - description: "" - linkType: 0 - thumbnailWidth: 480 - thumbnailHeight: 360 - thumbnailUrl: "https://picsum.photos/480/360?random=2" - thumbnailDataUri: "" - } - ListElement { - url: "https://www.youtube.com/watch?v=9bZkp7q19f0" - unfurled: true - immutable: false - hostname: "" - title: "PSY - GANGNAM STYLE(강남스타일) M/V" - description: "" - linkType: 0 - thumbnailWidth: 480 - thumbnailHeight: 360 - thumbnailUrl: "https://picsum.photos/480/360?random=3" - thumbnailDataUri: "" - } - ListElement { - url: "https://www.youtube.com/watch?v=9bZkp7q19f0" - unfurled: true - hostname: "youtube.com" - title: "PSY - GANGNAM STYLE(강남스타일) M/V" - description: "" - linkType: 0 - thumbnailWidth: 480 - thumbnailHeight: 360 - thumbnailUrl: "https://picsum.photos/480/360?random=4" - thumbnailDataUri: "" - } - ListElement { - url: "https://www.youtube.com/watch?v=9bZkp7q19f0" - unfurled: true - immutable: false - hostname: "youtube.com" - title: "PSY - GANGNAM STYLE(강남스타일) M/V" - description: "" - linkType: 0 - thumbnailWidth: 480 - thumbnailHeight: 360 - thumbnailUrl: "https://picsum.photos/480/360?random=5" - thumbnailDataUri: "" - } - ListElement { - url: "https://www.youtube.com/watch?v=9bZkp7q19f0" - unfurled: true - hostname: "youtube.com" - title: "PSY - GANGNAM STYLE(강남스타일) M/V" - description: "" - linkType: 0 - thumbnailWidth: 480 - thumbnailHeight: 360 - thumbnailUrl: "https://picsum.photos/480/360?random=6" - thumbnailDataUri: "" - } - ListElement { - url: "https://www.youtube.com/watch?v=9bZkp7q19f0" - unfurled: true - immutable: false - hostname: "youtube.com" - title: "PSY - GANGNAM STYLE(강남스타일) M/V" - description: "" - linkType: 0 - thumbnailWidth: 480 - thumbnailHeight: 360 - thumbnailUrl: "https://picsum.photos/480/360?random=7" - thumbnailDataUri: "" - } - ListElement { - url: "https://www.youtube.com/watch?v=9bZkp7q19f0" - unfurled: true - immutable: false - hostname: "youtube.com" - title: "PSY - GANGNAM STYLE(강남스타일) M/V" - description: "" - linkType: 0 - thumbnailWidth: 480 - thumbnailHeight: 360 - thumbnailUrl: "https://picsum.photos/480/360?random=8" - thumbnailDataUri: "" - } - ListElement { - url: "https://www.youtube.com/watch?v=9bZkp7q19f0" - unfurled: true - immutable: false - hostname: "youtube.com" - title: "PSY - GANGNAM STYLE(강남스타일) M/V" - description: "" - linkType: 0 - thumbnailWidth: 480 - thumbnailHeight: 360 - thumbnailUrl: "https://picsum.photos/480/360?random=9" - thumbnailDataUri: "" - } + LinkPreviewModel { + id: mockedLinkPreviewModel } } diff --git a/storybook/pages/LinkPreviewCardPage.qml b/storybook/pages/LinkPreviewCardPage.qml index 2c89b65fa8..3bb6f88975 100644 --- a/storybook/pages/LinkPreviewCardPage.qml +++ b/storybook/pages/LinkPreviewCardPage.qml @@ -2,39 +2,145 @@ import QtQuick 2.15 import QtQuick.Controls 2.15 import QtQuick.Layouts 1.15 +import Qt.labs.settings 1.0 + import StatusQ.Core.Theme 0.1 import shared.controls.chat 1.0 import utils 1.0 + SplitView { id: root - property alias logoSettings: previewCard.logoSettings property string ytBannerQuality: "hqdefault" + property string image: Style.png("tokens/SOCKS") + property string banner: rawImageCheck.checked ? rawImageCheck.rawImageData : "https://img.youtube.com/vi/yHN1M7vcPKU/%1.jpg".arg(root.ytBannerQuality) + property bool globalUtilsReady: false - Pane { - SplitView.fillWidth: true - SplitView.fillHeight: true + // globalUtilsInst mock + QtObject { + function getEmojiHashAsJson(publicKey) { + return JSON.stringify(["👨🏻‍🍼", "🏃🏿‍♂️", "🌇", "🤶🏿", "🏮","🤷🏻‍♂️", "🤦🏻", "📣", "🤎", "👷🏽", "😺", "🥞", "🔃", "🧝🏽‍♂️"]) + } + function getColorId(publicKey) { return 4 } - LinkPreviewCard { - id: previewCard - bannerImageSource: "https://img.youtube.com/vi/yHN1M7vcPKU/%1.jpg".arg(root.ytBannerQuality) - title: titleInput.text - description: descriptionInput.text - footer: footerInput.text - logoSettings.name: Style.png("tokens/SOCKS") - logoSettings.isImage: true - logoSettings.isLetterIdenticon: false + function getCompressedPk(publicKey) { return "zx3sh" + publicKey } + + function getColorHashAsJson(publicKey) { + return JSON.stringify([{4: 0, segmentLength: 1}, + {5: 19, segmentLength: 2}]) + } + + function isCompressedPubKey(publicKey) { return true } + + Component.onCompleted: { + Utils.globalUtilsInst = this + root.globalUtilsReady = true + + } + Component.onDestruction: { + root.globalUtilsReady = false + Utils.globalUtilsInst = {} } } Pane { + SplitView.fillWidth: true + SplitView.fillHeight: true + + LinkPreviewCard { + id: previewCard + type: 1 + linkData { + title: titleInput.text + description: descriptionInput.text + domain: footerInput.text + thumbnail: root.banner + image: root.image + } + userData { + name: userNameInput.text + publicKey: "zQ3shgmVJjmwwhkfAemjDizYJtv9nzot7QD4iRJ52ZkgdU6Ci" + bio: bioInput.text + image: root.image + ensVerified: false + } + communityData { + name: titleInput.text + description: descriptionInput.text + banner: root.banner + image: root.image + membersCount: parseInt(membersCountInput.text) + activeMembersCount: parseInt(activeMembersCountInput.text) + color: "orchid" + } + channelData { + name: titleInput.text + description: descriptionInput.text + emoji: "" + color: "blue" + communityData { + name: "Community" + titleInput.text + description: "Community" + descriptionInput.text + banner: root.banner + image: root.image + membersCount: parseInt(membersCountInput.text) + activeMembersCount: parseInt(activeMembersCountInput.text) + color: "orchid" + } + } + } + } + + + ScrollView { SplitView.preferredWidth: 500 SplitView.fillHeight: true + leftPadding: 10 ColumnLayout { + id: layout spacing: 24 ColumnLayout { + Label { + Layout.fillWidth: true + text: "Card type" + } + + RadioButton { + text: qsTr("Link") + checked: previewCard.type === Constants.LinkPreviewType.Standard + onToggled: previewCard.type = Constants.LinkPreviewType.Standard + } + + RadioButton { + text: qsTr("Contact") + checked: previewCard.type === Constants.LinkPreviewType.StatusContact + onToggled: previewCard.type = Constants.LinkPreviewType.StatusContact + } + + RadioButton { + text: qsTr("Community") + checked: previewCard.type === Constants.LinkPreviewType.StatusCommunity + onToggled: { + previewCard.type = Constants.LinkPreviewType.StatusCommunity + titleInput.text = "Socks" + descriptionInput.text = "Community description goes here. If blank it will enable multi line title." + } + } + + RadioButton { + text: qsTr("Channel") + checked: previewCard.type === Constants.LinkPreviewType.StatusCommunityChannel + onToggled: { + previewCard.type = Constants.LinkPreviewType.StatusCommunityChannel + titleInput.text = "general" + descriptionInput.text = "Channel description goes here. If blank it will enable multi line title." + } + } + } + ColumnLayout { + visible: previewCard.type !== Constants.LinkPreviewType.StatusContact Label { text: "Title" } @@ -45,7 +151,7 @@ SplitView { Layout.fillWidth: true text: "What Is Web3? A Decentralized Internet Via Blockchain Technology That Will Revolutionise All Sectors- Decrypt (@decryptmedia) August 31 2021" } - + Label { text: "Description" } @@ -65,77 +171,125 @@ SplitView { onClicked: descriptionInput.text = "Link description goes here. If blank it will enable multi line title." } } + ColumnLayout { + visible: previewCard.type === Constants.LinkPreviewType.Standard + Label { + text: "Footer" + } + TextField { + id: footerInput + Layout.fillHeight: true + Layout.fillWidth: true + text: "X" + } + } + + ColumnLayout { + visible: previewCard.type === Constants.LinkPreviewType.StatusCommunity + Label { + text: "MembersCount" + } + TextField { + id: membersCountInput + Layout.fillHeight: true + Layout.fillWidth: true + inputMethodHints: Qt.ImhDigitsOnly + text: "629200" + } + TextField { + id: activeMembersCountInput + Layout.fillHeight: true + Layout.fillWidth: true + inputMethodHints: Qt.ImhDigitsOnly + text: "112100" + } + } + } + + ColumnLayout { + visible: previewCard.type === Constants.LinkPreviewType.StatusContact Label { - text: "Footer" + text: "User name" } TextField { - id: footerInput + id: userNameInput Layout.fillHeight: true Layout.fillWidth: true - text: footerTypeCommunity.footerRichText + text: "Test user name" + } + + Label { + text: "Bio" + } + RowLayout { + TextField { + id: bioInput + Layout.fillHeight: true + Layout.fillWidth: true + text: "User bio description goes here. If blank it will enable multi line title." + } + Button { + text: "clear" + onClicked: bioInput.text = "" + } + Button { + text: "Set" + onClicked: bioInput.text = "User bio description goes here. If blank it will enable multi line title." + } } } + ColumnLayout { + visible: previewCard.type === Constants.LinkPreviewType.StatusCommunityChannel Label { Layout.fillWidth: true - text: "Logo" + text: "channel settings" } - - RadioButton { - text: qsTr("No logo") - checked: root.logoSettings.name === "" && root.logoSettings.emoji === "" - onToggled: { - root.logoSettings.name = "" - root.logoSettings.emoji = "" - root.logoSettings.isImage = false - root.logoSettings.isLetterIdenticon = false - } - } - - RadioButton { - readonly property string rawImageData: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB0AAAAcCAYAAACdz7SqAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAM2SURBVHgBtVbNbtNAEJ7ZpBQ4pRGF9kQqWqkBRNwnwLlxI9y4NX2CiiOntE9QeINw49a8QdwT3NhKQCKaSj4WUVXmABRqe5hxE+PGTuyk5ZOSXe/ftzs/3y5CBiw/NEzw/cdAaCJAifgXdCA4QGAjggbEvbMf0LJt7aSth6lkHjW4akIG8GI2/1k5H7e7XW2PGRdHqWQU8jdoNytZIrnC7YNPupnUnxtuWF01SjhD77hqwPQosNlrxdt34OTb172xpELoKvrA1QW4EqCZRJyLEnpI7ZBQggThlGvXYVLI3HAeE88vfj85Pno/6FaDiqeoEUZlMA9bvc/7cxyxVa6/SeM5j2Tcdn/hnHsNly520s7KAyN0V17+7pWNGhHVhxYJTNLraosLi8e0kMBxT0FH00IW830oeT/ButBertjRQ5BPO1xUQ1IE2oQUHHZ0K6mdI1RzoSEdpqRg76O2lPgSElKDdz919JYMoxA95QDow7qUykWoxTo5z2YIXsGUsLV2CPD1cDu7MODiQKKnsVmI1jhFyQJvFrb6URxFQWJAYYIZSEF6tKZATitFQpehEm1PkCraWYCE+8Nt5ENBwX8EAd2NNaKQxu0ukVuCqwATQHwnjhphShMuiSAVKZ527E6bzYt78Q3SulxvcAm44K8ntXMqagmkJDUpzNwMZGsqBDqLuDXcLvkvqajcWWgm+ZUI6svlym5fsbITlh9tsgi0Ezs5//vkMtBocqSJOZw84ZrHPiXFJ6UwECx5A/FbqNXX2hAiefkzqCNRha1Wi8yJgddeCk4qHzkK1aMgdypfshYRbkTGm3z0Rs6LW0REgDXVEMuMI0TE5kDlgkv8+PjIKRYXfzPxEyH2EYzDzv7L4q1FHsvpg8Gkt186OlGp5uYXZMjzkYS8txwfQnj63//APmzDIF1yWJVrCDJgeZVfjTjCj0KicC3qlny0053FZ/k/PFnyy6P2yv1Kk1T/1eCGF/pEYCncGI6DCzIo/uGnRvg8CfzE5MEPoQGT4Pz5Uj3oxp+hMe0V4oOOrssOMfmWyMJo5X1cG2WZkYIvO2Tn85sGXwg5B5Q9kiKMas5DntPr6Oq4+/gvs8hkkbAzoC8AAAAASUVORK5CYII=" - text: qsTr("Raw image") - checked: root.logoSettings.name === rawImageData - onToggled: { - root.logoSettings.name = rawImageData - root.logoSettings.isImage = true - root.logoSettings.isLetterIdenticon = false - } - } - - RadioButton { - text: qsTr("QRC asset: SOCKS") - checked: root.logoSettings.name = Style.png("tokens/SOCKS") - onToggled:{ - root.logoSettings.name = Style.png("tokens/SOCKS") - root.logoSettings.isImage = true - root.logoSettings.isLetterIdenticon = false - } - } - RadioButton { - text: qsTr("Letter identicon") - checked: root.logoSettings.name = "github.com" - onToggled: { - root.logoSettings.name = "github.com" - root.logoSettings.emoji = "" - root.logoSettings.isLetterIdenticon = true - root.logoSettings.color = "blue" - } - } - RadioButton { + CheckBox { text: qsTr("Emoji") - checked: root.logoSettings.emoji === "👋" - onToggled: { - root.logoSettings.emoji = "👋" - root.logoSettings.isLetterIdenticon = true - root.logoSettings.color = "orchid" - } + checked: previewCard.channelData.emoji === "👋" + onToggled: previewCard.channelData.emoji = checked ? "👋" : "" + } + RadioButton { + text: qsTr("Blue channel color") + checked: previewCard.channelData.color === "blue" + onToggled: previewCard.channelData.color = "blue" + } + RadioButton { + text: qsTr("Red channel color") + checked: previewCard.channelData.color === "red" + onToggled: previewCard.channelData.color = "red" } } + Label { + Layout.fillWidth: true + text: "Logo" + } + + RadioButton { + text: qsTr("no image") + checked: root.image === "" + onToggled: root.image = "" + } + + RadioButton { + readonly property string rawImageData: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB0AAAAcCAYAAACdz7SqAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAM2SURBVHgBtVbNbtNAEJ7ZpBQ4pRGF9kQqWqkBRNwnwLlxI9y4NX2CiiOntE9QeINw49a8QdwT3NhKQCKaSj4WUVXmABRqe5hxE+PGTuyk5ZOSXe/ftzs/3y5CBiw/NEzw/cdAaCJAifgXdCA4QGAjggbEvbMf0LJt7aSth6lkHjW4akIG8GI2/1k5H7e7XW2PGRdHqWQU8jdoNytZIrnC7YNPupnUnxtuWF01SjhD77hqwPQosNlrxdt34OTb172xpELoKvrA1QW4EqCZRJyLEnpI7ZBQggThlGvXYVLI3HAeE88vfj85Pno/6FaDiqeoEUZlMA9bvc/7cxyxVa6/SeM5j2Tcdn/hnHsNly520s7KAyN0V17+7pWNGhHVhxYJTNLraosLi8e0kMBxT0FH00IW830oeT/ButBertjRQ5BPO1xUQ1IE2oQUHHZ0K6mdI1RzoSEdpqRg76O2lPgSElKDdz919JYMoxA95QDow7qUykWoxTo5z2YIXsGUsLV2CPD1cDu7MODiQKKnsVmI1jhFyQJvFrb6URxFQWJAYYIZSEF6tKZATitFQpehEm1PkCraWYCE+8Nt5ENBwX8EAd2NNaKQxu0ukVuCqwATQHwnjhphShMuiSAVKZ527E6bzYt78Q3SulxvcAm44K8ntXMqagmkJDUpzNwMZGsqBDqLuDXcLvkvqajcWWgm+ZUI6svlym5fsbITlh9tsgi0Ezs5//vkMtBocqSJOZw84ZrHPiXFJ6UwECx5A/FbqNXX2hAiefkzqCNRha1Wi8yJgddeCk4qHzkK1aMgdypfshYRbkTGm3z0Rs6LW0REgDXVEMuMI0TE5kDlgkv8+PjIKRYXfzPxEyH2EYzDzv7L4q1FHsvpg8Gkt186OlGp5uYXZMjzkYS8txwfQnj63//APmzDIF1yWJVrCDJgeZVfjTjCj0KicC3qlny0053FZ/k/PFnyy6P2yv1Kk1T/1eCGF/pEYCncGI6DCzIo/uGnRvg8CfzE5MEPoQGT4Pz5Uj3oxp+hMe0V4oOOrssOMfmWyMJo5X1cG2WZkYIvO2Tn85sGXwg5B5Q9kiKMas5DntPr6Oq4+/gvs8hkkbAzoC8AAAAASUVORK5CYII=" + text: qsTr("Raw image") + checked: root.image === rawImageData + onToggled: root.image = rawImageData + } + + RadioButton { + text: qsTr("QRC asset: SOCKS") + checked: root.image === Style.png("tokens/SOCKS") + onToggled: root.image = Style.png("tokens/SOCKS") + } + ColumnLayout { + visible: previewCard.type !== Constants.LinkPreviewType.StatusContact Label { Layout.fillWidth: true text: "Banner size" @@ -157,29 +311,17 @@ SplitView { } } + CheckBox { + id: rawImageCheck + readonly property string rawImageData: "data:image/jpeg;base64,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" + text: qsTr("Raw image banner") + checked: root.banner === rawImageData + } + ColumnLayout { Label { Layout.fillWidth: true - text: "Footer type" - } - RadioButton { - id: footerTypeCommunity - property string footerRichText: ` 629.2K 112.1K`.arg(Style.svg("group")).arg(Theme.palette.directColor1).arg(Style.svg("active-members")) - text: qsTr("Community") - checked: footerInput.text === footerRichText - onToggled: footerInput.text = footerRichText - } - RadioButton { - property string footerRichText: `%1 %3`.arg(qsTr("Channel in")).arg(Style.png("tokens/SOCKS")).arg(qsTr("Doodles")) - text: qsTr("Channel") - checked: footerInput.text === footerRichText - onToggled: footerInput.text = footerRichText - } - RadioButton { - text: qsTr("Link domain") - property string footerText: "X" - checked: footerInput.text === footerText - onToggled: footerInput.text = footerText + text: "UserName" } } @@ -201,4 +343,12 @@ SplitView { } } } + + Settings { + property alias linkType: previewCard.type + } } + +// category: Controls + +// https://www.figma.com/file/Mr3rqxxgKJ2zMQ06UAKiWL/💬-Chat⎜Desktop?type=design&node-id=22347-219545&mode=design&t=bODv5MUGQgU9ThJF-0 diff --git a/storybook/pages/LinkPreviewMiniCardPage.qml b/storybook/pages/LinkPreviewMiniCardPage.qml index bf08923785..efb35722e9 100644 --- a/storybook/pages/LinkPreviewMiniCardPage.qml +++ b/storybook/pages/LinkPreviewMiniCardPage.qml @@ -25,15 +25,39 @@ SplitView { LinkPreviewMiniCard { id: previewMiniCard anchors.centerIn: parent - titleStr: type === LinkPreviewMiniCard.Type.User ? userNameInput.text : titleInput.text - domain: domainInput.text - favIconUrl: faviconInput.text - previewState: stateInput.currentIndex - thumbnailImageUrl: externalImageInput.text type: previewTypeInput.currentIndex - communityName: communityNameInput.text - channelName: channelNameInput.text - } + previewState: stateInput.currentIndex + linkData { + title: titleInput.text + description: "" + domain: domainInput.text + thumbnail: externalImageInput.text + image: faviconInput.text + } + userData { + name: userNameInput.text + publicKey: "zQ3shgmVJjmwwhkfAemjDizYJtv9nzot7QD4iRJ52ZkgdU6Ci" + image: faviconInput.text + ensVerified: false + } + communityData { + name: communityNameInput.text + banner: externalImageInput.text + image: faviconInput.text + color: "orchid" + } + channelData { + name: channelNameInput.text + emoji: "" + color: "blue" + communityData { + name: communityNameInput.text + banner: externalImageInput.text + image: faviconInput.text + color: "orchid" + } + } + } } Pane { @@ -50,7 +74,7 @@ SplitView { id: previewTypeInput Layout.fillHeight: true Layout.fillWidth: true - model: ["link", "image", "community", "channel", "user profile"] + model: ["unknown", "standard", "user profile", "community", "channel"] } Label { text: "Community name" @@ -146,4 +170,4 @@ SplitView { //Category: Controls -//"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/LinksMessageViewPage.qml b/storybook/pages/LinksMessageViewPage.qml index e947d47f39..708594e87d 100644 --- a/storybook/pages/LinksMessageViewPage.qml +++ b/storybook/pages/LinksMessageViewPage.qml @@ -2,87 +2,15 @@ import QtQuick 2.15 import QtQuick.Controls 2.15 import QtQuick.Layouts 1.15 +import Models 1.0 + import shared.views.chat 1.0 import shared.stores 1.0 SplitView { - ListModel { + LinkPreviewModel { id: mockedLinkPreviewModel - - // Create the model dynamically, because `ListElement` doesnt suppport nested elements - Component.onCompleted: { - const emptyObject = { - "unfurled": true, - "immutable": false, - "empty": false, - "url": "", - "previewType": 1, - "standardPreview": {}, - "standardPreviewThumbnail": {}, - "statusContactPreview": {}, - "statusContactPreviewThumbnail": {}, - "statusCommunityPreview": {}, - "statusCommunityPreviewIcon": {}, - "statusCommunityPreviewBanner": {}, - "statusCommunityChannelPreview": {}, - "statusCommunityChannelCommunityPreview": {}, - "statusCommunityChannelCommunityPreviewIcon": {}, - "statusCommunityChannelCommunityPreviewBanner": {}, - } - - const preview1 = Object.assign({}, emptyObject) - preview1.url = "https://www.youtube.com/watch?v=9bZkp7q19f0" - preview1.previewType = 1 - preview1.standardPreview = {} - preview1.standardPreviewThumbnail = {} - preview1.standardPreview.hostname = "www.youtube.com" - preview1.standardPreview.title = "PSY - GANGNAM STYLE(강남스타일) M/V" - preview1.standardPreview.description = "PSY - ‘I LUV IT’ M/V @ https://youtu.be/Xvjnoagk6GU PSY - ‘New Face’ M/V @https://youtu.be/OwJPPaEyqhI PSY - 8TH ALBUM '4X2=8' on iTunes @ https://smarturl.it/PSY_8thAlbum PSY - GANGNAM STYLE(강남스타일) on iTunes @ http://smarturl.it/PsyGangnam #PSY #싸이 #GANGNAMSTYLE #강남스타일 More about PSY@ http://www.psyp..." - preview1.standardPreview.linkType = 0 - preview1.standardPreviewThumbnail.width = 480 - preview1.standardPreviewThumbnail.height = 360 - preview1.standardPreviewThumbnail.url = "https://i.ytimg.com/vi/9bZkp7q19f0/hqdefault.jpg" - preview1.standardPreviewThumbnail.dataUri = "" - - - const preview2 = Object.assign({}, emptyObject) - preview2.url = "https://status.app/u/Ow==#zQ3shgmVJjmwwhkfAemjDizYJtv9nzot7QD4iRJ52ZkgdU6Ci" - preview2.previewType = 2 - preview2.statusContactPreview = {} - preview2.statusContactPreview.publicKey = "zQ3shgmVJjmwwhkfAemjDizYJtv9nzot7QD4iRJ52ZkgdU6Ci" - preview2.statusContactPreview.displayName = "Test contact display name" - preview2.statusContactPreview.description = "Test description" - preview2.statusContactPreviewThumbnail = {} - preview2.statusContactPreviewThumbnail.width = 64 - preview2.statusContactPreviewThumbnail.height = 64 - preview2.statusContactPreviewThumbnail.url = "https://placehold.co/64x64" - preview2.statusContactPreviewThumbnail.dataUri = "" - - const preview3 = Object.assign({}, emptyObject) - preview3.url = "https://status.app/c/ixiACjAKDlRlc3QgQ29tbXVuaXR5Eg9PcGVuIGZvciBhbnlvbmUYdiIHI0ZGMDAwMCoCHwkD#zQ3shnd55dNx9yTihuL6XMbmyM6UNjzU6jk77h5Js31jxcT5V" - preview3.previewType = 3 - preview3.statusCommunityPreview = {} - preview3.statusCommunityPreview.communityId = "zQ3shnd55dNx9yTihuL6XMbmyM6UNjzU6jk77h5Js31jxcT5V" - preview3.statusCommunityPreview.displayName = "Test community display name" - preview3.statusCommunityPreview.description = "Test community description" - preview3.statusCommunityPreview.membersCount = 10 - preview3.statusCommunityPreview.color = "#123456" - preview3.statusCommunityPreviewIcon = {} - preview3.statusCommunityPreviewIcon.width = 64 - preview3.statusCommunityPreviewIcon.height = 64 - preview3.statusCommunityPreviewIcon.url = "https://placehold.co/64x64" - preview3.statusCommunityPreviewIcon.dataUri = "" - preview3.statusCommunityPreviewBanner = {} - preview3.statusCommunityPreviewBanner.width = 320 - preview3.statusCommunityPreviewBanner.height = 180 - preview3.statusCommunityPreviewBanner.url = "https://placehold.co/320x180" - preview3.statusCommunityPreviewBanner.dataUri = "" - - mockedLinkPreviewModel.append(preview1) - mockedLinkPreviewModel.append(preview2) - mockedLinkPreviewModel.append(preview3) - } } Pane { @@ -90,38 +18,19 @@ SplitView { SplitView.fillWidth: true SplitView.fillHeight: true - component LinkPreviewObject: QtObject { - required property string url - required property bool unfurled - required property bool empty - required property int previewType - } - - component StandardPreviewObject: QtObject { - required property string hostname - required property string title - required property string description - required property int linkType // 0 = link, 1 = image - } - - component ThumbnailObject: QtObject { - required property int width - required property int height - required property string url - required property string dataUri - } - LinksMessageView { id: linksMessageView anchors.fill: parent - store: {} - messageStore: {} + isOnline: true + playAnimations: true linkPreviewModel: mockedLinkPreviewModel gifLinks: [ "https://media.tenor.com/qN_ytiwLh24AAAAC/cold.gif" ] isCurrentUser: true + gifUnfurlingEnabled: false + canAskToUnfurlGifs: true onImageClicked: { console.log("image clicked") } @@ -148,18 +57,28 @@ SplitView { } CheckBox { text: qsTr("Enabled") - checked: RootStore.gifUnfurlingEnabled - onToggled: RootStore.gifUnfurlingEnabled = !RootStore.gifUnfurlingEnabled + checked: linksMessageView.gifUnfurlingEnabled + onToggled: linksMessageView.gifUnfurlingEnabled = !linksMessageView.gifUnfurlingEnabled } CheckBox { - text: qsTr("Never ask about GIF unfurling again") - checked: RootStore.neverAskAboutUnfurlingAgain - onClicked: RootStore.neverAskAboutUnfurlingAgain = !RootStore.neverAskAboutUnfurlingAgain + text: qsTr("Can ask about GIF unfurling") + checked: linksMessageView.canAskToUnfurlGifs + onClicked: linksMessageView.canAskToUnfurlGifs = !linksMessageView.canAskToUnfurlGifs } Button { text: qsTr("Reset local `askAboutUnfurling` setting") onClicked: linksMessageView.resetLocalAskAboutUnfurling() } + CheckBox { + text: qsTr("Play animations") + checked: linksMessageView.playAnimations + onToggled: linksMessageView.playAnimations = !linksMessageView.playAnimations + } + CheckBox { + text: qsTr("Is online") + checked: linksMessageView.isOnline + onToggled: linksMessageView.isOnline = !linksMessageView.isOnline + } } } } diff --git a/storybook/pages/StatusChatInputPage.qml b/storybook/pages/StatusChatInputPage.qml index 15f0b14896..36af98ea30 100644 --- a/storybook/pages/StatusChatInputPage.qml +++ b/storybook/pages/StatusChatInputPage.qml @@ -74,7 +74,7 @@ SplitView { id: fakeUsersModel } - ListModel { + LinkPreviewModel { id: fakeLinksModel } @@ -172,19 +172,12 @@ SplitView { 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: "" - }) + const linkPreview = fakeLinksModel.getStandardLinkPreview() + linkPreview.url = encodeURI(word) + linkPreview.unfurled = Math.random() > 0.2 + linkPreview.immutable = !d.linkPreviewsEnabled + linkPreview.empty = Math.random() > 0.7 + fakeLinksModel.append(linkPreview) } }) } diff --git a/storybook/pages/UserProfileCardPage.qml b/storybook/pages/UserProfileCardPage.qml deleted file mode 100644 index 3c04ceeb11..0000000000 --- a/storybook/pages/UserProfileCardPage.qml +++ /dev/null @@ -1,86 +0,0 @@ -import QtQuick 2.15 -import QtQuick.Controls 2.15 -import QtQuick.Layouts 1.15 - -import shared.controls.chat 1.0 -import utils 1.0 - -SplitView { - id: root - - - - property bool globalUtilsReady: false - - // globalUtilsInst mock - QtObject { - function getEmojiHashAsJson(publicKey) { - return JSON.stringify(["👨🏻‍🍼", "🏃🏿‍♂️", "🌇", "🤶🏿", "🏮","🤷🏻‍♂️", "🤦🏻", "📣", "🤎", "👷🏽", "😺", "🥞", "🔃", "🧝🏽‍♂️"]) - } - function getColorId(publicKey) { return 4 } - - function getCompressedPk(publicKey) { return "zx3sh" + publicKey } - - function getColorHashAsJson(publicKey) { - return JSON.stringify([{4: 0, segmentLength: 1}, - {5: 19, segmentLength: 2}]) - } - - function isCompressedPubKey(publicKey) { return true } - - Component.onCompleted: { - Utils.globalUtilsInst = this - root.globalUtilsReady = true - - } - Component.onDestruction: { - root.globalUtilsReady = false - Utils.globalUtilsInst = {} - } - } - - - Pane { - SplitView.fillWidth: true - SplitView.fillHeight: true - - Loader { - anchors.centerIn: parent - active: root.globalUtilsReady - sourceComponent: UserProfileCard { - id: userProfileCard - userName: nameInput.text - userPublicKey: "0x1234567890" - userBio: bioInput.text - userImage: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADIAAAAyCAYAAAAeP4ixAAAAlklEQVR4nOzW0QmDQBAG4SSkl7SUQlJGCrElq9F3QdjjVhh/5nv3cFhY9vUIYQiNITSG0BhCExPynn1gWf9bx498P7/ - nzPcxEzGExhBdJGYihtAYQlO+tUZvqrPbqeudo5iJGEJjCE15a3VtodH3q2ImYgiNITTlTdG1nUZ5a92VITQxITFiJmIIjSE0htAYQrMHAAD//+wwFVpz+yqXAAAAAElFTkSuQmCC" - ensVerified: false - } - } - } - - Pane { - SplitView.fillWidth: true - SplitView.fillHeight: true - SplitView.minimumWidth: 300 - - ColumnLayout { - Label { - text: "userName" - } - TextField { - id: nameInput - text: "John Doe" - } - Label { - text: "userBio" - } - TextField { - id: bioInput - text: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed non risus. Suspendisse lectus tortor, dignissim sit amet, adipiscing nec, ultricies sed, dolor." - } - } - } -} - -// https://www.figma.com/file/Mr3rqxxgKJ2zMQ06UAKiWL/💬-Chat⎜Desktop?type=design&node-id=21961-655678&mode=design&t=JiMnPfMaLPWlrFK3-0 diff --git a/storybook/src/Models/LinkPreviewModel.qml b/storybook/src/Models/LinkPreviewModel.qml new file mode 100644 index 0000000000..5ca01dfe1d --- /dev/null +++ b/storybook/src/Models/LinkPreviewModel.qml @@ -0,0 +1,136 @@ +import QtQuick 2.15 + +ListModel { + id: root + + + function getStandardLinkPreview() { + const preview = Object.assign({}, emptyObject) + preview.url = "https://www.youtube.com/watch?v=9bZkp7q19f0" + preview.previewType = 1 + preview.standardPreview = {} + preview.standardPreviewThumbnail = {} + preview.standardPreview.hostname = "www.youtube.com" + preview.standardPreview.title = "PSY - GANGNAM STYLE(강남스타일) M/V" + preview.standardPreview.description = "PSY - ‘I LUV IT’ M/V @ https://youtu.be/Xvjnoagk6GU PSY - ‘New Face’ M/V @https://youtu.be/OwJPPaEyqhI PSY - 8TH ALBUM '4X2=8' on iTunes @ https://smarturl.it/PSY_8thAlbum PSY - GANGNAM STYLE(강남스타일) on iTunes @ http://smarturl.it/PsyGangnam #PSY #싸이 #GANGNAMSTYLE #강남스타일 More about PSY@ http://www.psyp..." + preview.standardPreview.linkType = 0 + preview.standardPreviewThumbnail.width = 480 + preview.standardPreviewThumbnail.height = 360 + preview.standardPreviewThumbnail.url = "https://i.ytimg.com/vi/9bZkp7q19f0/hqdefault.jpg" + preview.standardPreviewThumbnail.dataUri = "" + return preview + } + + function getImageLinkPreview() { + const preview = Object.assign({}, emptyObject) + preview.url = "https://i.ytimg.com/vi/9bZkp7q19f0/hqdefault.jpg" + preview.previewType = 1 + preview.standardPreview = {} + preview.standardPreviewThumbnail = {} + preview.standardPreview.hostname = "i.ytimg.com" + preview.standardPreview.title = "Image_link_preview.png" + preview.standardPreview.description = "Image link preview" + preview.standardPreview.linkType = 1 + preview.standardPreviewThumbnail.width = 480 + preview.standardPreviewThumbnail.height = 360 + preview.standardPreviewThumbnail.url = "https://i.ytimg.com/vi/9bZkp7q19f0/hqdefault.jpg" + preview.standardPreviewThumbnail.dataUri = "" + return preview + } + + function getCommunityLinkPreview() { + const preview = Object.assign({}, emptyObject) + preview.url = "https://status.app/c/ixiACjAKDlRlc3QgQ29tbXVuaXR5Eg9PcGVuIGZvciBhbnlvbmUYdiIHI0ZGMDAwMCoCHwkD#zQ3shnd55dNx9yTihuL6XMbmyM6UNjzU6jk77h5Js31jxcT5V" + preview.previewType = 3 + preview.statusCommunityPreview = {} + preview.statusCommunityPreview.communityId = "zQ3shnd55dNx9yTihuL6XMbmyM6UNjzU6jk77h5Js31jxcT5V" + preview.statusCommunityPreview.displayName = "Test community display name" + preview.statusCommunityPreview.description = "Test community description" + preview.statusCommunityPreview.membersCount = 10 + preview.statusCommunityPreview.color = "#123456" + preview.statusCommunityPreviewIcon = {} + preview.statusCommunityPreviewIcon.width = 64 + preview.statusCommunityPreviewIcon.height = 64 + preview.statusCommunityPreviewIcon.url = "https://picsum.photos/64/64?random=1" + preview.statusCommunityPreviewIcon.dataUri = "" + preview.statusCommunityPreviewBanner = {} + preview.statusCommunityPreviewBanner.width = 320 + preview.statusCommunityPreviewBanner.height = 180 + preview.statusCommunityPreviewBanner.url = "https://picsum.photos/320/180?random=1" + preview.statusCommunityPreviewBanner.dataUri = "" + return preview + } + + function getChannelLinkPreview() { + const preview = Object.assign({}, emptyObject) + preview.url = "https://status.app/c/ixiACjAKDlRlc3QgQ29tbXVuaXR5Eg9PcGVuIGZvciBhbnlvbmUYdiIHI0ZGMDAwMCoCHwkD#zQ3shnd55dNx9yTihuL6XMbmyM6UNjzU6jk77h5Js31jxcT5V" + preview.previewType = 4 + preview.statusCommunityChannelPreview = {} + preview.statusCommunityChannelPreview.channelUuid = "zQ3shnd55dNx9yTihuL6XMbmyM6UNjzU6jk77h5Js31jxcT5V" + preview.statusCommunityChannelPreview.emoji = "👋" + preview.statusCommunityChannelPreview.displayName = "general" + preview.statusCommunityChannelPreview.description = "Test channel description" + preview.statusCommunityChannelPreview.color = "#122456" + preview.statusCommunityChannelCommunityPreview = {} + preview.statusCommunityChannelCommunityPreview.communityId = "zQ3shnd55dNx9yTihuL6XMbmyM6UNjzU6jk77h5Js31jxcT5V" + preview.statusCommunityChannelCommunityPreview.displayName = "Doodles" + preview.statusCommunityChannelCommunityPreview.description = "Test community description" + preview.statusCommunityChannelCommunityPreview.membersCount = 10 + preview.statusCommunityChannelCommunityPreview.color = "#123456" + preview.statusCommunityChannelCommunityPreviewIcon = {} + preview.statusCommunityChannelCommunityPreviewIcon.width = 64 + preview.statusCommunityChannelCommunityPreviewIcon.height = 64 + preview.statusCommunityChannelCommunityPreviewIcon.url = "https://picsum.photos/64/64?random=1" + preview.statusCommunityChannelCommunityPreviewIcon.dataUri = "" + preview.statusCommunityChannelCommunityPreviewBanner = {} + preview.statusCommunityChannelCommunityPreviewBanner.width = 320 + preview.statusCommunityChannelCommunityPreviewBanner.height = 180 + preview.statusCommunityChannelCommunityPreviewBanner.url = "https://picsum.photos/320/180?random=1" + preview.statusCommunityChannelCommunityPreviewBanner.dataUri = "" + return preview + } + + function getContactLinkPreview() { + const preview = Object.assign({}, emptyObject) + preview.url = "https://status.app/u/Ow==#zQ3shgmVJjmwwhkfAemjDizYJtv9nzot7QD4iRJ52ZkgdU6Ci" + preview.previewType = 2 + preview.statusContactPreview = {} + preview.statusContactPreview.publicKey = "zQ3shgmVJjmwwhkfAemjDizYJtv9nzot7QD4iRJ52ZkgdU6Ci" + preview.statusContactPreview.displayName = "Test contact display name" + preview.statusContactPreview.description = "Test description" + preview.statusContactPreviewThumbnail = {} + preview.statusContactPreviewThumbnail.width = 64 + preview.statusContactPreviewThumbnail.height = 64 + preview.statusContactPreviewThumbnail.url = "https://picsum.photos/64/64?random=1" + preview.statusContactPreviewThumbnail.dataUri = "" + return preview + } + + readonly property var emptyObject: { + "unfurled": true, + "immutable": false, + "empty": false, + "url": "", + "previewType": 1, + "standardPreview": {}, + "standardPreviewThumbnail": {}, + "statusContactPreview": {}, + "statusContactPreviewThumbnail": {}, + "statusCommunityPreview": {}, + "statusCommunityPreviewIcon": {}, + "statusCommunityPreviewBanner": {}, + "statusCommunityChannelPreview": {}, + "statusCommunityChannelCommunityPreview": {}, + "statusCommunityChannelCommunityPreviewIcon": {}, + "statusCommunityChannelCommunityPreviewBanner": {}, + } + + // Create the model dynamically, because `ListElement` doesnt suppport nested elements + Component.onCompleted: { + append(getStandardLinkPreview()) + append(getImageLinkPreview()) + append(getCommunityLinkPreview()) + append(getChannelLinkPreview()) + append(getContactLinkPreview()) + } +} diff --git a/storybook/src/Models/qmldir b/storybook/src/Models/qmldir index 8d4f18cd30..7e2c826713 100644 --- a/storybook/src/Models/qmldir +++ b/storybook/src/Models/qmldir @@ -6,6 +6,7 @@ ChannelsModel 1.0 ChannelsModel.qml CollectiblesModel 1.0 CollectiblesModel.qml FeesModel 1.0 FeesModel.qml IconModel 1.0 IconModel.qml +LinkPreviewModel 1.0 LinkPreviewModel.qml MintedTokensModel 1.0 MintedTokensModel.qml RecipientModel 1.0 RecipientModel.qml TokenHoldersModel 1.0 TokenHoldersModel.qml diff --git a/ui/StatusQ/src/StatusQ/Core/LocaleUtils.qml b/ui/StatusQ/src/StatusQ/Core/LocaleUtils.qml index 8737ffa1fc..c19ec2b1f5 100644 --- a/ui/StatusQ/src/StatusQ/Core/LocaleUtils.qml +++ b/ui/StatusQ/src/StatusQ/Core/LocaleUtils.qml @@ -80,6 +80,30 @@ QtObject { return num.toLocaleString(locale, 'f', precision) } + function numberToLocaleStringInCompactForm(num, locale = null) { + locale = locale || Qt.locale() + const numberOfDigits = integralPartLength(num) + let oneArgStrFormat = "%1" + let formattedNumber = num + let multiplier = 1 + if(numberOfDigits >=4 && numberOfDigits < 7) { // 1K - 999K + multiplier = 1 / 1000 + oneArgStrFormat = qsTr("%1K", "Thousand") + } else if(numberOfDigits >= 7 && numberOfDigits < 10) { // 1M - 999M + multiplier = 1 / 1000000 + oneArgStrFormat = qsTr("%1M", "Million") + } else if(numberOfDigits >= 10 && numberOfDigits < 13) { // 1B - 999B + multiplier = 1 / 1000000000 + oneArgStrFormat = qsTr("%1B", "Billion") + } else if(numberOfDigits >= 13 && numberOfDigits < 16) { // 1T - 999T + multiplier = 1 / 1000000000000 + oneArgStrFormat = qsTr("%1T", "Trillion") + } + + const stringNumber = numberToLocaleString(num * multiplier, 2, locale) + return oneArgStrFormat.arg(stripTrailingZeroes(stringNumber, locale)) + } + function numberFromLocaleString(num, locale = null) { locale = locale || Qt.locale() try { diff --git a/ui/StatusQ/src/assets/img/icons/active-members.svg b/ui/StatusQ/src/assets/img/icons/active-members.svg new file mode 100644 index 0000000000..3870e6a58e --- /dev/null +++ b/ui/StatusQ/src/assets/img/icons/active-members.svg @@ -0,0 +1,3 @@ + + + diff --git a/ui/StatusQ/tests/TestUtils/tst_test-LocaleUtils.qml b/ui/StatusQ/tests/TestUtils/tst_test-LocaleUtils.qml index 86bd88b786..52c3d39181 100644 --- a/ui/StatusQ/tests/TestUtils/tst_test-LocaleUtils.qml +++ b/ui/StatusQ/tests/TestUtils/tst_test-LocaleUtils.qml @@ -123,4 +123,168 @@ TestCase { compare(LocaleUtils.currencyAmountToLocaleString( data.amount, null, locale), data.amountString) } + + function test_numberToLocaleStringInCompactForm_data() { + return [ + { + amount: NaN, + amountString: "nan" + }, + { + amount: null, + amountString: "0" + }, + { + amount: "", + amountString: "0" + }, + { + amount: "string", + amountString: "nan" + }, + { + amount: {}, + amountString: "nan" + }, + { + amount: -1, + amountString: "-1" + }, + { + amount: -1.1, + amountString: "-1.1" + }, + { + amount: -1.1234, + amountString: "-1.12" + }, + { + amount: -1000, + amountString: "-1K" + }, + { + amount: -1000.1, + amountString: "-1K" + }, + { + amount: -100000, + amountString: "-100K" + }, + { + amount: -1001, + amountString: "-1K" + }, + { + amount: -1100, + amountString: "-1.1K" + }, + { + amount: -1000000, + amountString: "-1M" + }, + { + amount: -1100000.123, + amountString: "-1.1M" + }, + { + amount: -1000000000, + amountString: "-1B" + }, + { + amount: -1100000000, + amountString: "-1.1B" + }, + { + amount: -1000000000.123, + amountString: "-1B" + }, + { + amount: -1000000000000, + amountString: "-1T" + }, + { + amount: -999000000000000, + amountString: "-999T" + }, + { + amount: -1000000000000000, + amountString: "-1,000,000,000,000,000" + }, + { + amount: 0, + amountString: "0" + }, + { + amount: 1, + amountString: "1" + }, + { + amount: 1.1, + amountString: "1.1" + }, + { + amount: 1.1234, + amountString: "1.12" + }, + { + amount: 1000, + amountString: "1K" + }, + { + amount: 1000.1, + amountString: "1K" + }, + { + amount: 100000, + amountString: "100K" + }, + { + amount: 1001, + amountString: "1K" + }, + { + amount: 1100, + amountString: "1.1K" + }, + { + amount: 1000000, + amountString: "1M" + }, + { + amount: 1100000.123, + amountString: "1.1M" + }, + { + amount: 1000000000, + amountString: "1B" + }, + { + amount: 1100000000, + amountString: "1.1B" + }, + { + amount: 1000000000.123, + amountString: "1B" + }, + { + amount: 1000000000000, + amountString: "1T" + }, + { + amount: 999000000000000, + amountString: "999T" + }, + { + amount: 1000000000000000, + amountString: "1,000,000,000,000,000" + } + ] + } + + function test_numberToLocaleStringInCompactForm(data) { + const locale = Qt.locale("en_US") + + compare(LocaleUtils.numberToLocaleStringInCompactForm( + data.amount, locale), data.amountString) + } } diff --git a/ui/app/mainui/AppMain.qml b/ui/app/mainui/AppMain.qml index 8279044fee..a6a6795f8e 100644 --- a/ui/app/mainui/AppMain.qml +++ b/ui/app/mainui/AppMain.qml @@ -277,6 +277,10 @@ Item { popups.openConfirmExternalLinkPopup(link, domain) } + function onActivateDeepLink(link: string) { + appMain.rootStore.mainModuleInst.activateStatusDeepLink(link) + } + function onPlaySendMessageSound() { sendMessageSound.stop() sendMessageSound.play() diff --git a/ui/imports/shared/controls/chat/ChatInputLinksPreviewArea.qml b/ui/imports/shared/controls/chat/ChatInputLinksPreviewArea.qml index a380b5b621..b897b8b196 100644 --- a/ui/imports/shared/controls/chat/ChatInputLinksPreviewArea.qml +++ b/ui/imports/shared/controls/chat/ChatInputLinksPreviewArea.qml @@ -6,7 +6,7 @@ import QtQuick.Layouts 1.15 import StatusQ.Core 0.1 import shared.status 1.0 -import shared.controls.chat 1.0 +import shared.controls.delegates 1.0 import utils 1.0 @@ -99,57 +99,8 @@ Control { Repeater { id: linkPreviewRepeater model: d.filteredModel - delegate: LinkPreviewMiniCard { - // Model properties - + delegate: LinkPreviewMiniCardDelegate { required property int index - required property bool unfurled - required property bool empty - required property string url - required property bool immutable - required property int previewType - required property var standardPreview - required property var standardPreviewThumbnail - required property var statusContactPreview - required property var statusContactPreviewThumbnail - required property var statusCommunityPreview - required property var statusCommunityPreviewIcon - required property var statusCommunityPreviewBanner - required property var statusCommunityChannelPreview - required property var statusCommunityChannelCommunityPreview - required property var statusCommunityChannelCommunityPreviewIcon - required property var statusCommunityChannelCommunityPreviewBanner - - readonly property var thumbnail: { - switch (previewType) { - case Constants.Standard: - return standardPreviewThumbnail - case Constants.StatusContact: - return statusContactPreviewThumbnail - case Constants.StatusCommunity: - return statusCommunityPreviewIcon - case Constants.StatusCommunityChannel: - return statusCommunityChannelCommunityPreviewIcon - } - } - - readonly property string thumbnailUrl: thumbnail ? thumbnail.url : "" - readonly property string thumbnailDataUri: thumbnail ? thumbnail.dataUri : "" - - - Layout.preferredHeight: 64 - - titleStr: standardPreview ? standardPreview.title : statusContactPreview ? statusContactPreview.displayName : "" - domain: standardPreview ? standardPreview.hostname : "" //TODO: use domain when available - favIconUrl: "" //TODO: use favicon when available - communityName: statusCommunityPreview ? statusCommunityPreview.displayName : "" - channelName: statusCommunityChannelPreview ? statusCommunityChannelPreview.displayName : "" - - thumbnailImageUrl: thumbnailUrl.length > 0 ? thumbnailUrl : thumbnailDataUri - type: getCardType(previewType, standardPreview) - previewState: unfurled && !empty ? LinkPreviewMiniCard.State.Loaded : - unfurled && empty ? LinkPreviewMiniCard.State.LoadingFailed : - !unfurled ? LinkPreviewMiniCard.State.Loading : LinkPreviewMiniCard.State.Invalid onClose: root.dismissLinkPreview(d.filteredModel.mapToSource(index)) onRetry: root.linkReload(url) diff --git a/ui/imports/shared/controls/chat/LinkPreviewCard.qml b/ui/imports/shared/controls/chat/LinkPreviewCard.qml index 6f6ef6722b..0e11ea4b8c 100644 --- a/ui/imports/shared/controls/chat/LinkPreviewCard.qml +++ b/ui/imports/shared/controls/chat/LinkPreviewCard.qml @@ -6,26 +6,23 @@ import StatusQ.Core 0.1 import StatusQ.Core.Theme 0.1 import StatusQ.Components 0.1 +import shared.controls 1.0 import shared.status 1.0 import utils 1.0 +import "./private" + CalloutCard { id: root - - /// The title of the callout card - required property string title - required property string description - required property string footer + + readonly property LinkData linkData: LinkData { } + readonly property UserData userData: UserData { } + readonly property CommunityData communityData: CommunityData { } + readonly property ChannelData channelData: ChannelData { } + + property int type: Constants.LinkPreviewType.NoPreview property bool highlight: false - - property StatusAssetSettings logoSettings: StatusAssetSettings { - width: 28 - height: 28 - bgRadius: bgWidth / 2 - } - - property string bannerImageSource: "" signal clicked(var mouse) @@ -41,71 +38,103 @@ CalloutCard { } contentItem: ColumnLayout { - StatusImage { - id: bannerImage + Loader { + id: bannerImageLoader Layout.fillWidth: true Layout.leftMargin: d.bannerImageMargins Layout.rightMargin: d.bannerImageMargins Layout.topMargin: d.bannerImageMargins Layout.preferredHeight: 170 - asynchronous: true - source: root.bannerImageSource - fillMode: Image.PreserveAspectCrop - layer.enabled: true - layer.effect: root.clippingEffect + active: !!d.bannerImageSource + sourceComponent: StatusImage { + id: bannerImage + asynchronous: true + source: d.bannerImageSource + fillMode: Image.PreserveAspectCrop + layer.enabled: true + layer.effect: root.clippingEffect + } } ColumnLayout { Layout.fillWidth: true Layout.fillHeight: true Layout.margins: 12 - + Loader { + id: userImageLoader + Layout.alignment: Qt.AlignLeft | Qt.AlignVCenter + Layout.topMargin: 8 + Layout.bottomMargin: 8 + visible: active + active: root.type === Constants.LinkPreviewType.StatusContact + sourceComponent: UserImage { + interactive: false + imageWidth: 58 + imageHeight: imageWidth + ensVerified: root.userData.ensVerified + name: root.userData.name + pubkey: root.userData.publicKey + image: root.userData.image + } + } RowLayout { + id: titleLayout Layout.fillWidth: true Layout.fillHeight: true - Layout.maximumHeight: root.description.length ? 28 : 72 + Layout.maximumHeight: description.text.length ? 28 : 72 + Layout.minimumHeight: 18 StatusSmartIdenticon { - Layout.alignment: Qt.AlignLeft | Qt.AlignTop + id: logo + Layout.alignment: Qt.AlignTop Layout.preferredWidth: 28 Layout.preferredHeight: 28 - asset: root.logoSettings - name: root.logoSettings.name - visible: !!root.logoSettings.name.length || !!root.logoSettings.emoji.length + asset.width: width + asset.height: height + visible: false } StatusBaseText { - text: root.title + id: title + // One line centered next to the logo + // Two or more lines, or no logo, top aligned + readonly property bool centerText: lineCount == 1 && height === logo.height && logo.visible Layout.fillWidth: true Layout.fillHeight: true + Layout.alignment: Qt.AlignTop + Layout.topMargin: verticalAlignment === Text.AlignTop && contentHeight < logo.height ? (logo.height - contentHeight) / 2 : 0 font.pixelSize: 13 font.weight: Font.Medium wrapMode: Text.Wrap elide: Text.ElideRight - verticalAlignment: Text.AlignVCenter + verticalAlignment: centerText ? Text.AlignVCenter : Text.AlignTop + visible: text.length } } + EmojiHash { + Layout.topMargin: 4 + Layout.bottomMargin: 6 + visible: root.type === Constants.LinkPreviewType.StatusContact + publicKey: root.userData.publicKey + oneRow: true + } StatusBaseText { + id: description Layout.fillWidth: true Layout.fillHeight: true - text: root.description font.pixelSize: 12 wrapMode: Text.Wrap elide: Text.ElideRight color: Theme.palette.baseColor1 - visible: root.description.length + visible: description.text.length } - StatusBaseText { - id: linkSite + Loader { + id: footerLoader Layout.fillWidth: true - text: root.footer - font.pixelSize: 12 - lineHeight: 16 - lineHeightMode: Text.FixedHeight - color: Theme.palette.baseColor1 - elide: Text.ElideRight - verticalAlignment: Text.AlignBottom - textFormat: Text.RichText + visible: active + sourceComponent: FooterText { + } } } } + MouseArea { anchors.fill: root hoverEnabled: true @@ -114,9 +143,151 @@ CalloutCard { onClicked: root.clicked(mouse) } + component FooterText: StatusBaseText { + font.pixelSize: 12 + lineHeight: 16 + lineHeightMode: Text.FixedHeight + color: Theme.palette.baseColor1 + elide: Text.ElideRight + wrapMode: Text.Wrap + verticalAlignment: Text.AlignBottom + text: root.linkData.domain + maximumLineCount: 1 + } + + Component { + id: channelFooterComponent + RowLayout { + spacing: 4 + FooterText { + Layout.fillHeight: true + text: qsTr("Channel in") + verticalAlignment: Text.AlignVCenter + } + StatusRoundedImage { + Layout.preferredHeight: 16 + Layout.preferredWidth: height + image.source: channelData.communityData.image + } + FooterText { + Layout.fillHeight: true + Layout.fillWidth: true + text: channelData.communityData.name + verticalAlignment: Text.AlignVCenter + color: Theme.palette.directColor1 + } + } + } + + Component { + id: communityFooterComponent + RowLayout { + spacing: 2 + StatusIcon { + icon: "group" + color: Theme.palette.directColor1 + width: 16 + height: width + } + FooterText { + Layout.fillHeight: true + Layout.fillWidth: communityData.activeMembersCount === -1 //TODO: remove magic number once we have activeMembersCount + color: Theme.palette.directColor1 + text: LocaleUtils.numberToLocaleStringInCompactForm(communityData.membersCount) + verticalAlignment: Text.AlignVCenter + } + StatusIcon { + Layout.leftMargin: 6 + icon: "active-members" + color: Theme.palette.directColor1 + width: 16 + height: width + visible: communityData.activeMembersCount > -1 + } + FooterText { + Layout.fillWidth: true + Layout.fillHeight: true + color: Theme.palette.directColor1 + text: LocaleUtils.numberToLocaleStringInCompactForm(communityData.activeMembersCount) + verticalAlignment: Text.AlignVCenter + visible: communityData.activeMembersCount > -1 + } + } + } + + //behavior + states: [ + State { + name: "noPreview" + when: root.type === Constants.LinkPreviewType.NoPreview + PropertyChanges { target: root; visible: false } + }, + State { + name: "linkPreview" + when: root.type === Constants.LinkPreviewType.Standard + PropertyChanges { + target: logo + visible: !!root.linkData.image + name: root.linkData.domain + asset.name: root.linkData.image + asset.isImage: !!root.linkData.image + asset.color: Theme.palette.baseColor2 + } + PropertyChanges { target: bannerImageLoader; visible: true } + PropertyChanges { target: title; text: root.linkData.title } + PropertyChanges { target: description; text: root.linkData.description } + PropertyChanges { target: d; bannerImageSource: root.linkData.thumbnail } + }, + State { + name: "community" + when: root.type === Constants.LinkPreviewType.StatusCommunity + PropertyChanges { + target: logo + visible: true + name: root.communityData.name + asset.name: root.communityData.image + asset.isImage: !!root.communityData.image + asset.color: root.communityData.color + } + PropertyChanges { target: bannerImageLoader; visible: true } + PropertyChanges { target: title; text: root.communityData.name } + PropertyChanges { target: description; text: root.communityData.description } + PropertyChanges { target: d; bannerImageSource: root.communityData.banner } + PropertyChanges { target: footerLoader; active: true; visible: true; sourceComponent: communityFooterComponent } + }, + State { + name: "channel" + when: root.type === Constants.LinkPreviewType.StatusCommunityChannel + PropertyChanges { + target: logo + visible: true + name: root.channelData.name + asset.name: "" + asset.isImage: false + asset.color: root.channelData.color + asset.emoji: root.channelData.emoji + } + PropertyChanges { target: bannerImageLoader; visible: true } + PropertyChanges { target: title; text: "#" + root.channelData.name } + PropertyChanges { target: description; text: root.channelData.description || root.channelData.communityData.description } + PropertyChanges { target: d; bannerImageSource: root.channelData.communityData.banner } + PropertyChanges { target: footerLoader; active: true; visible: true; sourceComponent: channelFooterComponent } + }, + State { + name: "contact" + when: root.type === Constants.LinkPreviewType.StatusContact + PropertyChanges { target: root; implicitHeight: 187 } + PropertyChanges { target: bannerImageLoader; visible: false } + PropertyChanges { target: footerLoader; active: false; visible: !root.userData.bio; Layout.fillHeight: true } + PropertyChanges { target: title; text: root.userData.name } + PropertyChanges { target: description; text: root.userData.bio; Layout.minimumHeight: 32; visible: true } + } + ] + QtObject { id: d property real bannerImageMargins: 1 / Screen.devicePixelRatio // image size isn't pixel perfect.. property bool highlight: root.highlight || root.hovered + property string bannerImageSource: "" } } diff --git a/ui/imports/shared/controls/chat/LinkPreviewMiniCard.qml b/ui/imports/shared/controls/chat/LinkPreviewMiniCard.qml index f8a8bcbc58..e9da236658 100644 --- a/ui/imports/shared/controls/chat/LinkPreviewMiniCard.qml +++ b/ui/imports/shared/controls/chat/LinkPreviewMiniCard.qml @@ -10,6 +10,8 @@ import shared 1.0 import utils 1.0 +import "./private" 1.0 + CalloutCard { id: root @@ -20,45 +22,11 @@ CalloutCard { Loaded } - enum Type { - Unknown = 0, - Link, - Image, - Community, - Channel, - User - } + readonly property LinkData linkData: LinkData { } + readonly property UserData userData: UserData { } + readonly property CommunityData communityData: CommunityData { } + readonly property ChannelData channelData: ChannelData { } - function getCardType(previewType, standardLinkPreview) { - switch (previewType) { - case Constants.StatusContact: - return LinkPreviewMiniCard.Type.User - case Constants.StatusCommunity: - return LinkPreviewMiniCard.Type.Community - case Constants.StatusCommunityChannel: - return LinkPreviewMiniCard.Type.Channel - case Constants.Standard: - if (!standardLinkPreview) - return LinkPreviewMiniCard.Type.Unknown - switch (standardLinkPreview.linkType) { - case Constants.StandardLinkPreviewType.Link: - return LinkPreviewMiniCard.Type.Link - case Constants.StandardLinkPreviewType.Image: - return LinkPreviewMiniCard.Type.Image - default: - return LinkPreviewMiniCard.Type.Unknown - } - default: - return LinkPreviewMiniCard.Type.Unknown - } - } - - required property string titleStr - required property string domain - required property string communityName - required property string channelName - required property url favIconUrl - required property url thumbnailImageUrl required property int previewState required property int type @@ -106,47 +74,78 @@ CalloutCard { }, State { name: "loaded" - when: root.previewState === LinkPreviewMiniCard.State.Loaded && root.type === LinkPreviewMiniCard.Type.Link + when: root.previewState === LinkPreviewMiniCard.State.Loaded && + root.type === Constants.LinkPreviewType.Standard && + root.linkData.type === Constants.StandardLinkPreviewType.Link PropertyChanges { target: root; visible: true; dashedBorder: false; borderWidth: 0; backgroundColor: root.containsMouse ? Theme.palette.directColor8 : Theme.palette.indirectColor1; borderColor: backgroundColor; } PropertyChanges { target: loadingAnimation; visible: false; } - PropertyChanges { target: titleText; text: root.titleStr; color: Theme.palette.directColor1 } - PropertyChanges { target: subtitleText; visible: true; } + PropertyChanges { target: titleText; text: root.linkData.title; color: Theme.palette.directColor1 } + PropertyChanges { target: subtitleText; visible: true; text: root.linkData.domain; } PropertyChanges { target: reloadButton; visible: false; } - PropertyChanges { target: favIcon; visible: true } - }, - State { - name: "loadedImage" - when: root.previewState === LinkPreviewMiniCard.State.Loaded && root.type === LinkPreviewMiniCard.Type.Image - extend: "loaded" - PropertyChanges { target: thumbnailImage; visible: root.thumbnailImageUrl != "" } - PropertyChanges { target: favIcon; visible: true; name: root.domain; asset.isLetterIdenticon: true; asset.color: Theme.palette.baseColor2; } - }, - State { - name: "loadedCommunity" - when: root.previewState === LinkPreviewMiniCard.State.Loaded && root.type === LinkPreviewMiniCard.Type.Community - extend: "loaded" - PropertyChanges { target: titleText; text: root.communityName; } - }, - State { - name: "loadedChannel" - when: root.previewState === LinkPreviewMiniCard.State.Loaded && root.type === LinkPreviewMiniCard.Type.Channel - extend: "loaded" - PropertyChanges { target: titleText; text: root.communityName; Layout.fillWidth: false; Layout.maximumWidth: Math.min(92, implicitWidth); } - PropertyChanges { target: secondTitleText; text: root.channelName; visible: true; } - }, - State { - name: "loadedUser" - when: root.previewState === LinkPreviewMiniCard.State.Loaded && root.type === LinkPreviewMiniCard.Type.User - extend: "loaded" PropertyChanges { target: favIcon visible: true - name: root.titleStr - asset.isLetterIdenticon: true + name: root.linkData.title + asset.isLetterIdenticon: !root.linkData.image + asset.color: Theme.palette.baseColor2 + } + }, + State { + name: "loadedImage" + when: root.previewState === LinkPreviewMiniCard.State.Loaded && + root.type === Constants.LinkPreviewType.Standard && + root.linkData.type === Constants.StandardLinkPreviewType.Image + extend: "loaded" + PropertyChanges { target: thumbnailImage; visible: root.linkData.thumbnail != ""; image.source: root.linkData.thumbnail; } + PropertyChanges { target: favIcon; visible: true; name: root.linkData.domain; asset.isLetterIdenticon: true; asset.color: Theme.palette.baseColor2; } + PropertyChanges { target: subtitleText; visible: true; text: root.linkData.domain; } + }, + State { + name: "loadedCommunity" + when: root.previewState === LinkPreviewMiniCard.State.Loaded && root.type === Constants.LinkPreviewType.StatusCommunity + extend: "loaded" + PropertyChanges { target: titleText; text: root.communityData.name; } + PropertyChanges { target: subtitleText; visible: true; text: Constants.externalStatusLink; } + PropertyChanges { + target: favIcon + visible: true + name: root.communityData.name + asset.isLetterIdenticon: root.communityData.image.length === 0 + asset.color: root.communityData.color + asset.name: root.communityData.image + } + }, + State { + name: "loadedChannel" + when: root.previewState === LinkPreviewMiniCard.State.Loaded && root.type === Constants.LinkPreviewType.StatusCommunityChannel + extend: "loadedCommunity" + PropertyChanges { target: titleText; text: root.channelData.communityData.name; Layout.fillWidth: false; Layout.maximumWidth: Math.min(92, implicitWidth); } + PropertyChanges { target: secondTitleText; text: "#" + root.channelData.name; visible: true; } + PropertyChanges { + target: favIcon + visible: true + name: root.channelData.communityData.name + asset.isLetterIdenticon: root.channelData.communityData.image.length === 0 + asset.color: root.channelData.communityData.color + asset.name: root.channelData.communityData.image + } + }, + State { + name: "loadedUser" + when: root.previewState === LinkPreviewMiniCard.State.Loaded && root.type === Constants.LinkPreviewType.StatusContact + extend: "loaded" + PropertyChanges { target: titleText; text: root.userData.name; Layout.fillWidth: false; Layout.maximumWidth: Math.min(92, implicitWidth); } + PropertyChanges { target: subtitleText; visible: true; text: Constants.externalStatusLink; } + PropertyChanges { + target: favIcon + visible: true + name: root.userData.name + asset.name: root.userData.image + asset.isLetterIdenticon: root.userData.image.length === 0 asset.charactersLen: 2 asset.color: Theme.palette.miscColor9 } @@ -174,8 +173,6 @@ CalloutCard { Layout.preferredWidth: 16 Layout.preferredHeight: 16 visible: false - name: root.titleStr - asset.name: root.favIconUrl asset.letterSize: asset.charactersLen == 1 ? 10 : 7 } ColumnLayout { @@ -190,7 +187,6 @@ CalloutCard { spacing: 0 StatusBaseText { id: titleText - text: root.titleStr Layout.fillWidth: true Layout.fillHeight: true font.pixelSize: Style.current.additionalTextSize @@ -227,7 +223,6 @@ CalloutCard { Layout.fillHeight: true font.pixelSize: Style.current.tertiaryTextFontSize color: Theme.palette.baseColor1 - text: root.domain wrapMode: Text.WordWrap elide: Text.ElideRight } @@ -239,7 +234,6 @@ CalloutCard { implicitWidth: 34 implicitHeight: 34 radius: 4 - image.source: root.thumbnailImageUrl visible: false } StatusFlatButton { diff --git a/ui/imports/shared/controls/chat/UserProfileCard.qml b/ui/imports/shared/controls/chat/UserProfileCard.qml deleted file mode 100644 index 037e0df933..0000000000 --- a/ui/imports/shared/controls/chat/UserProfileCard.qml +++ /dev/null @@ -1,78 +0,0 @@ -import QtQuick 2.15 -import QtQuick.Layouts 1.15 - -import StatusQ.Core 0.1 -import StatusQ.Core.Theme 0.1 - -import shared.status 1.0 -import shared.controls 1.0 -import shared.controls.chat 1.0 - -import utils 1.0 - - -CalloutCard { - id: root - - required property string userName - required property string userPublicKey - required property string userBio - required property var userImage - required property bool ensVerified - - signal clicked() - - implicitWidth: 305 - implicitHeight: 187 - - padding: 12 - - contentItem: ColumnLayout { - spacing: 0 - UserImage { - Layout.alignment: Qt.AlignLeft | Qt.AlignVCenter - name: root.userName - pubkey: root.userPublicKey - image: root.userImage - interactive: false - imageWidth: 58 - imageHeight: imageWidth - ensVerified: root.ensVerified - } - - StatusBaseText { - id: contactName - Layout.fillWidth: true - Layout.topMargin: 12 - font.pixelSize: Style.current.additionalTextSize - font.weight: Font.Medium - elide: Text.ElideRight - text: root.userName - } - - EmojiHash { - Layout.fillWidth: true - Layout.topMargin: 4 - publicKey: root.userPublicKey - oneRow: true - } - - StatusBaseText { - Layout.fillWidth: true - Layout.fillHeight: true - Layout.topMargin: 15 - font.pixelSize: Style.current.tertiaryTextFontSize - color: Theme.palette.baseColor1 - text: root.userBio - wrapMode: Text.WordWrap - elide: Text.ElideRight - } - } - - MouseArea { - anchors.fill: root - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onClicked: root.clicked() - } -} \ No newline at end of file diff --git a/ui/imports/shared/controls/chat/private/ChannelData.qml b/ui/imports/shared/controls/chat/private/ChannelData.qml new file mode 100644 index 0000000000..f54e82b042 --- /dev/null +++ b/ui/imports/shared/controls/chat/private/ChannelData.qml @@ -0,0 +1,9 @@ +import QtQuick 2.15 + +QtObject { + property string name + property string description + property string emoji + property string color + readonly property CommunityData communityData: CommunityData {} +} diff --git a/ui/imports/shared/controls/chat/private/CommunityData.qml b/ui/imports/shared/controls/chat/private/CommunityData.qml new file mode 100644 index 0000000000..b70d7c3db5 --- /dev/null +++ b/ui/imports/shared/controls/chat/private/CommunityData.qml @@ -0,0 +1,11 @@ +import QtQuick 2.15 + +QtObject { + property string name + property string description + property string banner + property string image + property string color + property int membersCount + property int activeMembersCount: -1 // TODO: implement this and remove the magic number +} diff --git a/ui/imports/shared/controls/chat/private/LinkData.qml b/ui/imports/shared/controls/chat/private/LinkData.qml new file mode 100644 index 0000000000..d75f35c5ac --- /dev/null +++ b/ui/imports/shared/controls/chat/private/LinkData.qml @@ -0,0 +1,10 @@ +import QtQuick 2.15 + +QtObject { + property string title + property string description + property string domain + property string thumbnail + property string image + property int type +} diff --git a/ui/imports/shared/controls/chat/private/UserData.qml b/ui/imports/shared/controls/chat/private/UserData.qml new file mode 100644 index 0000000000..437215277d --- /dev/null +++ b/ui/imports/shared/controls/chat/private/UserData.qml @@ -0,0 +1,9 @@ +import QtQuick 2.15 + +QtObject { + property string name + property string publicKey + property string bio + property string image + property bool ensVerified +} diff --git a/ui/imports/shared/controls/chat/qmldir b/ui/imports/shared/controls/chat/qmldir index 5d683d5261..4068bec001 100644 --- a/ui/imports/shared/controls/chat/qmldir +++ b/ui/imports/shared/controls/chat/qmldir @@ -19,6 +19,5 @@ SendTransactionButton 1.0 SendTransactionButton.qml SentMessage 1.0 SentMessage.qml StateBubble 1.0 StateBubble.qml UserImage 1.0 UserImage.qml -UserProfileCard 1.0 UserProfileCard.qml UsernameLabel 1.0 UsernameLabel.qml VerificationLabel 1.0 VerificationLabel.qml diff --git a/ui/imports/shared/controls/delegates/LinkPreviewCardDelegate.qml b/ui/imports/shared/controls/delegates/LinkPreviewCardDelegate.qml new file mode 100644 index 0000000000..b0af202721 --- /dev/null +++ b/ui/imports/shared/controls/delegates/LinkPreviewCardDelegate.qml @@ -0,0 +1,91 @@ +import QtQuick 2.15 + +import StatusQ.Core 0.1 +import StatusQ.Core.Theme 0.1 + +import utils 1.0 + +import shared.controls.chat 1.0 + +LinkPreviewCard { + id: root + + /* + * Model properties + * The following properties are required to be set by the user of this component + * unfurled: Whether the link has been unfurled or not + * empty: Whether the link preview is empty or not + * url: The url of the link + * immutable: Whether the link preview can be updated + * previewType: The type of the preview. See Constants.LinkPreviewType + * standardPreview: The standard preview data (title, description, linkType, hostname) + * standardPreviewThumbnail: The standard preview thumbnail data (url, dataUri) + * statusContactPreview: The status contact preview data (displayName, publicKey, description, icon) + * statusContactPreviewThumbnail: The status contact preview thumbnail data (url, dataUri) + * statusCommunityPreview: The status community preview data (communityId, displayName, description, membersCount, color) + * statusCommunityPreviewIcon: The status community preview icon data (url, dataUri) + * statusCommunityPreviewBanner: The status community preview banner data (url, dataUri) + * statusCommunityChannelPreview: The status community channel preview data (channelId, displayName, description, emoji, color) + * statusCommunityChannelCommunityPreview: The status community channel community preview data (communityId, displayName, description, membersCount, color) + * statusCommunityChannelCommunityPreviewIcon: The status community channel community preview icon data (url, dataUri) + * statusCommunityChannelCommunityPreviewBanner: The status community channel community preview banner data (url, dataUri) + */ + required property bool unfurled + required property bool empty + required property string url + required property bool immutable + required property int previewType + required property var standardPreview + required property var standardPreviewThumbnail + required property var statusContactPreview + required property var statusContactPreviewThumbnail + required property var statusCommunityPreview + required property var statusCommunityPreviewIcon + required property var statusCommunityPreviewBanner + required property var statusCommunityChannelPreview + required property var statusCommunityChannelCommunityPreview + required property var statusCommunityChannelCommunityPreviewIcon + required property var statusCommunityChannelCommunityPreviewBanner + + //View properties + property bool isCurrentUser: false + + leftTail: !isCurrentUser + type: root.previewType + linkData { + title: standardPreview ? standardPreview.title : "" + description: standardPreview ? standardPreview.description : "" + domain: standardPreview ? standardPreview.hostname : "" //TODO: Use domainName when available + thumbnail: standardPreviewThumbnail ? (standardPreviewThumbnail.url || standardPreviewThumbnail.dataUri) || "" : "" + image: "" //TODO: usefavicon when available + } + userData { + name: statusContactPreview ? statusContactPreview.displayName : "" + publicKey: statusContactPreview ? statusContactPreview.publicKey : "" + bio: statusContactPreview ? statusContactPreview.description : "" + image: statusContactPreviewThumbnail ? (statusContactPreviewThumbnail.url || statusContactPreviewThumbnail.dataUri) || "" : "" + ensVerified: false // not supported yet + } + communityData { + name: statusCommunityPreview ? statusCommunityPreview.displayName : "" + description: statusCommunityPreview ? statusCommunityPreview.description : "" + banner: statusCommunityPreviewBanner ? (statusCommunityPreviewBanner.url || statusCommunityPreviewBanner.dataUri) || "" : "" + image: statusCommunityPreviewIcon ? (statusCommunityPreviewIcon.url || statusCommunityPreviewIcon.dataUri) || "" : "" + membersCount: statusCommunityPreview ? statusCommunityPreview.membersCount : 0 + color: statusCommunityPreview ? statusCommunityPreview.color : "" + } + channelData { + name: statusCommunityChannelPreview ? statusCommunityChannelPreview.displayName : "" + description: statusCommunityChannelPreview ? statusCommunityChannelPreview.description : "" + emoji: statusCommunityChannelPreview ? statusCommunityChannelPreview.emoji : "" + color: statusCommunityChannelPreview ? statusCommunityChannelPreview.color : "" + communityData { + name: statusCommunityChannelCommunityPreview ? statusCommunityChannelCommunityPreview.displayName : "" + description: statusCommunityChannelCommunityPreview ? statusCommunityChannelCommunityPreview.description : "" + banner: statusCommunityChannelCommunityPreviewBanner ? (statusCommunityChannelCommunityPreviewBanner.url || statusCommunityChannelCommunityPreviewBanner.dataUri) || "" : "" + image: statusCommunityChannelCommunityPreviewIcon ? (statusCommunityChannelCommunityPreviewIcon.url || statusCommunityChannelCommunityPreviewIcon.dataUri) || "" : "" + membersCount: statusCommunityChannelCommunityPreview ? statusCommunityChannelCommunityPreview.membersCount : 0 + color: statusCommunityChannelCommunityPreview ? statusCommunityChannelCommunityPreview.color : "" + } + } +} diff --git a/ui/imports/shared/controls/delegates/LinkPreviewGifDelegate.qml b/ui/imports/shared/controls/delegates/LinkPreviewGifDelegate.qml new file mode 100644 index 0000000000..c6326f8f0d --- /dev/null +++ b/ui/imports/shared/controls/delegates/LinkPreviewGifDelegate.qml @@ -0,0 +1,80 @@ +import QtQuick 2.15 + +import utils 1.0 + +import shared.controls.chat 1.0 +import shared.status 1.0 + +import StatusQ.Core 0.1 + +CalloutCard { + id: root + + required property string link + required property bool playAnimation + required property bool isOnline + required property bool isCurrentUser + + readonly property bool isPlaying: linkImage.playing + readonly property alias imageAlias: linkImage.imageAlias + + + signal clicked(var mouse) + + implicitWidth: linkImage.width + implicitHeight: linkImage.height + leftTail: !isCurrentUser + + StatusChatImageLoader { + id: linkImage + + property bool localAnimationEnabled: true + + objectName: "LinksMessageView_unfurledImageComponent_linkImage" + anchors.centerIn: parent + source: root.link + imageWidth: 300 + isCurrentUser: root.isCurrentUser + playing: root.playAnimation && localAnimationEnabled + isOnline: root.isOnline + asynchronous: true + isAnimated: true + onClicked: { + if (isAnimated && !playing) + localAnimationEnabled = true + else + root.clicked(mouse) + } + imageAlias.cache: localAnimationEnabled // GIFs can only loop/play properly with cache enabled + Loader { + width: 45 + height: 38 + anchors.left: parent.left + anchors.leftMargin: 12 + anchors.bottom: parent.bottom + anchors.bottomMargin: 12 + active: linkImage.isAnimated && !linkImage.playing + sourceComponent: Item { + anchors.fill: parent + Rectangle { + anchors.fill: parent + color: "black" + radius: Style.current.radius + opacity: .4 + } + StatusBaseText { + anchors.centerIn: parent + text: "GIF" + font.pixelSize: 13 + color: "white" + } + } + } + Timer { + id: animationPlayingTimer + interval: 10000 + running: linkImage.isAnimated && linkImage.playing + onTriggered: { linkImage.localAnimationEnabled = false } + } + } +} diff --git a/ui/imports/shared/controls/delegates/LinkPreviewMiniCardDelegate.qml b/ui/imports/shared/controls/delegates/LinkPreviewMiniCardDelegate.qml new file mode 100644 index 0000000000..4520508736 --- /dev/null +++ b/ui/imports/shared/controls/delegates/LinkPreviewMiniCardDelegate.qml @@ -0,0 +1,86 @@ +import QtQuick 2.15 + +import utils 1.0 +import shared.controls.chat 1.0 + +LinkPreviewMiniCard { + id: root + + /* + * Model properties + * The following properties are required to be set by the user of this component + * unfurled: Whether the link has been unfurled or not + * empty: Whether the link preview is empty or not + * url: The url of the link + * immutable: Whether the link preview can be updated + * previewType: The type of the preview. See Constants.LinkPreviewType + * standardPreview: The standard preview data (title, description, linkType, hostname) + * standardPreviewThumbnail: The standard preview thumbnail data (url, dataUri) + * statusContactPreview: The status contact preview data (displayName, publicKey, description, icon) + * statusContactPreviewThumbnail: The status contact preview thumbnail data (url, dataUri) + * statusCommunityPreview: The status community preview data (communityId, displayName, description, membersCount, color) + * statusCommunityPreviewIcon: The status community preview icon data (url, dataUri) + * statusCommunityPreviewBanner: The status community preview banner data (url, dataUri) + * statusCommunityChannelPreview: The status community channel preview data (channelId, displayName, description, emoji, color) + * statusCommunityChannelCommunityPreview: The status community channel community preview data (communityId, displayName, description, membersCount, color) + * statusCommunityChannelCommunityPreviewIcon: The status community channel community preview icon data (url, dataUri) + * statusCommunityChannelCommunityPreviewBanner: The status community channel community preview banner data (url, dataUri) + */ + required property bool unfurled + required property bool empty + required property string url + required property bool immutable + required property int previewType + required property var standardPreview + required property var standardPreviewThumbnail + required property var statusContactPreview + required property var statusContactPreviewThumbnail + required property var statusCommunityPreview + required property var statusCommunityPreviewIcon + required property var statusCommunityPreviewBanner + required property var statusCommunityChannelPreview + required property var statusCommunityChannelCommunityPreview + required property var statusCommunityChannelCommunityPreviewIcon + required property var statusCommunityChannelCommunityPreviewBanner + + previewState: !root.unfurled ? LinkPreviewMiniCard.State.Loading : root.unfurled && !root.empty ? LinkPreviewMiniCard.State.Loaded : LinkPreviewMiniCard.State.LoadingFailed + type: root.previewType + + linkData { + title: standardPreview ? standardPreview.title : "" + description: standardPreview ? standardPreview.description : "" + domain: standardPreview ? standardPreview.hostname : "" //TODO: Use domainName when available + thumbnail: standardPreviewThumbnail ? (standardPreviewThumbnail.url || standardPreviewThumbnail.dataUri) || "" : "" + image: "" //TODO: usefavicon when available + type: standardPreview ? standardPreview.linkType : Constants.StandardLinkPreviewType.Link + } + userData { + name: statusContactPreview ? statusContactPreview.displayName : "" + publicKey: statusContactPreview ? statusContactPreview.publicKey : "" + bio: statusContactPreview ? statusContactPreview.description : "" + image: statusContactPreviewThumbnail ? (statusContactPreviewThumbnail.url || statusContactPreviewThumbnail.dataUri) || "" : "" + ensVerified: false // not supported yet + } + communityData { + name: statusCommunityPreview ? statusCommunityPreview.displayName : "" + description: statusCommunityPreview ? statusCommunityPreview.description : "" + banner: statusCommunityPreviewBanner ? (statusCommunityPreviewBanner.url || statusCommunityPreviewBanner.dataUri) || "" : "" + image: statusCommunityPreviewIcon ? (statusCommunityPreviewIcon.url || statusCommunityPreviewIcon.dataUri) || "" : "" + membersCount: statusCommunityPreview ? statusCommunityPreview.membersCount : 0 + color: statusCommunityPreview ? statusCommunityPreview.color : "" + } + channelData { + name: statusCommunityChannelPreview ? statusCommunityChannelPreview.displayName : "" + description: statusCommunityChannelPreview ? statusCommunityChannelPreview.description : "" + emoji: statusCommunityChannelPreview ? statusCommunityChannelPreview.emoji : "" + color: statusCommunityChannelPreview ? statusCommunityChannelPreview.color : "" + communityData { + name: statusCommunityChannelCommunityPreview ? statusCommunityChannelCommunityPreview.displayName : "" + description: statusCommunityChannelCommunityPreview ? statusCommunityChannelCommunityPreview.description : "" + banner: statusCommunityChannelCommunityPreviewBanner ? (statusCommunityChannelCommunityPreviewBanner.url || statusCommunityChannelCommunityPreviewBanner.dataUri) || "" : "" + image: statusCommunityChannelCommunityPreviewIcon ? (statusCommunityChannelCommunityPreviewIcon.url || statusCommunityChannelCommunityPreviewIcon.dataUri) || "" : "" + membersCount: statusCommunityChannelCommunityPreview ? statusCommunityChannelCommunityPreview.membersCount : 0 + color: statusCommunityChannelCommunityPreview ? statusCommunityChannelCommunityPreview.color : "" + } + } +} diff --git a/ui/imports/shared/controls/delegates/qmldir b/ui/imports/shared/controls/delegates/qmldir index 76d9aa335d..81e214886b 100644 --- a/ui/imports/shared/controls/delegates/qmldir +++ b/ui/imports/shared/controls/delegates/qmldir @@ -1 +1,4 @@ ContactListItemDelegate 1.0 ContactListItemDelegate.qml +LinkPreviewCardDelegate 1.0 LinkPreviewCardDelegate.qml +LinkPreviewGifDelegate 1.0 LinkPreviewGifDelegate.qml +LinkPreviewMiniCardDelegate 1.0 LinkPreviewMiniCardDelegate.qml diff --git a/ui/imports/shared/status/StatusChatInput.qml b/ui/imports/shared/status/StatusChatInput.qml index 880809f80e..e2220d31d7 100644 --- a/ui/imports/shared/status/StatusChatInput.qml +++ b/ui/imports/shared/status/StatusChatInput.qml @@ -1201,7 +1201,7 @@ Rectangle { } control.fileUrlsAndSources = urls } - onImageClicked: (chatImage) => Global.openImagePopup(chatImage) + onImageClicked: (chatImage) => Global.openImagePopup(chatImage, "") onLinkReload: (link) => control.linkPreviewReloaded(link) onLinkClicked: (link) => Global.openLink(link) onEnableLinkPreview: () => control.enableLinkPreview() diff --git a/ui/imports/shared/views/chat/ImageContextMenu.qml b/ui/imports/shared/views/chat/ImageContextMenu.qml index 8301b40042..4eae7f8118 100644 --- a/ui/imports/shared/views/chat/ImageContextMenu.qml +++ b/ui/imports/shared/views/chat/ImageContextMenu.qml @@ -7,6 +7,8 @@ StatusMenu { property string url property string imageSource + property string domain + property bool requireConfirmationOnOpen: false QtObject { id: d @@ -43,6 +45,6 @@ StatusMenu { text: qsTr("Open link") icon.name: "browser" enabled: d.isUnfurled - onTriggered: Global.openLink(root.url) + onTriggered: requireConfirmationOnOpen ? Global.openLinkWithConfirmation(root.url, root.domain) : Global.openLink(root.url) } } diff --git a/ui/imports/shared/views/chat/LinksMessageView.qml b/ui/imports/shared/views/chat/LinksMessageView.qml index b87f37596a..2e74edbdd4 100644 --- a/ui/imports/shared/views/chat/LinksMessageView.qml +++ b/ui/imports/shared/views/chat/LinksMessageView.qml @@ -10,24 +10,29 @@ import StatusQ.Components 0.1 import shared.controls 1.0 import shared.status 1.0 import shared.panels 1.0 -import shared.stores 1.0 -import shared.controls.chat 1.0 + +import shared.controls.delegates 1.0 Flow { id: root - required property var store - required property var messageStore + required property bool isOnline + required property bool playAnimations required property var linkPreviewModel required property var gifLinks required property bool isCurrentUser + required property bool gifUnfurlingEnabled + required property bool canAskToUnfurlGifs + readonly property alias hoveredLink: linksRepeater.hoveredUrl property string highlightLink: "" - signal imageClicked(var image, var mouse, var imageSource, string url) + signal imageClicked(var image, var mouse, string imageSource, string url) + signal openContextMenu(var item, string url, string domain) + signal setNeverAskAboutUnfurlingAgain(bool neverAskAgain) function resetLocalAskAboutUnfurling() { d.localAskAboutUnfurling = true @@ -45,15 +50,25 @@ Flow { Loader { visible: active active: root.gifLinks && root.gifLinks.length > 0 - && !RootStore.gifUnfurlingEnabled - && d.localAskAboutUnfurling && !RootStore.neverAskAboutUnfurlingAgain + && !root.gifUnfurlingEnabled + && d.localAskAboutUnfurling && root.canAskToUnfurlGifs sourceComponent: enableLinkComponent } Repeater { id: tempRepeater - model: RootStore.gifUnfurlingEnabled ? gifLinks : [] - delegate: gifComponent + visible: root.canAskToUnfurlGifs + model: root.gifUnfurlingEnabled ? gifLinks : [] + + delegate: LinkPreviewGifDelegate { + required property string modelData + + link: modelData + isOnline: root.isOnline + isCurrentUser: root.isCurrentUser + playAnimation: root.playAnimations + onClicked: root.imageClicked(imageAlias, mouse, link, link) + } } Repeater { @@ -62,154 +77,26 @@ Flow { property string hoveredUrl: "" model: root.linkPreviewModel - delegate: Loader { - id: linkMessageLoader - // properties from the model - - required property bool unfurled - required property bool empty - required property string url - required property bool immutable - required property int previewType - required property var standardPreview - required property var standardPreviewThumbnail - required property var statusContactPreview - required property var statusContactPreviewThumbnail - required property var statusCommunityPreview - required property var statusCommunityPreviewIcon - required property var statusCommunityPreviewBanner - required property var statusCommunityChannelPreview - required property var statusCommunityChannelCommunityPreview - required property var statusCommunityChannelCommunityPreviewIcon - required property var statusCommunityChannelCommunityPreviewBanner - - readonly property string hostname: standardPreview ? standardPreview.hostname : "" - readonly property string title: standardPreview ? standardPreview.title : "" - readonly property string description: standardPreview ? standardPreview.description : "" - readonly property int standardLinkType: standardPreview ? standardPreview.linkType : "" - readonly property int thumbnailWidth: standardPreviewThumbnail ? standardPreviewThumbnail.width : "" - readonly property int thumbnailHeight: standardPreviewThumbnail ? standardPreviewThumbnail.height : "" - readonly property string thumbnailUrl: standardPreviewThumbnail ? standardPreviewThumbnail.url : "" - readonly property string thumbnailDataUri: standardPreviewThumbnail ? standardPreviewThumbnail.dataUri : "" - - asynchronous: true - active: unfurled && !empty - - StateGroup { - //Using StateGroup as a warkardound for https://bugreports.qt.io/browse/QTBUG-47796 - states: [ - State { - name: "standardLinkPreview" - when: linkMessageLoader.previewType === Constants.LinkPreviewType.Standard - PropertyChanges { target: linkMessageLoader; sourceComponent: standardLinkPreviewCard } - }, - State { - name: "statusContactLinkPreview" - when: linkMessageLoader.previewType === Constants.LinkPreviewType.StatusContact - PropertyChanges { target: linkMessageLoader; sourceComponent: unfurledProfileLinkComponent } - } - ] + delegate: LinkPreviewCardDelegate { + id: delegate + isCurrentUser: root.isCurrentUser + highlight: url === root.highlightLink + onHoveredChanged: { + linksRepeater.hoveredUrl = hovered ? url : "" } - } - } - - Component { - id: standardLinkPreviewCard - LinkPreviewCard { - leftTail: !root.isCurrentUser // WARNING: Is this by design? - bannerImageSource: standardPreviewThumbnail ? standardPreviewThumbnail.url : "" - title: standardPreview ? standardPreview.title : "" - description: standardPreview ? standardPreview.description : "" - footer: standardPreview ? standardPreview.hostname : "" - highlight: root.highlightLink === url onClicked: (mouse) => { - switch (mouse.button) { - case Qt.RightButton: - root.imageClicked(unfurledLink, mouse, "", url) // request a dumb context menu with just "copy/open link" items - break - default: - Global.openLinkWithConfirmation(url, hostname) - break + if(mouse.button === Qt.RightButton) { + const domain = previewType === Constants.LinkPreviewType.Standard ? linkData.domain : Constants.externalStatusLink + root.openContextMenu(delegate, url, domain) + return } - } - } - } - - Component { - id: unfurledProfileLinkComponent - UserProfileCard { - id: unfurledProfileLink - leftTail: !root.isCurrentUser - userName: statusContactPreview && statusContactPreview.displayName ? statusContactPreview.displayName : "" - userPublicKey: statusContactPreview && statusContactPreview.publicKey ? statusContactPreview.publicKey : "" - userBio: statusContactPreview && statusContactPreview.description ? statusContactPreview.description : "" - userImage: statusContactPreviewThumbnail ? statusContactPreviewThumbnail.url : "" - ensVerified: false // not supported yet - onClicked: { - Global.openProfilePopup(userPublicKey) - } - } - } + if(previewType === Constants.LinkPreviewType.Standard) { + Global.openLinkWithConfirmation(url, linkData.domain) + return + } - //TODO: Remove this once we have gif support in new unfurling flow - Component { - id: gifComponent - CalloutCard { - implicitWidth: linkImage.width - implicitHeight: linkImage.height - leftTail: !root.isCurrentUser - StatusChatImageLoader { - id: linkImage - readonly property bool globalAnimationEnabled: root.messageStore.playAnimation - readonly property string urlLink: modelData - property bool localAnimationEnabled: true - objectName: "LinksMessageView_unfurledImageComponent_linkImage" - anchors.centerIn: parent - source: urlLink - imageWidth: 300 - isCurrentUser: root.isCurrentUser - playing: globalAnimationEnabled && localAnimationEnabled - isOnline: root.store.mainModuleInst.isOnline - asynchronous: true - isAnimated: true - onClicked: { - if (!playing) - localAnimationEnabled = true - else - root.imageClicked(linkImage.imageAlias, mouse, source, urlLink) - } - imageAlias.cache: localAnimationEnabled // GIFs can only loop/play properly with cache enabled - Loader { - width: 45 - height: 38 - anchors.left: parent.left - anchors.leftMargin: 12 - anchors.bottom: parent.bottom - anchors.bottomMargin: 12 - active: linkImage.isAnimated && !linkImage.playing - sourceComponent: Item { - anchors.fill: parent - Rectangle { - anchors.fill: parent - color: "black" - radius: Style.current.radius - opacity: .4 - } - StatusBaseText { - anchors.centerIn: parent - text: "GIF" - font.pixelSize: 13 - color: "white" - } - } - } - Timer { - id: animationPlayingTimer - interval: 10000 - running: linkImage.isAnimated && linkImage.playing - onTriggered: { linkImage.localAnimationEnabled = false } - } + Global.activateDeepLink(url) } } } @@ -308,7 +195,7 @@ Flow { text: qsTr("Don't ask me again") } } - onClicked: RootStore.setNeverAskAboutUnfurlingAgain(true) + onClicked: root.setNeverAskAboutUnfurlingAgain(true) Component.onCompleted: { background.radius = Style.current.padding; } @@ -316,5 +203,5 @@ Flow { } } } - } + diff --git a/ui/imports/shared/views/chat/MessageView.qml b/ui/imports/shared/views/chat/MessageView.qml index 4f270857f5..870a34a0ad 100644 --- a/ui/imports/shared/views/chat/MessageView.qml +++ b/ui/imports/shared/views/chat/MessageView.qml @@ -8,6 +8,7 @@ import shared.controls 1.0 import shared.popups 1.0 import shared.views.chat 1.0 import shared.controls.chat 1.0 +import shared.stores 1.0 import StatusQ.Core 0.1 import StatusQ.Core.Theme 0.1 @@ -570,7 +571,7 @@ Loader { rootStore.chatCommunitySectionModule.switchToChannel(link.replace("#", "")) return } else if (Utils.isStatusDeepLink(link)) { - rootStore.activateStatusDeepLink(link) + Global.activateDeepLink(link) return } @@ -755,14 +756,20 @@ Loader { id: linksMessageView linkPreviewModel: root.linkPreviewModel gifLinks: root.gifLinks - messageStore: root.messageStore - store: root.rootStore + playAnimations: root.messageStore.playAnimation + isOnline: root.rootStore.mainModuleInst.isOnline isCurrentUser: root.amISender highlightLink: delegate.hoveredLink onImageClicked: (image, mouse, imageSource, url) => { d.onImageClicked(image, mouse, imageSource, url) } + onOpenContextMenu: (item, url, domain) => { + Global.openMenu(imageContextMenuComponent, item, { url: url, domain: domain, requireConfirmationOnOpen: true }) + } onHoveredLinkChanged: delegate.highlightedLink = linksMessageView.hoveredLink + gifUnfurlingEnabled: RootStore.gifUnfurlingEnabled + canAskToUnfurlGifs: !RootStore.neverAskAboutUnfurlingAgain + onSetNeverAskAboutUnfurlingAgain: RootStore.setNeverAskAboutUnfurlingAgain(neverAskAgain) } } diff --git a/ui/imports/utils/Global.qml b/ui/imports/utils/Global.qml index 07b799accb..f75814587c 100644 --- a/ui/imports/utils/Global.qml +++ b/ui/imports/utils/Global.qml @@ -56,6 +56,7 @@ QtObject { signal openLink(string link) signal openLinkWithConfirmation(string link, string domain) + signal activateDeepLink(string link) signal setNthEnabledSectionActive(int nthSection) signal appSectionBySectionTypeChanged(int sectionType, int subsection)