450 lines
15 KiB
QML
450 lines
15 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 emojiPopup
|
|
property var stickersPopup
|
|
|
|
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 (!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: 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 && !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
|
|
|
|
isChatBlocked: root.isChatBlocked
|
|
|
|
sendViaPersonalChatEnabled: root.sendViaPersonalChatEnabled
|
|
|
|
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
|
|
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...")
|
|
}
|
|
}
|
|
}
|