feat: Generate link previews in StatusChatInput - introduce LinkPreviewMiniCard

Adding LinkPreviewMiniCard component based on Figma designs. This component is similar to LinkPreviewCard, but has less info and a different format.

There is also some additional functionality attached to this card: close button, reload button and hovered state. The LinkPreviewMiniCard can have multiple states, based on content type and loading state.

+ Adding storybook page

Figma design: https://www.figma.com/file/Mr3rqxxgKJ2zMQ06UAKiWL/💬-Chat⎜Desktop?type=design&node-id=22341-184809&mode=design&t=151TjdzkzI7flR4P-0
This commit is contained in:
Alex Jbanca 2023-09-26 17:12:33 +03:00 committed by Alex Jbanca
parent ebe102ac8a
commit 2c8ad61947
6 changed files with 431 additions and 43 deletions

View File

@ -0,0 +1,149 @@
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 1.0
import shared.controls.chat 1.0
import utils 1.0
SplitView {
id: root
Pane {
SplitView.fillWidth: true
SplitView.fillHeight: true
layer.enabled: true
layer.samples: 4
background: Rectangle {
color: Theme.palette.statusChatInput.secondaryBackgroundColor
}
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
}
}
Pane {
SplitView.preferredWidth: 300
SplitView.fillHeight: true
ColumnLayout {
spacing: 24
ColumnLayout {
Label {
text: "Preview type"
}
ComboBox {
id: previewTypeInput
Layout.fillHeight: true
Layout.fillWidth: true
model: ["link", "image", "community", "channel", "user profile"]
}
Label {
text: "Community name"
}
TextField {
id: communityNameInput
Layout.fillHeight: true
Layout.fillWidth: true
text: "Socks"
}
Label {
text: "Channel name"
}
TextField {
id: channelNameInput
Layout.fillHeight: true
Layout.fillWidth: true
text: "General"
}
Label {
text: "User name"
}
TextField {
id: userNameInput
Layout.fillHeight: true
Layout.fillWidth: true
text: "John Doe"
}
Label {
text: "Title"
}
TextField {
id: titleInput
Layout.fillHeight: true
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: "Domain"
}
TextField {
id: domainInput
Layout.fillHeight: true
Layout.fillWidth: true
text: "rarible.com"
}
Label {
text: "Favicon URL"
}
TextField {
id: faviconInput
Layout.fillHeight: true
Layout.fillWidth: true
text: "https://rarible.com/public/favicon.png"
}
Label {
text: "External image URL"
}
TextField {
id: externalImageInput
Layout.fillHeight: true
Layout.fillWidth: true
text: "https://rarible.com/public/favicon.png"
}
Label {
text: "State"
}
ComboBox {
id: stateInput
Layout.fillHeight: true
Layout.fillWidth: true
model: ["invalid", "loading", "loading failed", "loaded"]
}
}
}
}
Settings {
property alias linkPreviewMiniCardState: stateInput.currentIndex
property alias linkPreviewMiniCardCommunityName: communityNameInput.text
property alias linkPreviewMiniCardChannelName: channelNameInput.text
property alias linkPreviewMiniCardTitle: titleInput.text
property alias linkPreviewMiniCardDomain: domainInput.text
property alias linkPreviewMiniCardFavIconUrl: faviconInput.text
property alias linkPreviewMiniCardThumbnailImageUrl: externalImageInput.text
property alias linkPreviewMiniCardType: previewTypeInput.currentIndex
}
}
//Category: Controls
//"https://www.figma.com/file/Mr3rqxxgKJ2zMQ06UAKiWL/💬-ChatDesktop?type=design&node-id=22341-184809&mode=design&t=VWBVK4DOUxr1BmTp-0"

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="M5.96967 4.96967C5.67678 5.26256 5.67678 5.73744 5.96967 6.03033L8.08579 8.14645C8.28105 8.34171 8.28105 8.65829 8.08579 8.85355L5.96967 10.9697C5.67678 11.2626 5.67678 11.7374 5.96967 12.0303C6.26256 12.3232 6.73744 12.3232 7.03033 12.0303L10.0303 9.03033C10.3232 8.73744 10.3232 8.26256 10.0303 7.96967L7.03033 4.96967C6.73744 4.67678 6.26256 4.67678 5.96967 4.96967Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 538 B

View File

@ -34,6 +34,10 @@ Shape {
property alias font: description.font
property int radius: Style.current.radius
property int leftTopRadius: radius
property int rightTopRadius: radius
property int leftBottomRadius: radius
property int rightBottomRadius: radius
readonly property alias path: path
asynchronous: true
@ -59,55 +63,47 @@ Shape {
strokeStyle: ShapePath.DashLine
dashPattern: [4, 4]
startX: root.radius
startX: root.leftTopRadius
startY: 0
PathLine {
x: root.width - root.radius
x: root.width - root.rightTopRadius
y: 0
}
PathCubic {
control1X: root.width
control2X: root.width
control1Y: 0
control2Y: 0
PathArc {
x: root.width
y: root.radius
y: root.rightTopRadius
radiusX: root.rightTopRadius
radiusY: root.rightTopRadius
}
PathLine {
x: root.width
y: root.height - root.radius
y: root.height - root.rightBottomRadius
}
PathCubic {
control1X: root.width
control2X: root.width
control1Y: root.height
control2Y: root.height
x: root.width - root.radius
PathArc {
x:root.width - root.rightBottomRadius
y: root.height
radiusX: root.rightBottomRadius
radiusY: root.rightBottomRadius
}
PathLine {
x: root.radius
x: root.leftBottomRadius
y: root.height
}
PathCubic {
control1X: 0
control2X: 0
control1Y: root.height
control2Y: root.height
x: 0
y: root.height - root.radius
PathArc {
x:0
y: root.height - root.leftBottomRadius
radiusX: root.leftBottomRadius;
radiusY: root.leftBottomRadius
}
PathLine {
x: 0
y: root.radius
y: root.leftTopRadius
}
PathCubic {
control1X: 0
control2X: 0
control1Y: 0
control2Y: 0
x: root.radius
PathArc {
x:root.leftTopRadius
y: 0
radiusX: root.leftTopRadius
radiusY: root.leftTopRadius
}
}
}

View File

@ -1,14 +1,19 @@
import QtQuick 2.13
import QtQuick.Controls 2.15
import QtQuick.Shapes 1.5
import utils 1.0
import shared 1.0
import shared.controls 1.0
Control {
id: root
property bool leftTail: true
property color backgroundColor: Style.current.background
property color borderColor: Style.current.border
property bool dashedBorder: false
property real borderWidth: 1
readonly property Component clippingEffect: CalloutOpacityMask {
@ -16,19 +21,14 @@ Control {
height: parent.height
leftTail: root.leftTail
}
background: Rectangle {
color: Style.current.border
layer.enabled: true
layer.effect: root.clippingEffect
Rectangle {
id: clipping
anchors.fill: parent
anchors.margins: root.borderWidth
color: Style.current.background
layer.enabled: true
layer.effect: root.clippingEffect
}
background: ShapeRectangle {
path.fillColor: root.backgroundColor
path.strokeColor: root.borderColor
path.strokeWidth: root.borderWidth
path.strokeStyle: root.dashedBorder ? ShapePath.DashLine : ShapePath.SolidLine
radius: Style.current.radius * 2
leftBottomRadius: root.leftTail ? Style.current.radius / 2 : Style.current.radius * 2
rightBottomRadius: root.leftTail ? Style.current.radius * 2 : Style.current.radius / 2
}
}

View File

@ -0,0 +1,239 @@
import QtQuick 2.15
import QtQuick.Layouts 1.15
import StatusQ.Core 0.1
import StatusQ.Core.Theme 0.1
import StatusQ.Components 0.1
import StatusQ.Controls 0.1
import shared 1.0
import utils 1.0
CalloutCard {
id: root
enum State {
Invalid,
Loading,
LoadingFailed,
Loaded
}
enum Type {
Link,
Image,
Community,
Channel,
User
}
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
readonly property bool containsMouse: mouseArea.hovered || reloadButton.hovered || closeButton.hovered
signal close()
signal retry()
signal clicked(var eventPoint)
implicitWidth: 260
implicitHeight: 64
verticalPadding: 15
horizontalPadding: 12
visible: true
borderColor: Theme.palette.directColor7
backgroundColor: root.containsMouse ? Theme.palette.directColor7 : Theme.palette.baseColor4
// behavior
states: [
State {
name: "invalid"
when: root.previewState === LinkPreviewMiniCard.State.Invalid
PropertyChanges {
target: root
visible: false
}
},
State {
name: "loading"
when: root.previewState === LinkPreviewMiniCard.State.Loading
PropertyChanges { target: root; visible: true; dashedBorder: true; }
PropertyChanges { target: loadingAnimation; visible: true; }
PropertyChanges { target: titleText; text: qsTr("Generating preview..."); color: Theme.palette.baseColor1 }
PropertyChanges { target: subtitleText; visible: false; }
PropertyChanges { target: reloadButton; visible: false; }
},
State {
name: "loadingFailed"
when: root.previewState === LinkPreviewMiniCard.State.LoadingFailed
PropertyChanges { target: root; visible: true; dashedBorder: true; }
PropertyChanges { target: loadingAnimation; visible: false; }
PropertyChanges { target: titleText; text: qsTr("Failed to generate preview"); color: Theme.palette.directColor1 }
PropertyChanges { target: subtitleText; visible: false; }
PropertyChanges { target: reloadButton; visible: true; }
},
State {
name: "loaded"
when: root.previewState === LinkPreviewMiniCard.State.Loaded && root.type === LinkPreviewMiniCard.Type.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: 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; asset.charactersLen: 2; asset.color: Theme.palette.miscColor9; }
}
]
contentItem: Item {
RowLayout {
anchors.fill: parent
spacing: 0
LoadingAnimation {
id: loadingAnimation
Layout.alignment: Qt.AlignVCenter
Layout.margins: 4
visible: false
}
StatusSmartIdenticon {
id: favIcon
Layout.alignment: Qt.AlignLeft | Qt.AlignTop
Layout.topMargin: 1
Layout.preferredWidth: 16
Layout.preferredHeight: 16
visible: false
name: root.titleStr
asset.name: root.favIconUrl
asset.letterSize: asset.charactersLen == 1 ? 10 : 7
}
ColumnLayout {
Layout.fillWidth: true
Layout.fillHeight: true
Layout.leftMargin: 8
spacing: 0
RowLayout {
id: titleRow
Layout.fillWidth: true
Layout.fillHeight: true
spacing: 0
StatusBaseText {
id: titleText
text: root.titleStr
Layout.fillWidth: true
Layout.fillHeight: true
font.pixelSize: Style.current.additionalTextSize
font.weight: Font.Medium
wrapMode: Text.Wrap
elide: Text.ElideRight
verticalAlignment: Text.AlignVCenter
maximumLineCount: 1
}
StatusIcon {
id: secondTitleIcon
width: 16
height: 16
icon: "tiny/chevron-right"
color: Theme.palette.baseColor1
visible: secondTitleText.visible
}
StatusBaseText {
id: secondTitleText
Layout.fillWidth: true
Layout.fillHeight: true
font.pixelSize: Style.current.additionalTextSize
font.weight: Font.Medium
wrapMode: Text.Wrap
elide: Text.ElideRight
verticalAlignment: Text.AlignVCenter
maximumLineCount: 1
visible: false
}
}
StatusBaseText {
id: subtitleText
Layout.fillWidth: true
Layout.fillHeight: true
font.pixelSize: Style.current.tertiaryTextFontSize
color: Theme.palette.baseColor1
text: root.domain
wrapMode: Text.WordWrap
elide: Text.ElideRight
}
}
StatusRoundedImage {
id: thumbnailImage
Layout.alignment: Qt.AlignRight
Layout.rightMargin: 4
implicitWidth: 34
implicitHeight: 34
radius: 4
image.source: root.thumbnailImageUrl
visible: false
}
StatusFlatButton {
id: reloadButton
icon.name: "refresh"
size: StatusBaseButton.Size.Small
hoverColor: Theme.palette.directColor8
textColor: Theme.palette.directColor1
onClicked: root.retry()
}
StatusFlatButton {
id: closeButton
icon.name: "close"
size: StatusBaseButton.Size.Small
hoverColor: Theme.palette.directColor8
textColor: Theme.palette.directColor1
onClicked: root.close()
}
}
}
HoverHandler {
id: mouseArea
target: background
cursorShape: Qt.PointingHandCursor
}
TapHandler {
id: tapHandler
target: background
onTapped: root.clicked(eventPoint)
}
}

View File

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