fix(StatusMessage): Design update and minor improvements (#752)

This commit is contained in:
Igor Sirotin 2022-08-20 03:01:28 +03:00 committed by Michał Cieślak
parent c922bf5adf
commit 23e50341d2
24 changed files with 2800 additions and 546 deletions

View File

@ -14,9 +14,10 @@ ListView {
anchors.fill: parent
anchors.margins: 15
clip: true
delegate: StatusMessage {
id: delegate
width: parent.width
width: ListView.view.width
audioMessageInfoText: "Audio Message"
cancelButtonText: "Cancel"
@ -26,44 +27,70 @@ ListView {
resendText: "Resend"
pinnedMsgInfoText: "Pinned by"
timestamp: model.timestamp
isAReply: model.isReply
hasMention: model.hasMention
isPinned: model.isPinned
pinnedBy: model.pinnedBy
hasExpired: model.hasExpired
reactionsModel: model.reactions || []
messageDetails: StatusMessageDetails {
contentType: model.contentType
messageContent: model.messageContent
amISender: model.amIsender
displayName: model.userName
secondaryName: model.localName !== "" && model.ensName.startsWith("@") ? model.ensName: ""
chatID: model.chatKey
profileImage: StatusImageSettings {
sender.id: model.senderId
sender.userName: model.userName
sender.localName: model.localName
sender.ensName: model.ensName
sender.isContact: model.isContact
sender.trustIndicator: model.trustIndicator
sender.profileImage {
width: 40
height: 40
source: model.profileImage
isIdenticon: model.isIdenticon
pubkey: model.senderId
source: model.profileImage || ""
colorId: 1
colorHash: ListModel {
ListElement { colorId: 13; segmentLength: 5 }
ListElement { colorId: 31; segmentLength: 5 }
ListElement { colorId: 10; segmentLength: 1 }
ListElement { colorId: 2; segmentLength: 5 }
ListElement { colorId: 26; segmentLength: 2 }
ListElement { colorId: 19; segmentLength: 4 }
ListElement { colorId: 28; segmentLength: 3 }
}
}
messageText: model.message
hasMention: model.hasMention
isContact: model.isContact
trustIndicator: model.trustIndicator
isPinned: model.isPinned
pinnedBy: model.pinnedBy
hasExpired: model.hasExpired
}
timestamp.text: "10:00 am"
timestamp.tooltip.text: "10:01 am"
// reply related data
isAReply: model.isReply
replyDetails: StatusMessageDetails {
amISender: model.isReply ? model.replyAmISender : ""
displayName: model.isReply ? model.replySenderName: ""
profileImage: StatusImageSettings {
amISender: model.isReply && model.replyAmISender
sender.id: model.replySenderId || ""
sender.userName: model.isReply ? model.replySenderName: ""
sender.ensName: model.isReply ? model.replySenderEnsName : ""
sender.profileImage {
width: 20
height: 20
pubkey: model.replySenderId
source: model.isReply ? model.replyProfileImage: ""
isIdenticon: model.isReply ? model.replyIsIdenticon: ""
colorId: 1
colorHash: ListModel {
ListElement { colorId: 13; segmentLength: 5 }
ListElement { colorId: 31; segmentLength: 5 }
ListElement { colorId: 10; segmentLength: 1 }
ListElement { colorId: 2; segmentLength: 5 }
ListElement { colorId: 26; segmentLength: 2 }
ListElement { colorId: 19; segmentLength: 4 }
ListElement { colorId: 28; segmentLength: 3 }
}
}
messageText: model.isReply ? model.replyMessageText: ""
contentType: model.replyContentType
messageContent: model.replyMessageContent
}
quickActions: [
StatusFlatRoundButton {
id: emojiBtn

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,54 @@
import QtQuick 2.14
import StatusQ.Core 0.1
import StatusQ.Core.Theme 0.1
StatusBaseText {
id: root
property int previousMessageIndex: -1
property double previousMessageTimestamp
property double messageTimestamp
font.pixelSize: 13
color: Theme.palette.baseColor1
horizontalAlignment: Text.AlignHCenter
text: {
if (previousMessageIndex === -1)
return "";
const now = new Date()
const yesterday = new Date()
yesterday.setDate(now.getDate()-1)
const currentMsgDate = new Date(messageTimestamp);
const prevMsgDate = new Date(previousMessageTimestamp);
if (!!prevMsgDate && currentMsgDate.getDay() === prevMsgDate.getDay())
return "";
if (now == currentMsgDate)
return qsTr("Today");
if (yesterday == currentMsgDate)
return qsTr("Yesterday");
const monthNames = [
qsTr("January"),
qsTr("February"),
qsTr("March"),
qsTr("April"),
qsTr("May"),
qsTr("June"),
qsTr("July"),
qsTr("August"),
qsTr("September"),
qsTr("October"),
qsTr("November"),
qsTr("December")
];
return monthNames[currentMsgDate.getMonth()] + ", " + currentMsgDate.getDate();
}
}

View File

@ -1,14 +1,16 @@
import QtQuick 2.14
import QtQuick.Layouts 1.14
import QtQuick.Controls 2.14
import StatusQ.Core 0.1
import StatusQ.Core.Theme 0.1
import StatusQ.Core.Utils 0.1
import StatusQ.Controls 0.1
import "./private/statusMessage"
Rectangle {
id: statusMessage
id: root
enum ContentType {
Unknown = 0,
@ -21,12 +23,12 @@ Rectangle {
Invitation = 7
}
property alias messageHeader: messageHeader
property alias quickActions:quickActionsPanel.quickActions
property alias quickActions: quickActionsPanel.items
property alias statusChatInput: editComponent.inputComponent
property alias linksComponent: linksLoader.sourceComponent
property alias footerComponent: footer.sourceComponent
property alias timestamp: messageHeader.timestamp
property alias transcationComponent: transactionBubbleLoader.sourceComponent
property alias invitationComponent: invitationBubbleLoader.sourceComponent
property alias mouseArea: mouseArea
property string resendText: ""
property string cancelButtonText: ""
@ -35,155 +37,360 @@ Rectangle {
property string errorLoadingImageText: ""
property string audioMessageInfoText: ""
property string pinnedMsgInfoText: ""
property var reactionIcons: [
Emoji.iconSource("❤"),
Emoji.iconSource("👍"),
Emoji.iconSource("👎"),
Emoji.iconSource("🤣"),
Emoji.iconSource("😥"),
Emoji.iconSource("😠")
]
property string messageId: ""
property bool isAppWindowActive: false
property bool editMode: false
property bool isAReply: false
property bool isEdited: false
property bool isChatBlocked: false
property bool hasMention: false
property bool isPinned: false
property string pinnedBy: ""
property bool hasExpired: false
property double timestamp: 0
property var reactionsModel: []
readonly property bool dateGroupVisible: dateGroupLabel.visible
property bool showHeader: true
property bool isActiveMessage: false
property bool disableHover: false
property bool hideQuickActions: false
property color overrideBackgroundColor: "transparent"
property bool overrideBackground: false
property alias previousMessageIndex: dateGroupLabel.previousMessageIndex
property alias previousMessageTimestamp: dateGroupLabel.previousMessageTimestamp
property StatusMessageDetails messageDetails: StatusMessageDetails {}
property StatusMessageDetails replyDetails: StatusMessageDetails {}
signal profilePictureClicked()
signal senderNameClicked()
signal editCompleted(var newMsgText)
signal replyProfileClicked()
signal stickerLoaded()
signal imageClicked(var imageSource)
property string timestampString: Qt.formatTime(new Date(timestamp), "hh:mm");
property string timestampTooltipString: Qt.formatTime(new Date(timestamp), "dddd, MMMM d, yyyy hh:mm:ss t");
signal clicked(var sender, var mouse)
signal profilePictureClicked(var sender, var mouse)
signal senderNameClicked(var sender, var mouse)
signal replyProfileClicked(var sender, var mouse)
signal addReactionClicked(var sender, var mouse)
signal toggleReactionClicked(int emojiId)
signal imageClicked(var image, var mouse, var imageSource)
signal stickerClicked()
signal resendClicked()
height: childrenRect.height
color: hoverHandler.hovered ? (messageDetails.hasMention ? Theme.palette.mentionColor3 : messageDetails.isPinned ? Theme.palette.pinColor2 : Theme.palette.baseColor2) : messageDetails.hasMention ? Theme.palette.mentionColor4 : messageDetails.isPinned ? Theme.palette.pinColor3 : "transparent"
signal editCompleted(var newMsgText)
signal editCancelled()
signal stickerLoaded()
signal linkActivated(string link)
signal hoverChanged(string messageId, bool hovered)
signal activeChanged(string messageId, bool active)
function startMessageFoundAnimation() {
messageFoundAnimation.start();
}
implicitWidth: messageLayout.implicitWidth
+ messageLayout.anchors.leftMargin
+ messageLayout.anchors.rightMargin
implicitHeight: messageLayout.implicitHeight
+ messageLayout.anchors.topMargin
+ messageLayout.anchors.bottomMargin
color: {
if (root.overrideBackground)
return root.overrideBackgroundColor;
if (root.editMode)
return Theme.palette.baseColor2;
if (hoverHandler.hovered || root.isActiveMessage) {
if (root.hasMention)
return Theme.palette.mentionColor3;
if (root.isPinned)
return Theme.palette.pinColor2;
return Theme.palette.baseColor2;
}
if (root.hasMention)
return Theme.palette.mentionColor4;
if (root.isPinned)
return Theme.palette.pinColor3;
return "transparent";
}
Rectangle {
anchors {
top: parent.top
bottom: parent.bottom
left: parent.left
}
width: 2
visible: root.isPinned
color: Theme.palette.pinColor1
}
Rectangle {
anchors {
top: parent.top
bottom: parent.bottom
left: parent.left
}
width: 2
visible: root.hasMention
color: Theme.palette.mentionColor1
}
SequentialAnimation {
id: messageFoundAnimation
PauseAnimation {
duration: 600
}
NumberAnimation {
target: highlightRect
property: "opacity"
to: 1.0
duration: 1500
}
PauseAnimation {
duration: 1000
}
NumberAnimation {
target: highlightRect
property: "opacity"
to: 0.0
duration: 1500
}
}
Rectangle {
id: highlightRect
anchors.fill: parent
opacity: 0
visible: opacity > 0.001
color: Theme.palette.baseColor2
}
MouseArea {
id: mouseArea
anchors.fill: parent
}
HoverHandler {
id: hoverHandler
enabled: !root.isActiveMessage && !root.disableHover
}
ColumnLayout {
id: messageLayout
width: parent.width
StatusMessageReply {
anchors.fill: parent
anchors.topMargin: 8
anchors.bottomMargin: 8
StatusDateGroupLabel {
id: dateGroupLabel
Layout.fillWidth: true
visible: isAReply
replyDetails: statusMessage.replyDetails
onReplyProfileClicked: statusMessage.replyProfileClicked()
audioMessageInfoText: statusMessage.audioMessageInfoText
Layout.topMargin: 20
messageTimestamp: root.timestamp
visible: text !== ""
}
RowLayout {
spacing: 8
Loader {
Layout.fillWidth: true
StatusSmartIdenticon {
id: profileImage
active: isAReply
visible: active
sourceComponent: StatusMessageReply {
replyDetails: root.replyDetails
onReplyProfileClicked: root.replyProfileClicked(sender, mouse)
audioMessageInfoText: root.audioMessageInfoText
}
}
RowLayout {
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
spacing: 8
Item {
Layout.alignment: Qt.AlignTop
Layout.topMargin: 10
Layout.leftMargin: 16
image: messageDetails.profileImage
name: messageHeader.displayName
MouseArea {
cursorShape: Qt.PointingHandCursor
acceptedButtons: Qt.LeftButton | Qt.RightButton
anchors.fill: parent
onClicked: statusMessage.profilePictureClicked()
implicitWidth: profileImage.effectiveSize.width
implicitHeight: profileImage.visible ? profileImage.effectiveSize.height : 0
StatusSmartIdenticon {
id: profileImage
active: root.showHeader
visible: active
name: root.messageDetails.sender.userName
image: root.messageDetails.sender.profileImage.imageSettings
icon: root.messageDetails.sender.profileImage.iconSettings
ringSettings: root.messageDetails.sender.profileImage.ringSettings
MouseArea {
cursorShape: Qt.PointingHandCursor
acceptedButtons: Qt.LeftButton | Qt.RightButton
anchors.fill: parent
onClicked: root.profilePictureClicked(this, mouse)
}
}
}
Column {
ColumnLayout {
spacing: 4
Layout.alignment: Qt.AlignTop
Layout.topMargin: 10
Layout.fillWidth: true
StatusPinMessageDetails {
visible: messageDetails.isPinned && !editMode
pinnedMsgInfoText: statusMessage.pinnedMsgInfoText
pinnedBy: messageDetails.pinnedBy
Loader {
active: root.isPinned && !editMode
visible: active
sourceComponent: StatusPinMessageDetails {
pinnedMsgInfoText: root.pinnedMsgInfoText
pinnedBy: root.pinnedBy
}
}
StatusMessageHeader {
id: messageHeader
width: parent.width
displayName: messageDetails.displayName
secondaryName: messageDetails.secondaryName
tertiaryDetail: messageDetails.chatID
isContact: messageDetails.isContact
trustIndicator: messageDetails.trustIndicator
resendText: statusMessage.resendText
showResendButton: messageDetails.hasExpired && messageDetails.amISender
onClicked: statusMessage.senderNameClicked()
onResendClicked: statusMessage.resendClicked()
visible: !editMode
Layout.fillWidth: true
sender: root.messageDetails.sender
amISender: root.messageDetails.amISender
resendText: root.resendText
showResendButton: root.hasExpired && root.messageDetails.amISender
onClicked: root.senderNameClicked(sender, mouse)
onResendClicked: root.resendClicked()
visible: root.showHeader && !editMode
timestamp.text: root.timestampString
timestamp.tooltip.text: root.timestampTooltipString
}
Loader {
active: !editMode && !!messageDetails.messageText
width: parent.width
Layout.fillWidth: true
active: !editMode && !!root.messageDetails.messageText
visible: active
sourceComponent: StatusTextMessage {
width: parent.width
textField.text: messageDetails.messageText
textField.text: {
if (root.messageDetails.contentType === StatusMessage.ContentType.Sticker)
return "";
const formattedMessage = Utils.linkifyAndXSS(root.messageDetails.messageText);
if (root.messageDetails.contentType === StatusMessage.ContentType.Emoji)
return Emoji.parse(formattedMessage, Emoji.size.middle, Emoji.format.png);
if (root.isEdited) {
const index = formattedMessage.endsWith("code>") ? formattedMessage.length : formattedMessage.length - 4;
const editedMessage = formattedMessage.slice(0, index)
+ ` <span class="isEdited">` + qsTr("(edited)") + `</span>`
+ formattedMessage.slice(index);
return Utils.getMessageWithStyle(Emoji.parse(editedMessage), textField.hoveredLink)
}
return Utils.getMessageWithStyle(Emoji.parse(formattedMessage), textField.hoveredLink)
}
onLinkActivated: {
root.linkActivated(link);
}
}
}
Loader {
active: messageDetails.contentType === StatusMessage.ContentType.Image && !editMode
active: root.messageDetails.contentType === StatusMessage.ContentType.Image && !editMode
visible: active
sourceComponent: StatusImageMessage {
source: messageDetails.contentType === StatusMessage.ContentType.Image ? messageDetails.messageContent : ""
onClicked: statusMessage.imageClicked()
shapeType: messageDetails.amISender ? StatusImageMessage.ShapeType.RIGHT_ROUNDED : StatusImageMessage.ShapeType.LEFT_ROUNDED
source: root.messageDetails.contentType === StatusMessage.ContentType.Image ? root.messageDetails.messageContent : ""
onClicked: root.imageClicked(image, mouse, imageSource)
shapeType: root.messageDetails.amISender ? StatusImageMessage.ShapeType.RIGHT_ROUNDED : StatusImageMessage.ShapeType.LEFT_ROUNDED
}
}
StatusSticker {
visible: messageDetails.contentType === StatusMessage.ContentType.Sticker && !editMode
image.source: messageDetails.messageContent
onLoaded: statusMessage.stickerLoaded()
Loader {
active: root.messageDetails.contentType === StatusMessage.ContentType.Sticker && !editMode
visible: active
sourceComponent: StatusSticker {
image.source: root.messageDetails.messageContent
onLoaded: root.stickerLoaded()
onClicked: {
root.stickerClicked()
}
}
}
Loader {
active: messageDetails.contentType === StatusMessage.ContentType.Audio && !editMode
active: root.messageDetails.contentType === StatusMessage.ContentType.Audio && !editMode
visible: active
sourceComponent: StatusAudioMessage {
audioSource: messageDetails.messageContent
audioSource: root.messageDetails.messageContent
hovered: hoverHandler.hovered
audioMessageInfoText: statusMessage.audioMessageInfoText
audioMessageInfoText: root.audioMessageInfoText
}
}
Loader {
id: linksLoader
active: !!linksLoader.sourceComponent
active: !root.editMode
visible: active
}
Loader {
id: transactionBubbleLoader
active: messageDetails.contentType === StatusMessage.ContentType.Transaction && !editMode
active: root.messageDetails.contentType === StatusMessage.ContentType.Transaction && !editMode
visible: active
}
Loader {
id: invitationBubbleLoader
active: messageDetails.contentType === StatusMessage.ContentType.Invitation && !editMode
active: root.messageDetails.contentType === StatusMessage.ContentType.Invitation && !editMode
visible: active
}
StatusEditMessage {
id: editComponent
width: parent.width
msgText: messageDetails.messageText
visible: editMode
saveButtonText: statusMessage.saveButtonText
cancelButtonText: statusMessage.cancelButtonText
onCancelEditClicked: editMode = false
onEditCompleted: {
editMode = false
statusMessage.editCompleted(newMsgText)
}
Layout.fillWidth: true
Layout.rightMargin: 16
active: root.editMode
visible: active
msgText: root.messageDetails.messageText
saveButtonText: root.saveButtonText
cancelButtonText: root.cancelButtonText
onEditCancelled: root.editCancelled()
onEditCompleted: root.editCompleted(newMsgText)
}
StatusBaseText {
id: retryLbl
color: Theme.palette.dangerColor1
text: statusMessage.resendText
text: root.resendText
font.pixelSize: 12
visible: messageDetails.hasExpired && messageDetails.amISender && !messageDetails.timestamp && !editMode
visible: root.hasExpired && root.messageDetails.amISender && !root.timestamp && !editMode
MouseArea {
cursorShape: Qt.PointingHandCursor
anchors.fill: parent
onClicked: statusMessage.resendClicked()
onClicked: root.resendClicked()
}
}
Loader {
id: footer
active: sourceComponent && !editMode
active: root.reactionsModel.count > 0
visible: active
sourceComponent: StatusMessageEmojiReactions {
id: emojiReactionsPanel
emojiReactionsModel: root.reactionsModel
store: root.messageStore
icons: root.reactionIcons
onHoverChanged: {
root.hoverChanged(messageId, hovered)
}
isCurrentUser: root.messageDetails.amISender
onAddEmojiClicked: root.addReactionClicked(sender, mouse)
onToggleReaction: root.toggleReactionClicked(emojiID)
}
}
}
}
@ -195,6 +402,6 @@ Rectangle {
anchors.rightMargin: 20
anchors.top: parent.top
anchors.topMargin: -8
visible: hoverHandler.hovered && !editMode
visible: hoverHandler.hovered && !root.hideQuickActions
}
}

View File

@ -6,24 +6,13 @@ QtObject {
id: msgDetails
property bool amISender: false
property string displayName: ""
property string secondaryName: ""
property string chatID: ""
property StatusImageSettings profileImage: StatusImageSettings {
width: 40
height: 40
}
property StatusMessageSenderDetails sender: StatusMessageSenderDetails { }
property bool isEdited: false
property string messageText: ""
property int contentType: 0
property string messageText: ""
property string messageContent: ""
property bool isContact: false
property var trustIndicator: StatusContactVerificationIcons.TrustedType.None
property bool hasMention: false
property bool isPinned: false
property string pinnedBy: ""
property bool hasExpired: false
property string timestamp: ""
}

View File

@ -0,0 +1,33 @@
import QtQuick 2.0
import StatusQ.Core 0.1
QtObject {
id: root
property string id: ""
property string userName: ""
property string ensName: ""
property string localName: ""
property bool isContact: false
property int trustIndicator: StatusContactVerificationIcons.TrustedType.None
property StatusProfileImageSettings profileImage: StatusProfileImageSettings {
pubkey: root.id
showRing: !root.ensName
width: 40
height: 40
}
readonly property string displayName: root.localName !== ""
? root.localName
: root.ensName !== ""
? root.ensName
: root.userName
readonly property string secondaryName: root.localName === ""
? ""
: root.ensName !== ""
? root.ensName
: root.userName
}

View File

@ -28,6 +28,10 @@ Loader {
distinctiveColors: Theme.palette.identiconRingColors
}
readonly property size effectiveSize: !!statusSmartIdenticon.image.source.toString()
? Qt.size(statusSmartIdenticon.image.width, statusSmartIdenticon.image.width)
: Qt.size(statusSmartIdenticon.icon.width, statusSmartIdenticon.icon.height)
sourceComponent: statusSmartIdenticon.icon.isLetterIdenticon ? letterIdenticon :
!!statusSmartIdenticon.image.source.toString() ? roundedImage :
!!statusSmartIdenticon.icon.name.toString() ? roundedIcon : letterIdenticon

View File

@ -6,45 +6,63 @@ import StatusQ.Core.Theme 0.1
import StatusQ.Controls 0.1
Item {
id: editText
id: root
property alias inputComponent: chatInputLoader.sourceComponent
property alias active: chatInputLoader.active
property string cancelButtonText: ""
property string saveButtonText: ""
property string msgText: ""
signal cancelEditClicked()
signal editCancelled()
signal editCompleted(var newMsgText)
height: childrenRect.height
implicitHeight: layout.implicitHeight
implicitWidth: layout.implicitWidth
ColumnLayout {
id: layout
anchors.fill: parent
spacing: 4
Loader {
id: chatInputLoader
// To-Do: Move to StatusChatInput once its moved to StatusQ
Layout.fillWidth: true
/*
NOTE: sourceComponent must have `messageText` property
TODO: Replace with StatusChatInput once its moved to StatusQ.
*/
sourceComponent: StatusInput {
width: editText.width
placeholderText: ""
readonly property string messageText: input.text
width: parent.width
input.placeholderText: ""
input.text: msgText
maximumHeight: 40
}
}
RowLayout {
spacing: 4
StatusFlatButton {
id: cancelBtn
text: cancelButtonText
size: StatusBaseButton.Size.Small
onClicked: cancelEditClicked()
onClicked: {
editCancelled()
}
}
StatusButton {
id: saveBtn
text: saveButtonText
size: StatusBaseButton.Size.Small
enabled: chatInputLoader.item.input.text.trim().length > 0
onClicked: editCompleted(chatInputLoader.item.input.text)
enabled: !!chatInputLoader.item && chatInputLoader.item.messageText.trim().length > 0
onClicked: {
editCompleted(!chatInputLoader.item ? "" : chatInputLoader.item.messageText)
}
}
}
}

View File

@ -25,10 +25,10 @@ Item {
property string loadingImageText: ""
property string errorLoadingImageText: ""
signal clicked(var image, var mouse)
signal clicked(var image, var mouse, var imageSource)
width: loadingImage.visible ? loadingImage.width : imageMessage.width
height: loadingImage.visible ? loadingImage.height : imageMessage.paintedHeight
implicitWidth: loadingImage.visible ? loadingImage.width : imageMessage.width
implicitHeight: loadingImage.visible ? loadingImage.height : imageMessage.paintedHeight
QtObject {
id: _internal
@ -87,7 +87,7 @@ Item {
_internal.pausePlaying = ! _internal.pausePlaying
return
}
imageContainer.clicked(imageMessage, mouse)
imageContainer.clicked(imageMessage, mouse, imageMessage.source)
}
}
}

View File

@ -0,0 +1,227 @@
import QtQuick 2.3
import QtQuick.Controls 2.13
import QtGraphicalEffects 1.13
import StatusQ.Core 0.1
import StatusQ.Core.Theme 0.1
import StatusQ.Core.Utils 0.1
import StatusQ.Controls 0.1
import StatusQ.Components 0.1
Item {
id: root
implicitHeight: 22
implicitWidth: childrenRect.width
property int imageMargin: 4
signal addEmojiClicked(var sender, var mouse)
signal hoverChanged(bool hovered)
signal toggleReaction(int emojiID)
property var store
property bool isCurrentUser
property var emojiReactionsModel
property var icons: []
QtObject {
id: d
function lastTwoItems(nodes) {
return nodes.join(qsTr(" and "));
}
function showReactionAuthors(jsonArrayOfUsersReactedWithThisEmoji, emojiId) {
const listOfUsers = JSON.parse(jsonArrayOfUsersReactedWithThisEmoji)
if (listOfUsers.error) {
console.error("error parsing users who reacted to a message, error: ", obj.error)
return
}
let author;
if (listOfUsers.length === 1) {
author = listOfUsers[0]
} else if (listOfUsers.length === 2) {
author = lastTwoItems(listOfUsers);
} else {
var leftNode = [];
var rightNode = [];
const maxReactions = 12
let maximum = Math.min(maxReactions, listOfUsers.length)
if (listOfUsers.length > maxReactions) {
leftNode = listOfUsers.slice(0, maxReactions);
rightNode = listOfUsers.slice(maxReactions, listOfUsers.length);
return (rightNode.length === 1) ?
lastTwoItems([leftNode.join(", "), rightNode[0]]) :
lastTwoItems([leftNode.join(", "), qsTr("%1 more").arg(rightNode.length)]);
}
leftNode = listOfUsers.slice(0, maximum - 1);
rightNode = listOfUsers.slice(maximum - 1, listOfUsers.length);
author = lastTwoItems([leftNode.join(", "), rightNode[0]])
}
return qsTr("%1 reacted with %2")
.arg(author)
.arg(Emoji.getEmojiFromId(emojiId));
}
}
Row {
spacing: root.imageMargin
Repeater {
id: reactionRepeater
width: childrenRect.width
model: root.emojiReactionsModel
Rectangle {
id: emojiContainer
readonly property bool isHovered: mouseArea.containsMouse
width: emojiImage.width + emojiCount.width + (root.imageMargin * 2) + + 8
height: 20
radius: 10
color: model.didIReactWithThisEmoji ?
(isHovered ? Theme.palette.statusMessage.emojiReactionActiveBackgroundHovered : Theme.palette.statusMessage.emojiReactionActiveBackground) :
(isHovered ? Theme.palette.statusMessage.emojiReactionBackgroundHovered : Theme.palette.statusMessage.emojiReactionBackground)
StatusToolTip {
visible: mouseArea.containsMouse
maxWidth: 400
text: d.showReactionAuthors(model.jsonArrayOfUsersReactedWithThisEmoji, model.emojiId)
}
// Rounded corner to cover one corner
Rectangle {
color: parent.color
width: 10
height: 10
anchors.top: parent.top
anchors.left: !root.isCurrentUser ? parent.left : undefined
anchors.leftMargin: 0
anchors.right: !root.isCurrentUser ? undefined : parent.right
anchors.rightMargin: 0
radius: 2
z: -1
}
// This is a workaround to get a "border" around the rectangle including the weird rectangle
Loader {
active: model.didIReactWithThisEmoji
anchors.top: parent.top
anchors.topMargin: -1
anchors.left: parent.left
anchors.leftMargin: -1
z: -2
sourceComponent: Component {
Rectangle {
width: emojiContainer.width + 2
height: emojiContainer.height + 2
radius: emojiContainer.radius
color: Theme.palette.primaryColor1
Rectangle {
color: parent.color
width: 10
height: 10
anchors.top: parent.top
anchors.left: !root.isCurrentUser ? parent.left : undefined
anchors.leftMargin: 0
anchors.right: !root.isCurrentUser ? undefined : parent.right
anchors.rightMargin: 0
radius: 2
z: -1
}
}
}
}
// TODO: Use Row
StatusEmoji {
id: emojiImage
anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left
anchors.leftMargin: root.imageMargin
width: 15
height: 15
source: {
if (model.emojiId >= 1 && model.emojiId <= root.icons.length)
return root.icons[model.emojiId - 1];
return "";
}
}
StatusBaseText {
id: emojiCount
text: model.numberOfReactions
anchors.verticalCenter: parent.verticalCenter
anchors.left: emojiImage.right
anchors.leftMargin: root.imageMargin
font.pixelSize: 12
color: model.didIReactWithThisEmoji ? Theme.palette.primaryColor1 : Theme.palette.directColor1
}
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onEntered: {
root.hoverChanged(true)
}
onExited: {
root.hoverChanged(false)
}
onClicked: {
root.toggleReaction(model.emojiId)
}
}
}
}
Item {
width: addEmojiButton.width + addEmojiButton.anchors.leftMargin // there is more margin between the button and the emojis than between each emoji
height: addEmojiButton.height
StatusIcon {
id: addEmojiButton
property bool isHovered: false // TODO: Replace with mouseArea.containsMouse
anchors.left: parent.left
anchors.leftMargin: 2.5
icon: "reaction-b"
width: 16.5
height: 16.5
color: addEmojiButton.isHovered ? Theme.palette.primaryColor1 : Theme.palette.baseColor1
}
MouseArea {
id: addEmojiButtonMouseArea
anchors.fill: addEmojiButton
cursorShape: Qt.PointingHandCursor
hoverEnabled: true
onEntered: addEmojiButton.isHovered = true
onExited: addEmojiButton.isHovered = false
onClicked: {
root.addEmojiClicked(this, mouse);
}
}
StatusToolTip {
visible: addEmojiButton.isHovered
text: qsTr("Add reaction")
}
}
}
}

View File

@ -3,36 +3,39 @@ import QtQuick.Layouts 1.14
import StatusQ.Core 0.1
import StatusQ.Core.Theme 0.1
import StatusQ.Core.Utils 0.1
import StatusQ.Components 0.1
import StatusQ.Controls 0.1
Item {
id: statusMessageHeader
id: root
property StatusMessageSenderDetails sender: StatusMessageSenderDetails { }
property alias displayNameLabel: primaryDisplayName
property alias secondaryNameLabel: secondaryDisplayName
property alias tertiaryDetailsLabel: tertiaryDetailText
property alias timestamp: timestampText
property string displayName: ""
property string secondaryName: ""
property string tertiaryDetail: ""
property string tertiaryDetail: sender.id
property string resendText: ""
property bool showResendButton: false
property bool isContact: false
property var trustIndicator: StatusContactVerificationIcons.TrustedType.None
property bool isContact: sender.isContact
property int trustIndicator: sender.trustIndicator
property bool amISender: false
signal clicked()
signal clicked(var sender, var mouse)
signal resendClicked()
height: childrenRect.height
width: primaryDisplayName.width + (secondaryDisplayName.visible ? secondaryDisplayName.width + header.spacing : 0)
implicitHeight: layout.implicitHeight
implicitWidth: layout.implicitWidth
RowLayout {
id: header
id: layout
spacing: 4
TextEdit {
id: primaryDisplayName
Layout.alignment: Qt.AlignBottom
font.family: Theme.palette.baseFont.name
font.weight: Font.Medium
font.pixelSize: 15
@ -41,7 +44,7 @@ Item {
wrapMode: Text.WordWrap
selectByMouse: true
color: Theme.palette.primaryColor1
text: displayName
text: root.amISender ? qsTr("You") : root.sender.displayName
MouseArea {
id: mouseArea
anchors.fill: parent
@ -49,47 +52,45 @@ Item {
acceptedButtons: Qt.LeftButton | Qt.RightButton
hoverEnabled: true
onClicked: {
statusMessageHeader.clicked()
root.clicked(this, mouse)
}
}
Layout.alignment: Qt.AlignBottom
}
StatusContactVerificationIcons {
isContact: statusMessageHeader.isContact
trustIndicator: statusMessageHeader.trustIndicator
visible: !root.amISender
isContact: root.isContact
trustIndicator: root.trustIndicator
}
StatusBaseText {
id: secondaryDisplayName
Layout.alignment: Qt.AlignVCenter
visible: !root.amISender && !!root.sender.secondaryName
color: Theme.palette.baseColor1
font.pixelSize: 10
text: secondaryName
visible: !!text
text: `(${root.sender.secondaryName})`
}
StatusBaseText {
id: dotSeparator1
Layout.fillHeight: true
Layout.alignment: Qt.AlignVCenter
visible: secondaryDisplayName.visible
font.pixelSize: 10
color: Theme.palette.baseColor1
text: "."
visible: secondaryDisplayName.visible
text: "•"
}
StatusBaseText {
id: tertiaryDetailText
visible: !root.amISender
Layout.alignment: Qt.AlignVCenter
Layout.maximumWidth: 58
font.pixelSize: 10
elide: Text.ElideMiddle
color: Theme.palette.baseColor1
text: tertiaryDetail
text: Utils.elideText(tertiaryDetail, 5, 3)
}
StatusBaseText {
id: dotSeparator2
Layout.fillHeight: true
Layout.alignment: Qt.AlignVCenter
visible: tertiaryDetailText.visible
font.pixelSize: 10
color: Theme.palette.baseColor1
text: "."
visible: tertiaryDetailText.visible
text: "•"
}
StatusTimeStampLabel {
id: timestampText
@ -98,12 +99,12 @@ Item {
Layout.alignment: Qt.AlignVCenter
color: Theme.palette.dangerColor1
font.pixelSize: 12
text: statusMessageHeader.resendText
text: root.resendText
visible: showResendButton && !!timestampText.text
MouseArea {
cursorShape: Qt.PointingHandCursor
anchors.fill: parent
onClicked: statusMessageHeader.resendClicked()
onClicked: root.resendClicked()
}
}
}

View File

@ -6,29 +6,29 @@ import StatusQ.Controls 0.1
import StatusQ.Core.Theme 0.1
Rectangle {
id: buttonsContainer
id: root
property list<Item> quickActions
property list<Item> items
QtObject {
id: _internal
readonly property int containerMargin: 2
}
width: buttonRow.width + _internal.containerMargin * 2
height: 36
implicitWidth: buttonRow.width + _internal.containerMargin * 2
implicitHeight: 36
radius: 8
color: Theme.palette.statusSelect.menuItemBackgroundColor
layer.enabled: true
layer.effect: DropShadow {
width: buttonsContainer.width
height: buttonsContainer.height
x: buttonsContainer.x
y: buttonsContainer.y + 10
width: root.width
height: root.height
x: root.x
y: root.y + 10
horizontalOffset: 0
verticalOffset: 2
source: buttonsContainer
source: root
radius: 10
samples: 15
color: Theme.palette.dropShadow
@ -39,13 +39,13 @@ Rectangle {
spacing: _internal.containerMargin
anchors.left: parent.left
anchors.leftMargin: _internal.containerMargin
anchors.verticalCenter: buttonsContainer.verticalCenter
anchors.verticalCenter: root.verticalCenter
height: parent.height - 2 * _internal.containerMargin
}
onQuickActionsChanged: {
for (let idx in quickActions) {
quickActions[idx].parent = buttonRow
onItemsChanged: {
for (let idx in items) {
items[idx].parent = buttonRow
}
}
}

View File

@ -7,18 +7,19 @@ import StatusQ.Core 0.1
import StatusQ.Core.Theme 0.1
import StatusQ.Components 0.1
Loader {
id: chatReply
Item {
id: root
property StatusMessageDetails replyDetails
property string audioMessageInfoText: ""
signal replyProfileClicked()
signal replyProfileClicked(var sender, var mouse)
active: visible
implicitHeight: layout.implicitHeight
implicitWidth: layout.implicitWidth
sourceComponent: RowLayout {
id: replyLayout
RowLayout {
id: layout
spacing: 8
Shape {
id: replyCorner
@ -56,13 +57,16 @@ Loader {
StatusSmartIdenticon {
id: profileImage
Layout.alignment: Qt.AlignTop
image: replyDetails.profileImage
name: replyDetails.displayName
name: replyDetails.sender.userName
image: replyDetails.sender.profileImage.imageSettings
icon: replyDetails.sender.profileImage.iconSettings
ringSettings: replyDetails.sender.profileImage.ringSettings
MouseArea {
cursorShape: Qt.PointingHandCursor
acceptedButtons: Qt.LeftButton | Qt.RightButton
anchors.fill: parent
onClicked: replyProfileClicked()
onClicked: replyProfileClicked(this, mouse)
}
}
TextEdit {
@ -74,7 +78,7 @@ Loader {
font.weight: Font.Medium
selectByMouse: true
readOnly: true
text: replyDetails.displayName
text: replyDetails.amISender ? qsTr("You") : replyDetails.sender.displayName
}
}
StatusTextMessage {
@ -85,13 +89,14 @@ Loader {
textField.height: 18
clip: true
visible: !!replyDetails.messageText
allowShowMore: false
}
StatusImageMessage {
Layout.fillWidth: true
Layout.preferredHeight: imageAlias.paintedHeight
imageWidth: 56
source: replyDetails.contentType === StatusMessage.ContentType.Image ? replyDetails.messageContent : ""
visible: replyDetails.contentType === StatusMessage.ContentType.Image
// visible: replyDetails.contentType === StatusMessage.ContentType.Image
shapeType: StatusImageMessage.ShapeType.ROUNDED
}
Item {
@ -116,7 +121,7 @@ Loader {
height: 22
isPreview: true
audioSource: replyDetails.messageContent
audioMessageInfoText: chatReply.audioMessageInfoText
audioMessageInfoText: root.audioMessageInfoText
}
}
}

View File

@ -1,4 +1,5 @@
import QtQuick 2.13
import QtQuick.Controls 2.14
import QtQuick.Layouts 1.14
import QtGraphicalEffects 1.13
@ -11,14 +12,34 @@ Loader {
active: visible
sourceComponent: Rectangle {
height: 24
width: layout.width + 16
color: Theme.palette.pinColor2
radius: 12
RowLayout {
id: layout
anchors.centerIn: parent
sourceComponent: Control {
verticalPadding: 3
leftPadding: 2
rightPadding: 6
background: Rectangle {
readonly property color translucentColor: Theme.palette.pinColor2
implicitWidth: 24
implicitHeight: 24
color: Qt.rgba(translucentColor.r,
translucentColor.g,
translucentColor.b, 1)
opacity: translucentColor.a
layer.enabled: true
radius: 12
Rectangle {
anchors.bottom: parent.bottom
anchors.left: parent.left
width: parent.width / 2
height: parent.height / 2
color: parent.color
radius: 4
}
}
contentItem: RowLayout {
StatusIcon {
Layout.alignment: Qt.AlignVCenter
Layout.preferredWidth: 16

View File

@ -6,24 +6,33 @@ import StatusQ.Controls 0.1
import StatusQ.Core.Theme 0.1
Item {
id: textMessage
id: root
property int contentType: 0
property alias textField: chatText
property bool allowShowMore: true
signal linkActivated(url link)
signal linkActivated(string link)
implicitHeight: showMoreLoader.active ? childrenRect.height : chatText.height
implicitWidth: chatText.implicitWidth
implicitHeight: chatText.effectiveHeight + d.showMoreHeight
QtObject {
id: _internal
id: d
property bool readMore: false
property bool veryLongChatText: chatText.length > 1000
readonly property bool veryLongChatText: chatText.length > 1000
readonly property int showMoreHeight: showMoreLoader.visible ? showMoreLoader.height : 0
}
TextEdit {
id: chatText
visible: !showMoreLoader.active || _internal.readMore
readonly property int effectiveHeight: d.veryLongChatText && !d.readMore ? Math.min(chatText.implicitHeight, 200)
: chatText.implicitHeight
width: parent.width
height: effectiveHeight + d.showMoreHeight / 2
visible: !opMask.active
clip: true
selectedTextColor: Theme.palette.directColor1
selectionColor: Theme.palette.primaryColor3
color: Theme.palette.directColor1
@ -33,12 +42,12 @@ Item {
wrapMode: Text.Wrap
readOnly: true
selectByMouse: true
height: _internal.veryLongChatText && !_internal.readMore ? Math.min(implicitHeight, 200) : implicitHeight
width: parent.width
clip: height < implicitHeight
onLinkActivated: textMessage.linkActivated(link)
onLinkActivated: {
root.linkActivated(link);
}
onLinkHovered: {
cursorShape: Qt.PointingHandCursor
// Strange thing. Without this empty stub the cursorShape
// is not changed to pointingHandCursor.
}
}
@ -60,7 +69,7 @@ Item {
Loader {
id: opMask
active: showMoreLoader.active && !_internal.readMore
active: showMoreLoader.active && !d.readMore
anchors.fill: chatText
sourceComponent: OpacityMask {
source: chatText
@ -70,17 +79,17 @@ Item {
Loader {
id: showMoreLoader
active: _internal.veryLongChatText
anchors.top: chatText.bottom
anchors.topMargin: -10
active: root.allowShowMore && d.veryLongChatText
visible: active
anchors.verticalCenter: chatText.bottom
anchors.horizontalCenter: parent.horizontalCenter
sourceComponent: StatusRoundButton {
implicitWidth: 24
implicitHeight: 24
type: StatusRoundButton.Type.Secondary
icon.name: _internal.readMore ? "chevron-up": "chevron-down"
icon.name: d.readMore ? "chevron-up": "chevron-down"
onClicked: {
_internal.readMore = !_internal.readMore
d.readMore = !d.readMore
}
}
}

View File

@ -13,6 +13,7 @@ StatusChatToolBar 0.1 StatusChatToolBar.qml
StatusContactRequestsIndicatorListItem 0.1 StatusContactRequestsIndicatorListItem.qml
StatusEmoji 0.1 StatusEmoji.qml
StatusContactVerificationIcons 0.1 StatusContactVerificationIcons.qml
StatusDateGroupLabel 0.1 StatusDateGroupLabel.qml
StatusDescriptionListItem 0.1 StatusDescriptionListItem.qml
StatusLetterIdenticon 0.1 StatusLetterIdenticon.qml
StatusListItem 0.1 StatusListItem.qml
@ -31,6 +32,7 @@ StatusExpandableItem 0.1 StatusExpandableItem.qml
StatusSmartIdenticon 0.1 StatusSmartIdenticon.qml
StatusMessage 0.1 StatusMessage.qml
StatusMessageDetails 0.1 StatusMessageDetails.qml
StatusMessageSenderDetails 0.1 StatusMessageSenderDetails.qml
StatusTagSelector 0.1 StatusTagSelector.qml
StatusToastMessage 0.1 StatusToastMessage.qml
StatusWizardStepper 0.1 StatusWizardStepper.qml

View File

@ -0,0 +1,40 @@
import QtQuick 2.0
import StatusQ.Core.Theme 0.1
QtObject {
id: root
property url source
property int width
property int height
property bool isIdenticon: false
property string name
property string pubkey
property string image
property bool showRing: true
property bool interactive: true
property int colorId // TODO: default value Utils.colorIdForPubkey(pubkey)
property var colorHash // TODO: default value Utils.getColorHashAsJson(pubkey)
property StatusImageSettings imageSettings: StatusImageSettings {
width: root.width
height: root.height
source: root.source
}
readonly property StatusIconSettings iconSettings: StatusIconSettings {
width: root.width
height: root.height
color: Theme.palette.userCustomizationColors[root.colorId]
charactersLen: 2
}
readonly property StatusIdenticonRingSettings ringSettings: StatusIdenticonRingSettings {
initalAngleRad: 0
ringPxSize: Math.max(1.5, root.width / 24.0)
ringSpecModel: root.showRing ? root.colorHash : undefined
distinctiveColors: Theme.palette.identiconRingColors
}
}

View File

@ -154,5 +154,12 @@ ThemePalette {
property color menuItemBackgroundColor: baseColor2
property color menuItemHoverBackgroundColor: directColor7
}
property QtObject statusMessage: QtObject {
property color emojiReactionBackground: "#2d2823"
property color emojiReactionBackgroundHovered: "#3a3632"
property color emojiReactionActiveBackground: "#353a4d"
property color emojiReactionActiveBackgroundHovered: "#cbd5f1"
}
}

View File

@ -152,5 +152,12 @@ ThemePalette {
property color menuItemBackgroundColor: white
property color menuItemHoverBackgroundColor: baseColor2
}
property QtObject statusMessage: QtObject {
property color emojiReactionBackground: "#e2e6e9"
property color emojiReactionBackgroundHovered: "#d7dadd"
property color emojiReactionActiveBackground: getColor('blue6')
property color emojiReactionActiveBackgroundHovered: "#cbd5f1"
}
}

View File

@ -243,7 +243,8 @@ QtObject {
}
function getColor(name, alpha) {
return !!alpha ? alphaColor(StatusColors.colors[name], alpha) : StatusColors.colors[name]
return !!alpha ? alphaColor(StatusColors.colors[name], alpha)
: StatusColors.colors[name]
}
}

View File

@ -1,6 +1,8 @@
pragma Singleton
import QtQuick 2.13
import StatusQ.Core.Theme 0.1
import "./xss.js" as XSS
QtObject {
@ -151,6 +153,71 @@ QtObject {
}
}
function linkifyAndXSS(inputText) {
//URLs starting with http://, https://, or ftp://
var replacePattern1 = /(\b(https?|ftp|statusim):\/\/[-A-Z0-9+&@#\/%?=~_|!:,.;]*[-A-Z0-9+&@#\/%=~_|])/gim;
var replacedText = inputText.replace(replacePattern1, "<a href='$1'>$1</a>");
//URLs starting with "www." (without // before it, or it'd re-link the ones done above).
var replacePattern2 = /(^|[^\/])(www\.[\S]+(\b|$))/gim;
replacedText = replacedText.replace(replacePattern2, "$1<a href='http://$2'>$2</a>");
return XSS.filterXSS(replacedText)
}
function filterXSS(inputText) {
return XSS.filterXSS(inputText)
}
function getMessageWithStyle(msg, hoveredLink = "") {
return `<style type="text/css">` +
`img, a, del, code, blockquote { margin: 0; padding: 0; }` +
`code {` +
`font-family: ${Theme.palette.codeFont.name};` +
`font-weight: 400;` +
`font-size: 14;` +
`padding: 2px 4px;` +
`border-radius: 4px;` +
`background-color: ${Theme.palette.baseColor2};` +
`color: ${Theme.palette.directColor1};` +
`white-space: pre;` +
`}` +
`p {` +
`line-height: 22px;` +
`}` +
`a {` +
`color: ${Theme.palette.primaryColor1};` +
`}` +
`a.mention {` +
`color: ${Theme.palette.mentionColor1};` +
`background-color: ${Theme.palette.mentionColor4};` +
`text-decoration: none;` +
`padding: 0px 2px;` +
`}` +
(hoveredLink !== "" ? `a.mention[href="${hoveredLink}"] { background-color: ${Theme.palette.mentionColor2}; }` : ``) +
`del {` +
`text-decoration: line-through;` +
`}` +
`table.blockquote td {` +
`padding-left: 10px;` +
`color: ${Theme.palette.baseColor1};` +
`}` +
`table.blockquote td.quoteline {` +
`background-color: ${Theme.palette.baseColor1};` +
`height: 100%;` +
`padding-left: 0;` +
`}` +
`.emoji {` +
`vertical-align: bottom;` +
`}` +
`span.isEdited {` +
`color: ${Theme.palette.baseColor1};` +
`margin-left: 5px` +
`}` +
`</style>` +
`${msg}`
}
function delegateModelSort(srcGroup, dstGroup, lessThan) {
const insertPosition = (lessThan, item) => {
let lower = 0
@ -173,6 +240,10 @@ QtObject {
dstGroup.move(item.itemsIndex, index)
}
}
function elideText(text, leftCharsCount, rightCharsCount = leftCharsCount) {
return text.substr(0, leftCharsCount) + "..." + text.substr(text.length - rightCharsCount)
}
}

View File

@ -1,4 +1,6 @@
module StatusQ.Core.Utils
EmojiJSON 1.0 emojiList.js
XSS 1.0 xss.js
singleton Utils 0.1 Utils.qml
singleton Emoji 0.1 Emoji.qml

File diff suppressed because it is too large Load Diff

View File

@ -14,3 +14,4 @@ StatusAnimatedStack 0.1 StatusAnimatedStack.qml
StatusScrollView 0.1 StatusScrollView.qml
StatusListView 0.1 StatusListView.qml
StatusGridView 0.1 StatusGridView.qml
StatusProfileImageSettings 0.1 StatusProfileImageSettings.qml