feat: display message outgoing state (#15450)

* chore: storybook page

* feat: propagate outgoing status to StatusMessageHeader

* feat: improve message outgoing status UI

* fix: lock message `delivered` state
This commit is contained in:
Igor Sirotin 2024-07-08 13:26:04 +01:00 committed by GitHub
parent 2f1240602f
commit 8b80cfb9e3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 222 additions and 70 deletions

View File

@ -546,6 +546,8 @@ QtObject:
let ind = self.findIndexForMessageId(messageId)
if(ind == -1):
return
if self.items[ind].outgoingStatus == PARSED_TEXT_OUTGOING_STATUS_DELIVERED:
return
self.items[ind].outgoingStatus = status
let index = self.createIndex(ind, 0, nil)
defer: index.delete

View File

@ -29,6 +29,7 @@ SplitView {
isContact: true
isAReply: false
trustIndicator: StatusContactVerificationIcons.TrustedType.Verified
outgoingStatus: StatusMessage.OutgoingStatus.Delivered
}
ListElement {
timestamp: 1657937930135
@ -39,6 +40,7 @@ SplitView {
isContact: false
isAReply: false
trustIndicator: StatusContactVerificationIcons.TrustedType.Untrustworthy
outgoingStatus: StatusMessage.OutgoingStatus.Delivered
}
ListElement {
timestamp: 1667937930159
@ -49,6 +51,7 @@ SplitView {
isContact: true
isAReply: true
trustIndicator: StatusContactVerificationIcons.TrustedType.None
outgoingStatus: StatusMessage.OutgoingStatus.Delivered
}
ListElement {
timestamp: 1667937930489
@ -59,6 +62,71 @@ SplitView {
isContact: true
isAReply: true
trustIndicator: StatusContactVerificationIcons.TrustedType.None
outgoingStatus: StatusMessage.OutgoingStatus.Delivered
}
ListElement {
timestamp: 1719769718000
senderId: "zq123456790"
senderDisplayName: "Alice"
contentType: StatusMessage.ContentType.Text
message: "Sending message"
isAReply: false
isContact: true
amISender: true
trustIndicator: StatusContactVerificationIcons.TrustedType.None
outgoingStatus: StatusMessage.OutgoingStatus.Sending
}
ListElement {
timestamp: 1719769718000
senderId: "zq123456790"
senderDisplayName: "Alice"
contentType: StatusMessage.ContentType.Text
message: "Sent message"
isAReply: false
isContact: true
amISender: true
trustIndicator: StatusContactVerificationIcons.TrustedType.None
outgoingStatus: StatusMessage.OutgoingStatus.Sent
resendError: ""
}
ListElement {
timestamp: 1719769718000
senderId: "zq123456790"
senderDisplayName: "Alice"
contentType: StatusMessage.ContentType.Text
message: "Delivered message"
isAReply: false
isContact: true
amISender: true
trustIndicator: StatusContactVerificationIcons.TrustedType.None
outgoingStatus: StatusMessage.OutgoingStatus.Delivered
resendError: ""
}
ListElement {
timestamp: 1719769718000
senderId: "zq123456790"
senderDisplayName: "Alice"
contentType: StatusMessage.ContentType.Text
message: "Expired message"
isAReply: false
isContact: true
amISender: true
trustIndicator: StatusContactVerificationIcons.TrustedType.None
outgoingStatus: StatusMessage.OutgoingStatus.Expired
resendError: ""
}
ListElement {
timestamp: 1719769718000
senderId: "zq123456790"
senderDisplayName: "Alice"
contentType: StatusMessage.ContentType.Text
message: "Message with resend error"
isAReply: false
isContact: true
amISender: true
trustIndicator: StatusContactVerificationIcons.TrustedType.None
outgoingStatus: StatusMessage.OutgoingStatus.Expired
resendError: "can't send message on Tuesday"
}
}
readonly property var colorHash: ListModel {
@ -90,10 +158,15 @@ SplitView {
delegate: StatusMessage {
width: ListView.view.width
timestamp: model.timestamp
isAReply: model.isAReply
outgoingStatus: model.outgoingStatus
resendError: model.outgoingStatus === StatusMessage.OutgoingStatus.Expired ? model.resendError : ""
messageDetails {
readonly property bool isEnsVerified: model.senderDisplayName.endsWith(".eth")
messageText: model.message
contentType: model.contentType
amISender: model.amISender
sender.id: isEnsVerified ? "" : model.senderId
sender.displayName: model.senderDisplayName
sender.isContact: model.isContact
@ -106,7 +179,6 @@ SplitView {
}
}
isAReply: model.isAReply
replyDetails {
amISender: true
sender.id: "0xdeadbeef"
@ -123,6 +195,7 @@ SplitView {
onProfilePictureClicked: logs.logEvent("StatusMessage::profilePictureClicked")
onReplyProfileClicked: logs.logEvent("StatusMessage::replyProfileClicked")
onReplyMessageClicked: logs.logEvent("StatusMessage::replyMessageClicked")
onResendClicked: logs.logEvent("StatusMessage::resendClicked")
}
}
}

View File

@ -29,6 +29,14 @@ Control {
BridgeMessage = 18
}
enum OutgoingStatus {
Unknown = 0,
Sending,
Sent,
Delivered,
Expired
}
property list<Item> quickActions
property var statusChatInput
property alias linksComponent: linksLoader.sourceComponent
@ -51,9 +59,8 @@ Control {
property bool hasMention: false
property bool isPinned: false
property string pinnedBy: ""
property bool hasExpired: false
property bool isSending: false
property string resendError: ""
property int outgoingStatus: StatusMessage.OutgointStatus.Unknown
property double timestamp: 0
property var reactionsModel: []
@ -111,6 +118,7 @@ Control {
}
hoverEnabled: (!root.isActiveMessage && !root.disableHover)
opacity: outgoingStatus === StatusMessage.OutgoingStatus.Sending ? 0.5 : 1.0
background: Rectangle {
color: {
if (root.overrideBackground)
@ -254,14 +262,14 @@ Control {
sender: root.messageDetails.sender
amISender: root.messageDetails.amISender
messageOriginInfo: root.messageDetails.messageOriginInfo
showResendButton: root.hasExpired && root.messageDetails.amISender && !editMode && !root.isInPinnedPopup
showSendingLoader: root.isSending && root.messageDetails.amISender && !editMode
resendError: root.messageDetails.amISender && !editMode ? root.resendError : ""
resendError: root.messageDetails.amISender ? root.resendError : ""
onClicked: root.senderNameClicked(sender, mouse)
onResendClicked: root.resendClicked()
timestamp: root.timestamp
showFullTimestamp: root.isInPinnedPopup
displayNameClickable: root.profileClickable
outgoingStatus: root.outgoingStatus
showOutgointStatusLabel: root.hovered && !root.isInPinnedPopup
}
}
Loader {

View File

@ -18,9 +18,6 @@ Item {
property double timestamp: 0
property string tertiaryDetail: sender.id
property string resendText: qsTr("Resend")
property bool showResendButton: false
property bool showSendingLoader: false
property string resendError: ""
property bool isContact: sender.isContact
property int trustIndicator: sender.trustIndicator
@ -28,6 +25,8 @@ Item {
property bool displayNameClickable: true
property string messageOriginInfo: ""
property bool showFullTimestamp
property int outgoingStatus: StatusMessage.OutgoingStatus.Unknown
property bool showOutgointStatusLabel: false
signal clicked(var sender, var mouse)
signal resendClicked()
@ -35,6 +34,13 @@ Item {
implicitHeight: layout.implicitHeight
implicitWidth: layout.implicitWidth
QtObject {
id: d
readonly property bool expired: root.outgoingStatus === StatusMessage.OutgoingStatus.Expired
readonly property color outgoingStatusColor: expired ? Theme.palette.warningColor1 : Theme.palette.baseColor1
}
RowLayout {
id: layout
spacing: 4
@ -133,56 +139,12 @@ Item {
}
StatusTimeStampLabel {
verticalAlignment: Text.AlignVCenter
id: timestampText
verticalAlignment: Text.AlignVCenter
timestamp: root.timestamp
showFullTimestamp: root.showFullTimestamp
}
Loader {
id: resendButtonLoader
active: showResendButton && !!timestampText.text
asynchronous: true
sourceComponent: StatusBaseText {
id: resendButton
verticalAlignment: Text.AlignVCenter
color: Theme.palette.dangerColor1
font.pixelSize: Theme.tertiaryTextFontSize
text: root.resendText
MouseArea {
cursorShape: Qt.PointingHandCursor
anchors.fill: parent
onClicked: root.resendClicked()
}
}
}
Loader {
id: resendErrorTextLoader
active: resendError && !!timestampText.text
asynchronous: true
sourceComponent: StatusBaseText {
id: resendErrorText
verticalAlignment: Text.AlignVCenter
color: Theme.palette.baseColor1
font.pixelSize: Theme.tertiaryTextFontSize
text: qsTr("Failed to resend: %1").arg(resendError) // TODO replace this with the required design
}
}
Loader {
id: sendingInProgressLoader
active: showSendingLoader && !!timestampText.text
asynchronous: true
sourceComponent: StatusBaseText {
id: sendingInProgress
verticalAlignment: Text.AlignVCenter
color: Theme.palette.baseColor1
font.pixelSize: Theme.tertiaryTextFontSize
text: qsTr("Sending...") // TODO replace this with the required design
}
}
Component {
id: dotComponent
StatusBaseText {
@ -194,6 +156,79 @@ Item {
}
}
Loader {
id: deliveryStatusLoader
Layout.alignment: Qt.AlignVCenter
active: root.outgoingStatus !== StatusMessage.OutgoingStatus.Unknown
asynchronous: true
sourceComponent: RowLayout {
spacing: 0
StatusIcon {
Layout.preferredHeight: 15
Layout.preferredWidth: 15
Layout.alignment: Qt.AlignVCenter
color: d.outgoingStatusColor
icon: {
if (root.resendError != "")
return "tiny/tiny-exclamation"
switch (root.outgoingStatus) {
case StatusMessage.OutgoingStatus.Delivered:
return "tiny/message/delivered"
case StatusMessage.OutgoingStatus.Sent:
return "tiny/message/sent"
case StatusMessage.OutgoingStatus.Sending:
return "tiny/pending"
case StatusMessage.OutgoingStatus.Expired:
return "tiny/tiny-exclamation"
default:
return ""
}
}
}
Loader {
active: root.showOutgointStatusLabel
asynchronous: true
sourceComponent: StatusBaseText {
Layout.alignment: Qt.AlignVCenter
color: d.outgoingStatusColor
font.pixelSize: Theme.asideTextFontSize
text: {
if (root.resendError != "")
return qsTr("Failed to resend: %1").arg(root.resendError)
switch (root.outgoingStatus) {
case StatusMessage.OutgoingStatus.Delivered:
return qsTr("Delivered")
case StatusMessage.OutgoingStatus.Sent:
return qsTr("Sent")
case StatusMessage.OutgoingStatus.Sending:
return qsTr("Sending")
case StatusMessage.OutgoingStatus.Expired:
return qsTr("Sending failed")
default:
return ""
}
}
}
}
}
}
Loader {
id: resendButtonLoader
active: root.showOutgointStatusLabel && d.expired
asynchronous: true
sourceComponent: StatusButton {
Layout.fillHeight: true
verticalPadding: 1
horizontalPadding: 5
size: StatusBaseButton.Tiny
type: StatusBaseButton.Warning
font.pixelSize: 9
text: qsTr("Resend")
onClicked: root.resendClicked()
}
}
Item {
Layout.fillWidth: true
}

View File

@ -124,7 +124,7 @@ Loader {
property string deletedByContactIcon: ""
property string deletedByContactColorHash: ""
property bool shouldRepeatHeader: d.getShouldRepeatHeader(messageTimestamp, prevMessageTimestamp, messageOutgoingStatus)
property bool shouldRepeatHeader: d.shouldRepeatHeader
property bool hasMention: false
@ -143,9 +143,6 @@ Loader {
property bool isMessage: isEmoji || isImage || isSticker || isText || isAudio
|| messageContentType === Constants.messageContentType.communityInviteType || messageContentType === Constants.messageContentType.transactionType
readonly property bool isExpired: d.getIsExpired(messageTimestamp, messageOutgoingStatus)
readonly property bool isSending: messageOutgoingStatus === Constants.sending && !isExpired
function openProfileContextMenu(sender, mouse, isReply = false) {
if (isReply && !quotedMessageFrom) {
// The responseTo message was deleted
@ -274,8 +271,8 @@ Loader {
readonly property bool canPost: root.chatContentModule.chatDetails.canPost
readonly property bool canView: canPost || root.chatContentModule.chatDetails.canView
function nextMessageHasHeader() {
if(!root.nextMessageAsJsonObj) {
function getNextMessageHasHeader() {
if (!root.nextMessageAsJsonObj) {
return false
}
return root.senderId !== root.nextMessageAsJsonObj.senderId ||
@ -284,12 +281,27 @@ Loader {
}
function getShouldRepeatHeader(messageTimeStamp, prevMessageTimeStamp, messageOutgoingStatus) {
return ((messageTimeStamp - prevMessageTimeStamp) / 60 / 1000) > Constants.repeatHeaderInterval
return ((messageTimeStamp - prevMessageTimeStamp) / 60 / 1000) > Constants.repeatHeaderInterval
|| d.getIsExpired(messageTimeStamp, messageOutgoingStatus)
}
function getIsExpired(messageTimeStamp, messageOutgoingStatus) {
return (messageOutgoingStatus === Constants.sending && (Math.floor(messageTimeStamp) + 180000) < Date.now()) || messageOutgoingStatus === Constants.expired
return (messageOutgoingStatus === Constants.messageOutgoingStatus.sending && (Math.floor(messageTimeStamp) + 180000) < Date.now())
|| messageOutgoingStatus === Constants.expired
}
property bool isExpired: false
property bool shouldRepeatHeader: false
property bool nextMessageHasHeader: false
Component.onCompleted: {
onTimeChanged()
}
function onTimeChanged() {
isExpired = getIsExpired(root.messageTimestamp, root.messageOutgoingStatus)
shouldRepeatHeader = getShouldRepeatHeader(root.messageTimestamp, root.prevMessageTimestamp, root.messageOutgoingStatus)
nextMessageHasHeader = getNextMessageHasHeader()
}
function convertContentType(value) {
@ -332,6 +344,17 @@ Loader {
}
}
function convertOutgoingStatus(value) {
switch (value) {
case Constants.messageOutgoingStatus.sending:
return StatusMessage.OutgoingStatus.Sending
case Constants.messageOutgoingStatus.sent:
return StatusMessage.OutgoingStatus.Sent
case Constants.messageOutgoingStatus.delivered:
return StatusMessage.OutgoingStatus.Delivered
}
}
function addReactionClicked(mouseArea, mouse) {
if (!d.addReactionAllowed)
return
@ -357,6 +380,13 @@ Loader {
}
}
Connections {
target: StatusSharedUpdateTimer
onTriggered: {
d.onTimeChanged()
}
}
Component {
id: gapComponent
GapComponent {
@ -646,8 +676,9 @@ Loader {
return ProfileUtils.displayName(contact.localNickname, contact.name, contact.displayName, contact.alias)
}
isInPinnedPopup: root.isInPinnedPopup
hasExpired: root.isExpired
isSending: root.isSending
outgoingStatus: d.isExpired ? StatusMessage.OutgoingStatus.Expired
: d.convertOutgoingStatus(messageOutgoingStatus)
resendError: root.resendError
reactionsModel: root.reactionsModel
linkPreviewModel: root.linkPreviewModel
@ -663,7 +694,7 @@ Loader {
root.senderId !== root.prevMessageSenderId || root.prevMessageDeleted
isActiveMessage: d.isMessageActive
topPadding: showHeader ? Style.current.halfPadding : 0
bottomPadding: showHeader && d.nextMessageHasHeader() ? Style.current.halfPadding : 2
bottomPadding: showHeader && d.nextMessageHasHeader ? Style.current.halfPadding : 2
disableHover: root.disableHover ||
(delegate.hideQuickActions && !d.addReactionAllowed) ||
(root.chatLogView && root.chatLogView.moving) ||

View File

@ -2,6 +2,7 @@ pragma Singleton
import QtQuick 2.13
import StatusQ.Components 0.1
import StatusQ.Controls.Validators 0.1
import StatusQ.Core.Theme 0.1
@ -1202,11 +1203,13 @@ QtObject {
}
// Message outgoing status
readonly property string sending: "sending"
readonly property string sent: "sent"
readonly property string delivered: "delivered"
readonly property string expired: "expired"
readonly property string failedResending: "failedResending"
readonly property QtObject messageOutgoingStatus: QtObject {
readonly property string sending: "sending"
readonly property string sent: "sent"
readonly property string delivered: "delivered"
readonly property string expired: "expired"
readonly property string failedResending: "failedResending"
}
readonly property QtObject appTranslatableConstants: QtObject {
readonly property string loginAccountsListAddNewUser: "LOGIN-ACCOUNTS-LIST-ADD-NEW-USER"