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
This commit is contained in:
Alex Jbanca 2023-10-25 18:20:02 +03:00 committed by GitHub
parent 5c4dd60f1e
commit 4a30d13bdc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 1272 additions and 821 deletions

View File

@ -4,6 +4,7 @@ import QtQuick.Layouts 1.15
import QtGraphicalEffects 1.15 import QtGraphicalEffects 1.15
import Storybook 1.0 import Storybook 1.0
import Models 1.0
import StatusQ.Core.Theme 0.1 import StatusQ.Core.Theme 0.1
import shared.controls.chat 1.0 import shared.controls.chat 1.0
@ -30,7 +31,7 @@ SplitView {
anchors.centerIn: parent anchors.centerIn: parent
width: parent.width width: parent.width
imagePreviewArray: ["https://picsum.photos/200/300?random=1", "https://picsum.photos/200/300?random=1"] 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 showLinkPreviewSettings: !linkPreviewEnabledSwitch.checked
visible: hasContent visible: hasContent
@ -78,123 +79,8 @@ SplitView {
id: emptyModel id: emptyModel
} }
ListModel { LinkPreviewModel {
id: linkPreviewListModel id: mockedLinkPreviewModel
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: ""
}
} }
} }

View File

@ -2,16 +2,48 @@ import QtQuick 2.15
import QtQuick.Controls 2.15 import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15 import QtQuick.Layouts 1.15
import Qt.labs.settings 1.0
import StatusQ.Core.Theme 0.1 import StatusQ.Core.Theme 0.1
import shared.controls.chat 1.0 import shared.controls.chat 1.0
import utils 1.0 import utils 1.0
SplitView { SplitView {
id: root id: root
property alias logoSettings: previewCard.logoSettings
property string ytBannerQuality: "hqdefault" 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
// 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 { Pane {
SplitView.fillWidth: true SplitView.fillWidth: true
@ -19,22 +51,96 @@ SplitView {
LinkPreviewCard { LinkPreviewCard {
id: previewCard id: previewCard
bannerImageSource: "https://img.youtube.com/vi/yHN1M7vcPKU/%1.jpg".arg(root.ytBannerQuality) type: 1
linkData {
title: titleInput.text title: titleInput.text
description: descriptionInput.text description: descriptionInput.text
footer: footerInput.text domain: footerInput.text
logoSettings.name: Style.png("tokens/SOCKS") thumbnail: root.banner
logoSettings.isImage: true image: root.image
logoSettings.isLetterIdenticon: false }
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"
}
}
} }
} }
Pane {
ScrollView {
SplitView.preferredWidth: 500 SplitView.preferredWidth: 500
SplitView.fillHeight: true SplitView.fillHeight: true
leftPadding: 10
ColumnLayout { ColumnLayout {
id: layout
spacing: 24 spacing: 24
ColumnLayout { 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 { Label {
text: "Title" text: "Title"
} }
@ -65,7 +171,8 @@ SplitView {
onClicked: descriptionInput.text = "Link description goes here. If blank it will enable multi line title." onClicked: descriptionInput.text = "Link description goes here. If blank it will enable multi line title."
} }
} }
ColumnLayout {
visible: previewCard.type === Constants.LinkPreviewType.Standard
Label { Label {
text: "Footer" text: "Footer"
} }
@ -74,68 +181,115 @@ SplitView {
id: footerInput id: footerInput
Layout.fillHeight: true Layout.fillHeight: true
Layout.fillWidth: true Layout.fillWidth: true
text: footerTypeCommunity.footerRichText text: "X"
} }
} }
ColumnLayout { 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: "User name"
}
TextField {
id: userNameInput
Layout.fillHeight: true
Layout.fillWidth: true
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: "channel settings"
}
CheckBox {
text: qsTr("Emoji")
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 { Label {
Layout.fillWidth: true Layout.fillWidth: true
text: "Logo" text: "Logo"
} }
RadioButton { RadioButton {
text: qsTr("No logo") text: qsTr("no image")
checked: root.logoSettings.name === "" && root.logoSettings.emoji === "" checked: root.image === ""
onToggled: { onToggled: root.image = ""
root.logoSettings.name = ""
root.logoSettings.emoji = ""
root.logoSettings.isImage = false
root.logoSettings.isLetterIdenticon = false
}
} }
RadioButton { 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=" 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") text: qsTr("Raw image")
checked: root.logoSettings.name === rawImageData checked: root.image === rawImageData
onToggled: { onToggled: root.image = rawImageData
root.logoSettings.name = rawImageData
root.logoSettings.isImage = true
root.logoSettings.isLetterIdenticon = false
}
} }
RadioButton { RadioButton {
text: qsTr("QRC asset: SOCKS") text: qsTr("QRC asset: SOCKS")
checked: root.logoSettings.name = Style.png("tokens/SOCKS") checked: root.image === Style.png("tokens/SOCKS")
onToggled:{ onToggled: root.image = Style.png("tokens/SOCKS")
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 {
text: qsTr("Emoji")
checked: root.logoSettings.emoji === "👋"
onToggled: {
root.logoSettings.emoji = "👋"
root.logoSettings.isLetterIdenticon = true
root.logoSettings.color = "orchid"
}
}
} }
ColumnLayout { ColumnLayout {
visible: previewCard.type !== Constants.LinkPreviewType.StatusContact
Label { Label {
Layout.fillWidth: true Layout.fillWidth: true
text: "Banner size" 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 { ColumnLayout {
Label { Label {
Layout.fillWidth: true Layout.fillWidth: true
text: "Footer type" text: "UserName"
}
RadioButton {
id: footerTypeCommunity
property string footerRichText: `<img src="%1" width="16" height="16" style="vertical-align: top" ></img><font color="%2"> 629.2K </font> <img src="%3" width="16" height="16" style="vertical-align: top" ><font color="%2">112.1K</font>`.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 <img src="%2" width="16" height="16" style="vertical-align: top" ><font color="%2"> %3</font>`.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
} }
} }
@ -201,4 +343,12 @@ SplitView {
} }
} }
} }
Settings {
property alias linkType: previewCard.type
} }
}
// category: Controls
// https://www.figma.com/file/Mr3rqxxgKJ2zMQ06UAKiWL/💬-ChatDesktop?type=design&node-id=22347-219545&mode=design&t=bODv5MUGQgU9ThJF-0

View File

@ -25,14 +25,38 @@ SplitView {
LinkPreviewMiniCard { LinkPreviewMiniCard {
id: previewMiniCard id: previewMiniCard
anchors.centerIn: parent 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 type: previewTypeInput.currentIndex
communityName: communityNameInput.text previewState: stateInput.currentIndex
channelName: channelNameInput.text 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"
}
}
} }
} }
@ -50,7 +74,7 @@ SplitView {
id: previewTypeInput id: previewTypeInput
Layout.fillHeight: true Layout.fillHeight: true
Layout.fillWidth: true Layout.fillWidth: true
model: ["link", "image", "community", "channel", "user profile"] model: ["unknown", "standard", "user profile", "community", "channel"]
} }
Label { Label {
text: "Community name" text: "Community name"

View File

@ -2,87 +2,15 @@ import QtQuick 2.15
import QtQuick.Controls 2.15 import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15 import QtQuick.Layouts 1.15
import Models 1.0
import shared.views.chat 1.0 import shared.views.chat 1.0
import shared.stores 1.0 import shared.stores 1.0
SplitView { SplitView {
ListModel { LinkPreviewModel {
id: mockedLinkPreviewModel 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 { Pane {
@ -90,38 +18,19 @@ SplitView {
SplitView.fillWidth: true SplitView.fillWidth: true
SplitView.fillHeight: 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 { LinksMessageView {
id: linksMessageView id: linksMessageView
anchors.fill: parent anchors.fill: parent
store: {} isOnline: true
messageStore: {} playAnimations: true
linkPreviewModel: mockedLinkPreviewModel linkPreviewModel: mockedLinkPreviewModel
gifLinks: [ "https://media.tenor.com/qN_ytiwLh24AAAAC/cold.gif" ] gifLinks: [ "https://media.tenor.com/qN_ytiwLh24AAAAC/cold.gif" ]
isCurrentUser: true isCurrentUser: true
gifUnfurlingEnabled: false
canAskToUnfurlGifs: true
onImageClicked: { onImageClicked: {
console.log("image clicked") console.log("image clicked")
} }
@ -148,18 +57,28 @@ SplitView {
} }
CheckBox { CheckBox {
text: qsTr("Enabled") text: qsTr("Enabled")
checked: RootStore.gifUnfurlingEnabled checked: linksMessageView.gifUnfurlingEnabled
onToggled: RootStore.gifUnfurlingEnabled = !RootStore.gifUnfurlingEnabled onToggled: linksMessageView.gifUnfurlingEnabled = !linksMessageView.gifUnfurlingEnabled
} }
CheckBox { CheckBox {
text: qsTr("Never ask about GIF unfurling again") text: qsTr("Can ask about GIF unfurling")
checked: RootStore.neverAskAboutUnfurlingAgain checked: linksMessageView.canAskToUnfurlGifs
onClicked: RootStore.neverAskAboutUnfurlingAgain = !RootStore.neverAskAboutUnfurlingAgain onClicked: linksMessageView.canAskToUnfurlGifs = !linksMessageView.canAskToUnfurlGifs
} }
Button { Button {
text: qsTr("Reset local `askAboutUnfurling` setting") text: qsTr("Reset local `askAboutUnfurling` setting")
onClicked: linksMessageView.resetLocalAskAboutUnfurling() 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
}
} }
} }
} }

View File

@ -74,7 +74,7 @@ SplitView {
id: fakeUsersModel id: fakeUsersModel
} }
ListModel { LinkPreviewModel {
id: fakeLinksModel id: fakeLinksModel
} }
@ -172,19 +172,12 @@ SplitView {
fakeLinksModel.clear() fakeLinksModel.clear()
words.forEach(function(word){ words.forEach(function(word){
if(Utils.isURL(word)) { if(Utils.isURL(word)) {
fakeLinksModel.append({ const linkPreview = fakeLinksModel.getStandardLinkPreview()
url: encodeURI(word), linkPreview.url = encodeURI(word)
unfurled: d.linkPreviewsEnabled, linkPreview.unfurled = Math.random() > 0.2
immutable: !d.linkPreviewsEnabled, linkPreview.immutable = !d.linkPreviewsEnabled
hostname: Math.floor(Math.random() * 2) ? "youtube.com" : "", linkPreview.empty = Math.random() > 0.7
title: "PSY - GANGNAM STYLE(강남스타일) M/V", fakeLinksModel.append(linkPreview)
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: ""
})
} }
}) })
} }

View File

@ -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/💬-ChatDesktop?type=design&node-id=21961-655678&mode=design&t=JiMnPfMaLPWlrFK3-0

View File

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

View File

@ -6,6 +6,7 @@ ChannelsModel 1.0 ChannelsModel.qml
CollectiblesModel 1.0 CollectiblesModel.qml CollectiblesModel 1.0 CollectiblesModel.qml
FeesModel 1.0 FeesModel.qml FeesModel 1.0 FeesModel.qml
IconModel 1.0 IconModel.qml IconModel 1.0 IconModel.qml
LinkPreviewModel 1.0 LinkPreviewModel.qml
MintedTokensModel 1.0 MintedTokensModel.qml MintedTokensModel 1.0 MintedTokensModel.qml
RecipientModel 1.0 RecipientModel.qml RecipientModel 1.0 RecipientModel.qml
TokenHoldersModel 1.0 TokenHoldersModel.qml TokenHoldersModel 1.0 TokenHoldersModel.qml

View File

@ -80,6 +80,30 @@ QtObject {
return num.toLocaleString(locale, 'f', precision) 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) { function numberFromLocaleString(num, locale = null) {
locale = locale || Qt.locale() locale = locale || Qt.locale()
try { try {

View File

@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M6.46069 13.6656C6.42831 13.957 6.78989 14.1174 6.98417 13.8977L12.6016 7.54762C12.8588 7.25683 12.6524 6.79857 12.2641 6.79857H9.09442C8.91541 6.79857 8.77613 6.64297 8.7959 6.46505L9.25486 2.33445C9.28724 2.04302 8.92565 1.88265 8.73137 2.10227L3.11397 8.45238C2.85673 8.74317 3.06318 9.20143 3.45142 9.20143H6.28554C6.64358 9.20143 6.92212 9.51264 6.88258 9.86848L6.46069 13.6656Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 552 B

View File

@ -123,4 +123,168 @@ TestCase {
compare(LocaleUtils.currencyAmountToLocaleString( compare(LocaleUtils.currencyAmountToLocaleString(
data.amount, null, locale), data.amountString) 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)
}
} }

View File

@ -277,6 +277,10 @@ Item {
popups.openConfirmExternalLinkPopup(link, domain) popups.openConfirmExternalLinkPopup(link, domain)
} }
function onActivateDeepLink(link: string) {
appMain.rootStore.mainModuleInst.activateStatusDeepLink(link)
}
function onPlaySendMessageSound() { function onPlaySendMessageSound() {
sendMessageSound.stop() sendMessageSound.stop()
sendMessageSound.play() sendMessageSound.play()

View File

@ -6,7 +6,7 @@ import QtQuick.Layouts 1.15
import StatusQ.Core 0.1 import StatusQ.Core 0.1
import shared.status 1.0 import shared.status 1.0
import shared.controls.chat 1.0 import shared.controls.delegates 1.0
import utils 1.0 import utils 1.0
@ -99,57 +99,8 @@ Control {
Repeater { Repeater {
id: linkPreviewRepeater id: linkPreviewRepeater
model: d.filteredModel model: d.filteredModel
delegate: LinkPreviewMiniCard { delegate: LinkPreviewMiniCardDelegate {
// Model properties
required property int index 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)) onClose: root.dismissLinkPreview(d.filteredModel.mapToSource(index))
onRetry: root.linkReload(url) onRetry: root.linkReload(url)

View File

@ -6,27 +6,24 @@ import StatusQ.Core 0.1
import StatusQ.Core.Theme 0.1 import StatusQ.Core.Theme 0.1
import StatusQ.Components 0.1 import StatusQ.Components 0.1
import shared.controls 1.0
import shared.status 1.0 import shared.status 1.0
import utils 1.0 import utils 1.0
import "./private"
CalloutCard { CalloutCard {
id: root id: root
/// The title of the callout card readonly property LinkData linkData: LinkData { }
required property string title readonly property UserData userData: UserData { }
required property string description readonly property CommunityData communityData: CommunityData { }
required property string footer readonly property ChannelData channelData: ChannelData { }
property int type: Constants.LinkPreviewType.NoPreview
property bool highlight: false property bool highlight: false
property StatusAssetSettings logoSettings: StatusAssetSettings {
width: 28
height: 28
bgRadius: bgWidth / 2
}
property string bannerImageSource: ""
signal clicked(var mouse) signal clicked(var mouse)
borderWidth: 1 borderWidth: 1
@ -41,71 +38,103 @@ CalloutCard {
} }
contentItem: ColumnLayout { contentItem: ColumnLayout {
StatusImage { Loader {
id: bannerImage id: bannerImageLoader
Layout.fillWidth: true Layout.fillWidth: true
Layout.leftMargin: d.bannerImageMargins Layout.leftMargin: d.bannerImageMargins
Layout.rightMargin: d.bannerImageMargins Layout.rightMargin: d.bannerImageMargins
Layout.topMargin: d.bannerImageMargins Layout.topMargin: d.bannerImageMargins
Layout.preferredHeight: 170 Layout.preferredHeight: 170
active: !!d.bannerImageSource
sourceComponent: StatusImage {
id: bannerImage
asynchronous: true asynchronous: true
source: root.bannerImageSource source: d.bannerImageSource
fillMode: Image.PreserveAspectCrop fillMode: Image.PreserveAspectCrop
layer.enabled: true layer.enabled: true
layer.effect: root.clippingEffect layer.effect: root.clippingEffect
} }
}
ColumnLayout { ColumnLayout {
Layout.fillWidth: true Layout.fillWidth: true
Layout.fillHeight: true Layout.fillHeight: true
Layout.margins: 12 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 { RowLayout {
id: titleLayout
Layout.fillWidth: true Layout.fillWidth: true
Layout.fillHeight: true Layout.fillHeight: true
Layout.maximumHeight: root.description.length ? 28 : 72 Layout.maximumHeight: description.text.length ? 28 : 72
Layout.minimumHeight: 18
StatusSmartIdenticon { StatusSmartIdenticon {
Layout.alignment: Qt.AlignLeft | Qt.AlignTop id: logo
Layout.alignment: Qt.AlignTop
Layout.preferredWidth: 28 Layout.preferredWidth: 28
Layout.preferredHeight: 28 Layout.preferredHeight: 28
asset: root.logoSettings asset.width: width
name: root.logoSettings.name asset.height: height
visible: !!root.logoSettings.name.length || !!root.logoSettings.emoji.length visible: false
} }
StatusBaseText { 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.fillWidth: true
Layout.fillHeight: 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.pixelSize: 13
font.weight: Font.Medium font.weight: Font.Medium
wrapMode: Text.Wrap wrapMode: Text.Wrap
elide: Text.ElideRight 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 { StatusBaseText {
id: description
Layout.fillWidth: true Layout.fillWidth: true
Layout.fillHeight: true Layout.fillHeight: true
text: root.description
font.pixelSize: 12 font.pixelSize: 12
wrapMode: Text.Wrap wrapMode: Text.Wrap
elide: Text.ElideRight elide: Text.ElideRight
color: Theme.palette.baseColor1 color: Theme.palette.baseColor1
visible: root.description.length visible: description.text.length
} }
StatusBaseText { Loader {
id: linkSite id: footerLoader
Layout.fillWidth: true Layout.fillWidth: true
text: root.footer visible: active
font.pixelSize: 12 sourceComponent: FooterText {
lineHeight: 16
lineHeightMode: Text.FixedHeight
color: Theme.palette.baseColor1
elide: Text.ElideRight
verticalAlignment: Text.AlignBottom
textFormat: Text.RichText
} }
} }
} }
}
MouseArea { MouseArea {
anchors.fill: root anchors.fill: root
hoverEnabled: true hoverEnabled: true
@ -114,9 +143,151 @@ CalloutCard {
onClicked: root.clicked(mouse) 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 { QtObject {
id: d id: d
property real bannerImageMargins: 1 / Screen.devicePixelRatio // image size isn't pixel perfect.. property real bannerImageMargins: 1 / Screen.devicePixelRatio // image size isn't pixel perfect..
property bool highlight: root.highlight || root.hovered property bool highlight: root.highlight || root.hovered
property string bannerImageSource: ""
} }
} }

View File

@ -10,6 +10,8 @@ import shared 1.0
import utils 1.0 import utils 1.0
import "./private" 1.0
CalloutCard { CalloutCard {
id: root id: root
@ -20,45 +22,11 @@ CalloutCard {
Loaded Loaded
} }
enum Type { readonly property LinkData linkData: LinkData { }
Unknown = 0, readonly property UserData userData: UserData { }
Link, readonly property CommunityData communityData: CommunityData { }
Image, readonly property ChannelData channelData: ChannelData { }
Community,
Channel,
User
}
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 previewState
required property int type required property int type
@ -106,47 +74,78 @@ CalloutCard {
}, },
State { State {
name: "loaded" 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 { PropertyChanges {
target: root; visible: true; dashedBorder: false; borderWidth: 0; target: root; visible: true; dashedBorder: false; borderWidth: 0;
backgroundColor: root.containsMouse ? Theme.palette.directColor8 : Theme.palette.indirectColor1; backgroundColor: root.containsMouse ? Theme.palette.directColor8 : Theme.palette.indirectColor1;
borderColor: backgroundColor; borderColor: backgroundColor;
} }
PropertyChanges { target: loadingAnimation; visible: false; } PropertyChanges { target: loadingAnimation; visible: false; }
PropertyChanges { target: titleText; text: root.titleStr; color: Theme.palette.directColor1 } PropertyChanges { target: titleText; text: root.linkData.title; color: Theme.palette.directColor1 }
PropertyChanges { target: subtitleText; visible: true; } PropertyChanges { target: subtitleText; visible: true; text: root.linkData.domain; }
PropertyChanges { target: reloadButton; visible: false; } 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 { PropertyChanges {
target: favIcon target: favIcon
visible: true visible: true
name: root.titleStr name: root.linkData.title
asset.isLetterIdenticon: true 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.charactersLen: 2
asset.color: Theme.palette.miscColor9 asset.color: Theme.palette.miscColor9
} }
@ -174,8 +173,6 @@ CalloutCard {
Layout.preferredWidth: 16 Layout.preferredWidth: 16
Layout.preferredHeight: 16 Layout.preferredHeight: 16
visible: false visible: false
name: root.titleStr
asset.name: root.favIconUrl
asset.letterSize: asset.charactersLen == 1 ? 10 : 7 asset.letterSize: asset.charactersLen == 1 ? 10 : 7
} }
ColumnLayout { ColumnLayout {
@ -190,7 +187,6 @@ CalloutCard {
spacing: 0 spacing: 0
StatusBaseText { StatusBaseText {
id: titleText id: titleText
text: root.titleStr
Layout.fillWidth: true Layout.fillWidth: true
Layout.fillHeight: true Layout.fillHeight: true
font.pixelSize: Style.current.additionalTextSize font.pixelSize: Style.current.additionalTextSize
@ -227,7 +223,6 @@ CalloutCard {
Layout.fillHeight: true Layout.fillHeight: true
font.pixelSize: Style.current.tertiaryTextFontSize font.pixelSize: Style.current.tertiaryTextFontSize
color: Theme.palette.baseColor1 color: Theme.palette.baseColor1
text: root.domain
wrapMode: Text.WordWrap wrapMode: Text.WordWrap
elide: Text.ElideRight elide: Text.ElideRight
} }
@ -239,7 +234,6 @@ CalloutCard {
implicitWidth: 34 implicitWidth: 34
implicitHeight: 34 implicitHeight: 34
radius: 4 radius: 4
image.source: root.thumbnailImageUrl
visible: false visible: false
} }
StatusFlatButton { StatusFlatButton {

View File

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

View File

@ -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 {}
}

View File

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

View File

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

View File

@ -0,0 +1,9 @@
import QtQuick 2.15
QtObject {
property string name
property string publicKey
property string bio
property string image
property bool ensVerified
}

View File

@ -19,6 +19,5 @@ SendTransactionButton 1.0 SendTransactionButton.qml
SentMessage 1.0 SentMessage.qml SentMessage 1.0 SentMessage.qml
StateBubble 1.0 StateBubble.qml StateBubble 1.0 StateBubble.qml
UserImage 1.0 UserImage.qml UserImage 1.0 UserImage.qml
UserProfileCard 1.0 UserProfileCard.qml
UsernameLabel 1.0 UsernameLabel.qml UsernameLabel 1.0 UsernameLabel.qml
VerificationLabel 1.0 VerificationLabel.qml VerificationLabel 1.0 VerificationLabel.qml

View File

@ -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 : ""
}
}
}

View File

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

View File

@ -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 : ""
}
}
}

View File

@ -1 +1,4 @@
ContactListItemDelegate 1.0 ContactListItemDelegate.qml ContactListItemDelegate 1.0 ContactListItemDelegate.qml
LinkPreviewCardDelegate 1.0 LinkPreviewCardDelegate.qml
LinkPreviewGifDelegate 1.0 LinkPreviewGifDelegate.qml
LinkPreviewMiniCardDelegate 1.0 LinkPreviewMiniCardDelegate.qml

View File

@ -1201,7 +1201,7 @@ Rectangle {
} }
control.fileUrlsAndSources = urls control.fileUrlsAndSources = urls
} }
onImageClicked: (chatImage) => Global.openImagePopup(chatImage) onImageClicked: (chatImage) => Global.openImagePopup(chatImage, "")
onLinkReload: (link) => control.linkPreviewReloaded(link) onLinkReload: (link) => control.linkPreviewReloaded(link)
onLinkClicked: (link) => Global.openLink(link) onLinkClicked: (link) => Global.openLink(link)
onEnableLinkPreview: () => control.enableLinkPreview() onEnableLinkPreview: () => control.enableLinkPreview()

View File

@ -7,6 +7,8 @@ StatusMenu {
property string url property string url
property string imageSource property string imageSource
property string domain
property bool requireConfirmationOnOpen: false
QtObject { QtObject {
id: d id: d
@ -43,6 +45,6 @@ StatusMenu {
text: qsTr("Open link") text: qsTr("Open link")
icon.name: "browser" icon.name: "browser"
enabled: d.isUnfurled enabled: d.isUnfurled
onTriggered: Global.openLink(root.url) onTriggered: requireConfirmationOnOpen ? Global.openLinkWithConfirmation(root.url, root.domain) : Global.openLink(root.url)
} }
} }

View File

@ -10,24 +10,29 @@ import StatusQ.Components 0.1
import shared.controls 1.0 import shared.controls 1.0
import shared.status 1.0 import shared.status 1.0
import shared.panels 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 { Flow {
id: root id: root
required property var store required property bool isOnline
required property var messageStore required property bool playAnimations
required property var linkPreviewModel required property var linkPreviewModel
required property var gifLinks required property var gifLinks
required property bool isCurrentUser required property bool isCurrentUser
required property bool gifUnfurlingEnabled
required property bool canAskToUnfurlGifs
readonly property alias hoveredLink: linksRepeater.hoveredUrl readonly property alias hoveredLink: linksRepeater.hoveredUrl
property string highlightLink: "" 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() { function resetLocalAskAboutUnfurling() {
d.localAskAboutUnfurling = true d.localAskAboutUnfurling = true
@ -45,15 +50,25 @@ Flow {
Loader { Loader {
visible: active visible: active
active: root.gifLinks && root.gifLinks.length > 0 active: root.gifLinks && root.gifLinks.length > 0
&& !RootStore.gifUnfurlingEnabled && !root.gifUnfurlingEnabled
&& d.localAskAboutUnfurling && !RootStore.neverAskAboutUnfurlingAgain && d.localAskAboutUnfurling && root.canAskToUnfurlGifs
sourceComponent: enableLinkComponent sourceComponent: enableLinkComponent
} }
Repeater { Repeater {
id: tempRepeater id: tempRepeater
model: RootStore.gifUnfurlingEnabled ? gifLinks : [] visible: root.canAskToUnfurlGifs
delegate: gifComponent 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 { Repeater {
@ -62,154 +77,26 @@ Flow {
property string hoveredUrl: "" property string hoveredUrl: ""
model: root.linkPreviewModel model: root.linkPreviewModel
delegate: Loader { delegate: LinkPreviewCardDelegate {
id: linkMessageLoader id: delegate
// 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 }
}
]
}
}
}
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
}
}
}
}
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)
}
}
}
//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 isCurrentUser: root.isCurrentUser
playing: globalAnimationEnabled && localAnimationEnabled highlight: url === root.highlightLink
isOnline: root.store.mainModuleInst.isOnline onHoveredChanged: {
asynchronous: true linksRepeater.hoveredUrl = hovered ? url : ""
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 onClicked: (mouse) => {
Loader { if(mouse.button === Qt.RightButton) {
width: 45 const domain = previewType === Constants.LinkPreviewType.Standard ? linkData.domain : Constants.externalStatusLink
height: 38 root.openContextMenu(delegate, url, domain)
anchors.left: parent.left return
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 if(previewType === Constants.LinkPreviewType.Standard) {
text: "GIF" Global.openLinkWithConfirmation(url, linkData.domain)
font.pixelSize: 13 return
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") text: qsTr("Don't ask me again")
} }
} }
onClicked: RootStore.setNeverAskAboutUnfurlingAgain(true) onClicked: root.setNeverAskAboutUnfurlingAgain(true)
Component.onCompleted: { Component.onCompleted: {
background.radius = Style.current.padding; background.radius = Style.current.padding;
} }
@ -316,5 +203,5 @@ Flow {
} }
} }
} }
} }

View File

@ -8,6 +8,7 @@ import shared.controls 1.0
import shared.popups 1.0 import shared.popups 1.0
import shared.views.chat 1.0 import shared.views.chat 1.0
import shared.controls.chat 1.0 import shared.controls.chat 1.0
import shared.stores 1.0
import StatusQ.Core 0.1 import StatusQ.Core 0.1
import StatusQ.Core.Theme 0.1 import StatusQ.Core.Theme 0.1
@ -570,7 +571,7 @@ Loader {
rootStore.chatCommunitySectionModule.switchToChannel(link.replace("#", "")) rootStore.chatCommunitySectionModule.switchToChannel(link.replace("#", ""))
return return
} else if (Utils.isStatusDeepLink(link)) { } else if (Utils.isStatusDeepLink(link)) {
rootStore.activateStatusDeepLink(link) Global.activateDeepLink(link)
return return
} }
@ -755,14 +756,20 @@ Loader {
id: linksMessageView id: linksMessageView
linkPreviewModel: root.linkPreviewModel linkPreviewModel: root.linkPreviewModel
gifLinks: root.gifLinks gifLinks: root.gifLinks
messageStore: root.messageStore playAnimations: root.messageStore.playAnimation
store: root.rootStore isOnline: root.rootStore.mainModuleInst.isOnline
isCurrentUser: root.amISender isCurrentUser: root.amISender
highlightLink: delegate.hoveredLink highlightLink: delegate.hoveredLink
onImageClicked: (image, mouse, imageSource, url) => { onImageClicked: (image, mouse, imageSource, url) => {
d.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 onHoveredLinkChanged: delegate.highlightedLink = linksMessageView.hoveredLink
gifUnfurlingEnabled: RootStore.gifUnfurlingEnabled
canAskToUnfurlGifs: !RootStore.neverAskAboutUnfurlingAgain
onSetNeverAskAboutUnfurlingAgain: RootStore.setNeverAskAboutUnfurlingAgain(neverAskAgain)
} }
} }

View File

@ -56,6 +56,7 @@ QtObject {
signal openLink(string link) signal openLink(string link)
signal openLinkWithConfirmation(string link, string domain) signal openLinkWithConfirmation(string link, string domain)
signal activateDeepLink(string link)
signal setNthEnabledSectionActive(int nthSection) signal setNthEnabledSectionActive(int nthSection)
signal appSectionBySectionTypeChanged(int sectionType, int subsection) signal appSectionBySectionTypeChanged(int sectionType, int subsection)