import QtQuick 2.13 import QtQuick.Controls 2.13 import QtQuick.Layouts 1.13 import QtQuick.Dialogs 1.3 import StatusQ.Core 0.1 import StatusQ.Core.Theme 0.1 import StatusQ.Controls 0.1 import StatusQ.Components 0.1 import utils 1.0 import shared 1.0 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 "../controls" Item { id: root property var chatContentModule property var rootStore property var messageStore property var usersStore property var contactsStore property string channelEmoji property var emojiPopup property var stickersPopup property bool stickersLoaded: false property alias chatLogView: chatLogView property bool isChatBlocked: false property bool isActiveChannel: false property var messageContextMenu 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.chatDetails || null readonly property var loadMoreMessagesIfScrollBelowThreshold: Backpressure.oneInTime(root, 100, function() { if(scrollY < 1000) messageStore.loadMoreMessages() }) function markAllMessagesReadIfMostRecentMessageIsInViewport() { if (!isMostRecentMessageInViewport || !chatLogView.visible) { return } if (chatDetails && chatDetails.active && chatDetails.hasUnreadMessages && !messageStore.messageSearchOngoing && messageStore.firstUnseenMessageLoaded) { chatContentModule.markAllMessagesRead() } } function goToMessage(messageIndex) { chatLogView.currentIndex = -1 chatLogView.currentIndex = messageIndex } onIsMostRecentMessageInViewportChanged: markAllMessagesReadIfMostRecentMessageIsInViewport() } Connections { target: root.messageStore.messageModule function onMessageSuccessfullySent() { chatLogView.positionViewAtBeginning() } function onSendingMessageFailed() { sendingMsgFailedPopup.open() } function onScrollToMessage(messageIndex) { d.goToMessage(messageIndex) } function onScrollToFirstUnreadMessage(messageIndex) { if (d.isMostRecentMessageInViewport) { d.goToMessage(messageIndex) } } } Connections { target: root.messageStore function onMessageSearchOngoingChanged() { d.markAllMessagesReadIfMostRecentMessageIsInViewport() } function onFirstUnseenMessageLoadedChanged() { d.markAllMessagesReadIfMostRecentMessageIsInViewport() } } Connections { target: !!d.chatDetails ? d.chatDetails : null function onActiveChanged() { 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) { if (!d.isMostRecentMessageInViewport) { Qt.callLater(() => messageStore.addNewMessagesMarker()) } } else { 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 readonly property bool show: !messageStore.firstUnseenMessageLoaded || !messageStore.initialMessagesLoaded active: show visible: show anchors.top: loadingMessagesIndicator.bottom anchors.bottom: parent.bottom anchors.left: parent.left anchors.right: parent.right 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 model: messageStore.messagesModel onContentYChanged: { scrollDownButton.visible = contentHeight - (d.scrollY + height) > 400 d.loadMoreMessagesIfScrollBelowThreshold() } onCountChanged: d.markAllMessagesReadIfMostRecentMessageIsInViewport() onVisibleChanged: d.markAllMessagesReadIfMostRecentMessageIsInViewport() onCurrentItemChanged: { if(currentItem && currentIndex > 0) { currentItem.startMessageFoundAnimation() } } ScrollBar.vertical: StatusScrollBar { visible: chatLogView.visibleArea.heightRatio < 1 } Button { id: scrollDownButton anchors.bottom: parent.bottom anchors.right: parent.right anchors.rightMargin: Style.current.padding visible: false height: 32 width: arrowImage.width + 2 * Style.current.halfPadding background: Rectangle { color: Style.current.buttonSecondaryColor border.width: 0 radius: 16 } onClicked: { scrollDownButton.visible = false chatLogView.positionViewAtBeginning() } StatusIcon { id: arrowImage anchors.centerIn: parent width: 24 height: 24 icon: "arrow-down" color: Style.current.pillButtonTextColor } MouseArea { cursorShape: Qt.PointingHandCursor anchors.fill: parent acceptedButtons: Qt.NoButton } } delegate: MessageView { id: msgDelegate width: ListView.view.width objectName: "chatMessageViewDelegate" rootStore: root.rootStore messageStore: root.messageStore usersStore: root.usersStore contactsStore: root.contactsStore channelEmoji: root.channelEmoji emojiPopup: root.emojiPopup stickersPopup: root.stickersPopup chatLogView: ListView.view isActiveChannel: root.isActiveChannel isChatBlocked: root.isChatBlocked messageContextMenu: root.messageContextMenu 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(" ") messageTimestamp: model.timestamp messageOutgoingStatus: model.outgoingStatus resendError: model.resendError messageContentType: model.contentType pinnedMessage: model.pinned messagePinnedBy: model.pinnedBy reactionsModel: model.reactions sticker: model.sticker stickerPack: model.stickerPack editModeOn: model.editMode onEditModeOnChanged: root.editModeChanged(editModeOn) isEdited: model.isEdited linkUrls: model.links 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 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: prevMsgIndex prevMessageTimestamp: prevMsgTimestamp prevMessageSenderId: prevMsgSenderId prevMessageContentType: prevMsgContentType nextMessageIndex: nextMsgIndex nextMessageTimestamp: nextMsgTimestamp onOpenStickerPackPopup: { root.openStickerPackPopup(stickerPackId); } onShowReplyArea: { root.showReplyArea(messageId, author) } onImageClicked: Global.openImagePopup(image, messageContextMenu) stickersLoaded: root.stickersLoaded onVisibleChanged: { if(!visible && model.editMode) messageStore.setEditModeOff(model.id) } } } MessageDialog { id: sendingMsgFailedPopup standardButtons: StandardButton.Ok text: qsTr("Failed to send message.") icon: StandardIcon.Critical } }