mirror of
https://github.com/status-im/status-desktop.git
synced 2025-02-10 21:56:37 +00:00
* fix(badge): fix missing badge on the Chat section * fix(unread): fix unread count not incrementing when the chat is active but app is unfocused Fixes #16098 The problem was that we were marking the message as read because the chat kept scrolling, even if the app was in the background. I fixed that by only marking as read if the app is active. I added a Connections to the active property of the Applicaiton too to mark as read when the app comes back active. I also removed a condition that prevented the Unread bar appearing in that condition. Now, when a message is sent to the active chat, but the app is not in focus, the red dot appears, as well as the badges. Then when the app comes active, it is marked as read, but the unread messages line is shown to show when is the last time the user saw messages. This is similar to what Discord has.
464 lines
16 KiB
QML
464 lines
16 KiB
QML
import QtQuick 2.15
|
|
import QtQml 2.15
|
|
import QtQuick.Controls 2.15
|
|
import QtQuick.Layouts 1.15
|
|
import QtQuick.Dialogs 1.3
|
|
|
|
import StatusQ.Components 0.1
|
|
import StatusQ.Controls 0.1
|
|
import StatusQ.Core 0.1
|
|
import StatusQ.Core.Backpressure 0.1
|
|
import StatusQ.Core.Theme 0.1
|
|
|
|
import utils 1.0
|
|
import shared 1.0
|
|
import shared.stores 1.0 as SharedStores
|
|
import shared.views 1.0
|
|
import shared.panels 1.0
|
|
import shared.popups 1.0
|
|
import shared.status 1.0
|
|
import shared.controls 1.0
|
|
import shared.views.chat 1.0
|
|
|
|
import AppLayouts.Chat.stores 1.0
|
|
import AppLayouts.Profile.stores 1.0
|
|
|
|
import "../controls"
|
|
import "../panels"
|
|
|
|
Item {
|
|
id: root
|
|
|
|
property var chatContentModule
|
|
|
|
property SharedStores.RootStore sharedRootStore
|
|
property SharedStores.UtilsStore utilsStore
|
|
|
|
property RootStore rootStore
|
|
property MessageStore messageStore
|
|
property UsersStore usersStore
|
|
property ContactsStore contactsStore
|
|
property string channelEmoji
|
|
property var formatBalance
|
|
|
|
property var emojiPopup
|
|
property var stickersPopup
|
|
property bool areTestNetworksEnabled
|
|
|
|
property string chatId: ""
|
|
property bool stickersLoaded: false
|
|
property alias chatLogView: chatLogView
|
|
property bool isContactBlocked: false
|
|
property bool isChatBlocked: false
|
|
property bool isOneToOne: false
|
|
|
|
property bool sendViaPersonalChatEnabled
|
|
|
|
signal openStickerPackPopup(string stickerPackId)
|
|
signal showReplyArea(string messageId, string author)
|
|
signal editModeChanged(bool editModeOn)
|
|
|
|
QtObject {
|
|
id: d
|
|
|
|
readonly property real scrollY: chatLogView.visibleArea.yPosition * chatLogView.contentHeight
|
|
readonly property bool isMostRecentMessageInViewport: chatLogView.visibleArea.yPosition >= 0.999 - chatLogView.visibleArea.heightRatio
|
|
readonly property var chatDetails: chatContentModule && chatContentModule.chatDetails || null
|
|
readonly property bool keepUnread: messageStore.keepUnread
|
|
|
|
readonly property var loadMoreMessagesIfScrollBelowThreshold: Backpressure.oneInTimeQueued(root, 100, function() {
|
|
if(scrollY < 1000) messageStore.loadMoreMessages()
|
|
})
|
|
|
|
function setKeepUnread(flag: bool) {
|
|
root.messageStore.setKeepUnread(flag)
|
|
}
|
|
|
|
function markAllMessagesReadIfMostRecentMessageIsInViewport() {
|
|
if (Qt.application.state != Qt.ApplicationActive || !isMostRecentMessageInViewport || !chatLogView.visible || keepUnread) {
|
|
return
|
|
}
|
|
|
|
if (chatDetails && chatDetails.active && (chatDetails.hasUnreadMessages || chatDetails.highlight) && !messageStore.loading) {
|
|
chatContentModule.markAllMessagesRead()
|
|
}
|
|
}
|
|
|
|
function goToMessage(messageIndex) {
|
|
chatLogView.currentIndex = -1
|
|
chatLogView.currentIndex = messageIndex
|
|
}
|
|
|
|
onIsMostRecentMessageInViewportChanged: markAllMessagesReadIfMostRecentMessageIsInViewport()
|
|
}
|
|
|
|
Connections {
|
|
target: Qt.application
|
|
onStateChanged: {
|
|
if (Qt.application.state == Qt.ApplicationActive) {
|
|
d.markAllMessagesReadIfMostRecentMessageIsInViewport()
|
|
}
|
|
}
|
|
}
|
|
|
|
Connections {
|
|
target: root.messageStore.messageModule
|
|
|
|
function onMessageSuccessfullySent() {
|
|
chatLogView.positionViewAtBeginning()
|
|
}
|
|
|
|
function onSendingMessageFailed(error) {
|
|
sendingMsgFailedPopup.error = error
|
|
sendingMsgFailedPopup.open()
|
|
}
|
|
|
|
function onScrollToMessage(messageIndex) {
|
|
d.goToMessage(messageIndex)
|
|
}
|
|
}
|
|
|
|
Connections {
|
|
target: root.messageStore
|
|
|
|
function onMessageSearchOngoingChanged() {
|
|
d.markAllMessagesReadIfMostRecentMessageIsInViewport()
|
|
}
|
|
|
|
function onLoadingChanged() {
|
|
d.markAllMessagesReadIfMostRecentMessageIsInViewport()
|
|
}
|
|
}
|
|
|
|
Connections {
|
|
target: !!d.chatDetails ? d.chatDetails : null
|
|
|
|
function onActiveChanged() {
|
|
d.setKeepUnread(false)
|
|
d.markAllMessagesReadIfMostRecentMessageIsInViewport()
|
|
d.loadMoreMessagesIfScrollBelowThreshold()
|
|
}
|
|
|
|
function onHasUnreadMessagesChanged() {
|
|
if (!d.chatDetails.hasUnreadMessages) {
|
|
return
|
|
}
|
|
|
|
// HACK: we call `addNewMessagesMarker` later because messages model
|
|
// may not be yet propagated with unread messages when this signal is emitted
|
|
if (chatLogView.visible && (Qt.application.state != Qt.ApplicationActive || !d.isMostRecentMessageInViewport)) {
|
|
Qt.callLater(() => messageStore.addNewMessagesMarker())
|
|
}
|
|
}
|
|
}
|
|
|
|
Connections {
|
|
target: root.rootStore
|
|
enabled: d.chatDetails && d.chatDetails.active
|
|
|
|
function onLoadingHistoryMessagesInProgressChanged() {
|
|
if(!root.rootStore.loadingHistoryMessagesInProgress) {
|
|
d.loadMoreMessagesIfScrollBelowThreshold()
|
|
}
|
|
}
|
|
}
|
|
|
|
Item {
|
|
id: loadingMessagesIndicator
|
|
visible: root.rootStore.loadingHistoryMessagesInProgress
|
|
anchors.top: parent.top
|
|
anchors.left: parent.left
|
|
height: visible? 20 : 0
|
|
width: parent.width
|
|
|
|
Loader {
|
|
active: root.rootStore.loadingHistoryMessagesInProgress
|
|
anchors.horizontalCenter: parent.horizontalCenter
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
sourceComponent: Component {
|
|
LoadingAnimation {
|
|
width: 18
|
|
height: 18
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Loader {
|
|
id: loadingMessagesView
|
|
|
|
anchors.top: loadingMessagesIndicator.bottom
|
|
anchors.bottom: parent.bottom
|
|
anchors.left: parent.left
|
|
anchors.right: parent.right
|
|
|
|
active: messageStore.loading
|
|
visible: active
|
|
sourceComponent: MessagesLoadingView {
|
|
anchors.margins: 16
|
|
anchors.fill: parent
|
|
}
|
|
}
|
|
|
|
StatusListView {
|
|
id: chatLogView
|
|
visible: !loadingMessagesView.visible
|
|
objectName: "chatLogView"
|
|
anchors.top: loadingMessagesIndicator.bottom
|
|
anchors.bottom: parent.bottom
|
|
anchors.left: parent.left
|
|
anchors.right: parent.right
|
|
spacing: 0
|
|
verticalLayoutDirection: ListView.BottomToTop
|
|
cacheBuffer: height > 0 ? height * 2 : 0 // cache 2 screens worth of items
|
|
|
|
highlightRangeMode: ListView.ApplyRange
|
|
highlightMoveDuration: 200
|
|
preferredHighlightBegin: 0
|
|
preferredHighlightEnd: chatLogView.height / 2
|
|
|
|
Binding on flickDeceleration {
|
|
when: localAppSettings.isCustomMouseScrollingEnabled
|
|
value: localAppSettings.scrollDeceleration
|
|
restoreMode: Binding.RestoreBindingOrValue
|
|
}
|
|
|
|
Binding on maximumFlickVelocity {
|
|
when: localAppSettings.isCustomMouseScrollingEnabled
|
|
value: localAppSettings.scrollVelocity
|
|
restoreMode: Binding.RestoreBindingOrValue
|
|
}
|
|
|
|
model: messageStore.messagesModel
|
|
|
|
onContentYChanged: d.loadMoreMessagesIfScrollBelowThreshold()
|
|
|
|
onCountChanged: {
|
|
d.markAllMessagesReadIfMostRecentMessageIsInViewport()
|
|
|
|
// after inilial messages are loaded
|
|
// load as much messages as the view requires
|
|
if (!messageStore.loading) {
|
|
d.loadMoreMessagesIfScrollBelowThreshold()
|
|
}
|
|
}
|
|
|
|
onVisibleChanged: d.markAllMessagesReadIfMostRecentMessageIsInViewport()
|
|
|
|
onCurrentItemChanged: {
|
|
if(currentItem && currentIndex > 0) {
|
|
currentItem.startMessageFoundAnimation()
|
|
}
|
|
}
|
|
|
|
ScrollBar.vertical: StatusScrollBar {
|
|
visible: chatLogView.visibleArea.heightRatio < 1
|
|
}
|
|
|
|
ChatAnchorButtonsPanel {
|
|
anchors.bottom: parent.bottom
|
|
anchors.right: parent.right
|
|
anchors.rightMargin: Theme.padding
|
|
|
|
mentionsCount: d.chatDetails ? d.chatDetails.notificationCount : 0
|
|
recentMessagesButtonVisible: {
|
|
chatLogView.contentY // trigger binding on contentY change
|
|
return chatLogView.contentHeight - (d.scrollY + chatLogView.height) > 400
|
|
}
|
|
|
|
onRecentMessagesButtonClicked: chatLogView.positionViewAtBeginning()
|
|
onMentionsButtonClicked: {
|
|
let id = messageStore.firstUnseenMentionMessageId()
|
|
if (id !== "") {
|
|
messageStore.jumpToMessage(id)
|
|
chatContentModule.markMessageRead(id)
|
|
}
|
|
}
|
|
}
|
|
|
|
delegate: MessageView {
|
|
id: msgDelegate
|
|
|
|
width: ListView.view.width
|
|
|
|
objectName: "chatMessageViewDelegate"
|
|
|
|
sharedRootStore: root.sharedRootStore
|
|
utilsStore: root.utilsStore
|
|
rootStore: root.rootStore
|
|
messageStore: root.messageStore
|
|
usersStore: root.usersStore
|
|
contactsStore: root.contactsStore
|
|
channelEmoji: root.channelEmoji
|
|
emojiPopup: root.emojiPopup
|
|
stickersPopup: root.stickersPopup
|
|
chatLogView: ListView.view
|
|
chatContentModule: root.chatContentModule
|
|
formatBalance: root.formatBalance
|
|
|
|
isChatBlocked: root.isChatBlocked
|
|
|
|
sendViaPersonalChatEnabled: root.sendViaPersonalChatEnabled
|
|
areTestNetworksEnabled: root.areTestNetworksEnabled
|
|
|
|
chatId: root.chatId
|
|
messageId: model.id
|
|
communityId: model.communityId
|
|
responseToMessageWithId: model.responseToMessageWithId
|
|
senderId: model.senderId
|
|
senderDisplayName: model.senderDisplayName
|
|
senderOptionalName: model.senderOptionalName
|
|
senderIsEnsVerified: model.senderEnsVerified
|
|
senderIcon: model.senderIcon
|
|
senderColorHash: model.senderColorHash
|
|
senderIsAdded: model.senderIsAdded
|
|
senderTrustStatus: model.senderTrustStatus
|
|
amISender: model.amISender
|
|
messageText: model.messageText
|
|
unparsedText: model.unparsedText
|
|
messageImage: model.messageImage
|
|
album: model.albumMessageImages.split(" ")
|
|
albumCount: model.albumImagesCount
|
|
messageTimestamp: model.timestamp
|
|
messageOutgoingStatus: model.outgoingStatus
|
|
resendError: model.resendError
|
|
messageContentType: model.contentType
|
|
pinnedMessage: model.pinned
|
|
messagePinnedBy: model.pinnedBy
|
|
reactionsModel: model.reactions
|
|
emojiReactionsModel: model.emojiReactionsModel
|
|
sticker: model.sticker
|
|
stickerPack: model.stickerPack
|
|
editModeOn: model.editMode
|
|
onEditModeOnChanged: root.editModeChanged(editModeOn)
|
|
isEdited: model.isEdited
|
|
deleted: model.deleted
|
|
deletedBy: model.deletedBy
|
|
deletedByContactDisplayName: model.deletedByContactDisplayName
|
|
deletedByContactIcon: model.deletedByContactIcon
|
|
deletedByContactColorHash: model.deletedByContactColorHash
|
|
linkPreviewModel: model.linkPreviewModel
|
|
links: model.links
|
|
paymentRequestModel: model.paymentRequestModel
|
|
messageAttachments: model.messageAttachments
|
|
transactionParams: model.transactionParameters
|
|
hasMention: model.mentioned
|
|
quotedMessageText: model.quotedMessageParsedText
|
|
quotedMessageFrom: model.quotedMessageFrom
|
|
quotedMessageContentType: model.quotedMessageContentType
|
|
quotedMessageDeleted: model.quotedMessageDeleted
|
|
quotedMessageAuthorDetailsName: model.quotedMessageAuthorName
|
|
quotedMessageAuthorDetailsDisplayName: model.quotedMessageAuthorDisplayName
|
|
quotedMessageAuthorDetailsThumbnailImage: model.quotedMessageAuthorThumbnailImage
|
|
quotedMessageAuthorDetailsEnsVerified: model.quotedMessageAuthorEnsVerified
|
|
quotedMessageAuthorDetailsIsContact: model.quotedMessageAuthorIsContact
|
|
quotedMessageAuthorDetailsColorHash: model.quotedMessageAuthorColorHash
|
|
quotedMessageAlbumMessageImages: model.quotedMessageAlbumMessageImages.split(" ")
|
|
quotedMessageAlbumImagesCount: model.quotedMessageAlbumImagesCount
|
|
bridgeName: model.bridgeName
|
|
|
|
gapFrom: model.gapFrom
|
|
gapTo: model.gapTo
|
|
|
|
// This is possible since we have all data loaded before we load qml.
|
|
// When we fetch messages to fulfill a gap we have to set them at once.
|
|
// Also one important thing here is that messages are set in descending order
|
|
// in terms of `timestamp` of a message, that means a message with the most
|
|
// recent time is added at index 0.
|
|
prevMessageIndex: model.prevMsgIndex
|
|
prevMessageTimestamp: model.prevMsgTimestamp
|
|
prevMessageSenderId: model.prevMsgSenderId
|
|
prevMessageContentType: model.prevMsgContentType
|
|
prevMessageDeleted: model.prevMsgDeleted
|
|
nextMessageIndex: model.nextMsgIndex
|
|
nextMessageTimestamp: model.nextMsgTimestamp
|
|
|
|
onOpenStickerPackPopup: {
|
|
root.openStickerPackPopup(stickerPackId);
|
|
}
|
|
|
|
onShowReplyArea: {
|
|
root.showReplyArea(messageId, author)
|
|
}
|
|
|
|
stickersLoaded: root.stickersLoaded
|
|
|
|
onVisibleChanged: {
|
|
if(!visible && model.editMode)
|
|
messageStore.setEditModeOff(model.id)
|
|
}
|
|
}
|
|
header: {
|
|
if (!root.isContactBlocked && root.isOneToOne && root.rootStore.oneToOneChatContact) {
|
|
switch (root.rootStore.oneToOneChatContact.contactRequestState) {
|
|
case Constants.ContactRequestState.None: // no break
|
|
case Constants.ContactRequestState.Dismissed:
|
|
return sendContactRequestComponent
|
|
case Constants.ContactRequestState.Received:
|
|
return acceptOrDeclineContactRequestComponent
|
|
case Constants.ContactRequestState.Sent:
|
|
return pendingContactRequestComponent
|
|
default:
|
|
break
|
|
}
|
|
}
|
|
return null
|
|
}
|
|
onHeaderChanged: chatLogView.positionViewAtBeginning()
|
|
}
|
|
|
|
MessageDialog {
|
|
property string error
|
|
|
|
id: sendingMsgFailedPopup
|
|
standardButtons: StandardButton.Ok
|
|
text: qsTr("Failed to send message.\n" + error)
|
|
icon: StandardIcon.Critical
|
|
}
|
|
|
|
Component {
|
|
id: sendContactRequestComponent
|
|
|
|
StatusButton {
|
|
anchors.horizontalCenter: parent ? parent.horizontalCenter : undefined
|
|
text: qsTr("Send Contact Request")
|
|
onClicked: {
|
|
Global.openContactRequestPopup(root.chatId, null)
|
|
}
|
|
}
|
|
}
|
|
|
|
Component {
|
|
id: acceptOrDeclineContactRequestComponent
|
|
|
|
RowLayout {
|
|
anchors.horizontalCenter: parent ? parent.horizontalCenter : undefined
|
|
|
|
StatusButton {
|
|
text: qsTr("Reject Contact Request")
|
|
type: StatusBaseButton.Type.Danger
|
|
onClicked: {
|
|
root.contactsStore.dismissContactRequest(root.chatId, "")
|
|
}
|
|
}
|
|
|
|
StatusButton {
|
|
text: qsTr("Accept Contact Request")
|
|
onClicked: {
|
|
root.contactsStore.acceptContactRequest(root.chatId, "")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Component {
|
|
id: pendingContactRequestComponent
|
|
|
|
StatusButton {
|
|
anchors.horizontalCenter: parent ? parent.horizontalCenter : undefined
|
|
enabled: false
|
|
text: qsTr("Contact Request Pending...")
|
|
}
|
|
}
|
|
}
|