status-desktop/ui/app/AppLayouts/Chat/views/ChatMessagesView.qml

401 lines
16 KiB
QML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import QtQuick 2.13
import QtQuick.Controls 2.13
import QtQuick.Window 2.13
import QtQuick.Layouts 1.13
import QtQml.Models 2.13
import QtGraphicalEffects 1.13
import QtQuick.Dialogs 1.3
import "../../../../shared"
import "../../../../shared/panels"
import "../../../../shared/controls"
import "../../../../shared/status"
import "../controls"
//TODO REMOVE
import "../stores"
import utils 1.0
Item {
id: root
anchors.fill: parent
property var store
property alias chatLogView: chatLogView
property alias scrollToMessage: chatLogView.scrollToMessage
property var messageContextMenuInst
property var messageList: MessagesData {}
property real scrollY: chatLogView.visibleArea.yPosition * chatLogView.contentHeight
property int newMessages: 0
property int countOnStartUp: 0
ListView {
id: chatLogView
anchors.fill: parent
spacing: appSettings.useCompactMode ? 0 : 4
boundsBehavior: Flickable.StopAtBounds
clip: true
verticalLayoutDirection: ListView.BottomToTop
// This header and Connections is to create an invisible padding so that the chat identifier is at the top
// The Connections is necessary, because doing the check inside the header created a binding loop (the contentHeight includes the header height
// If the content height is smaller than the full height, we "show" the padding so that the chat identifier is at the top, otherwise we disable the Connections
header: Item {
height: 0
width: chatLogView.width
}
function checkHeaderHeight() {
if (!chatLogView.headerItem) {
return
}
if (chatLogView.contentItem.height - chatLogView.headerItem.height < chatLogView.height) {
chatLogView.headerItem.height = chatLogView.height - (chatLogView.contentItem.height - chatLogView.headerItem.height) - 36
} else {
chatLogView.headerItem.height = 0
}
}
property var scrollToMessage: function (messageId, isSearch = false) {
delayPositioningViewTimer.msgId = messageId;
delayPositioningViewTimer.isSearch = isSearch;
delayPositioningViewTimer.restart();
}
Timer {
id: delayPositioningViewTimer
interval: 1000
property string msgId
property bool isSearch
onTriggered: {
let item
for (let i = 0; i < messages.rowCount(); i++) {
item = messageListDelegate.items.get(i);
if (item.model.messageId === msgId) {
chatLogView.positionViewAtIndex(i, ListView.Beginning);
if (appSettings.useCompactMode && isSearch) {
chatLogView.itemAtIndex(i).startMessageFoundAnimation();
}
}
}
msgId = "";
isSearch = false;
}
}
ScrollBar.vertical: ScrollBar {
visible: chatLogView.visibleArea.heightRatio < 1
}
Connections {
id: contentHeightConnection
enabled: true
target: chatLogView
onContentHeightChanged: {
chatLogView.checkHeaderHeight()
}
onHeightChanged: {
chatLogView.checkHeaderHeight()
}
}
Timer {
id: timer
}
Button {
readonly property int buttonPadding: 5
id: scrollDownButton
visible: false
height: 32
width: nbMessages.width + arrowImage.width + 2 * Style.current.halfPadding + (nbMessages.visible ? scrollDownButton.buttonPadding : 0)
anchors.bottom: parent.bottom
anchors.right: parent.right
anchors.rightMargin: Style.current.padding
background: Rectangle {
color: Style.current.buttonSecondaryColor
border.width: 0
radius: 16
}
onClicked: {
newMessages = 0
scrollDownButton.visible = false
chatLogView.scrollToBottom(true)
}
StyledText {
id: nbMessages
visible: newMessages > 0
width: visible ? implicitWidth : 0
text: newMessages
anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left
color: Style.current.pillButtonTextColor
font.pixelSize: 15
anchors.leftMargin: Style.current.halfPadding
}
SVGImage {
id: arrowImage
width: 24
height: 24
anchors.verticalCenter: parent.verticalCenter
anchors.left: nbMessages.right
source: Style.svg("leave_chat")
anchors.leftMargin: nbMessages.visible ? scrollDownButton.buttonPadding : 0
rotation: -90
ColorOverlay {
anchors.fill: parent
source: parent
color: Style.current.pillButtonTextColor
}
}
MouseArea {
cursorShape: Qt.PointingHandCursor
anchors.fill: parent
onPressed: mouse.accepted = false
}
}
function scrollToBottom(force, caller) {
if (!force && !chatLogView.atYEnd) {
// User has scrolled up, we don't want to scroll back
return false
}
if (caller && caller !== chatLogView.itemAtIndex(chatLogView.count - 1)) {
// If we have a caller, only accept its request if it's the last message
return false
}
// Call this twice and with a timer since the first scroll to bottom might have happened before some stuff loads
// meaning that the scroll will not actually be at the bottom on switch
// Add a small delay because images, even though they say they say they are loaed, they aren't shown yet
Qt.callLater(chatLogView.positionViewAtBeginning)
timer.setTimeout(function() {
Qt.callLater(chatLogView.positionViewAtBeginning)
}, 100);
return true
}
Connections {
target: chatsModel
onAppReady: {
chatLogView.scrollToBottom(true)
}
}
Connections {
target: chatsModel.messageView
onSendingMessageSuccess: {
chatLogView.scrollToBottom(true)
}
onSendingMessageFailed: {
sendingMsgFailedPopup.open();
}
onNewMessagePushed: {
if (!chatLogView.scrollToBottom()) {
newMessages++
}
}
}
Connections {
target: chatsModel.communities
// Note:
// Whole this Connection object (both slots) should be moved to the nim side.
// Left here only cause we don't have a way to deal with translations on the nim side.
onMembershipRequestChanged: function (communityId, communityName, accepted) {
chatColumnLayout.currentNotificationChatId = null
chatColumnLayout.currentNotificationCommunityId = communityId
chatsModel.showOSNotification("Status",
//% "You have been accepted into the %1 community"
accepted ? qsTrId("you-have-been-accepted-into-the---1--community").arg(communityName) :
//% "Your request to join the %1 community was declined"
qsTrId("your-request-to-join-the---1--community-was-declined").arg(communityName),
accepted? Constants.osNotificationType.acceptedIntoCommunity :
Constants.osNotificationType.rejectedByCommunity,
communityId,
"",
"",
appSettings.useOSNotifications)
}
onMembershipRequestPushed: function (communityId, communityName, pubKey) {
chatColumnLayout.currentNotificationChatId = null
chatColumnLayout.currentNotificationCommunityId = communityId
//% "New membership request"
chatsModel.showOSNotification(qsTrId("new-membership-request"),
//% "%1 asks to join %2"
qsTrId("-1-asks-to-join---2-").arg(Utils.getDisplayName(pubKey)).arg(communityName),
Constants.osNotificationType.joinCommunityRequest,
communityId,
"",
"",
appSettings.useOSNotifications)
}
}
property var loadMsgs : Backpressure.oneInTime(chatLogView, 500, function() {
if(!messages.initialMessagesLoaded || messages.loadingHistoryMessages)
return
chatsModel.messageView.loadMoreMessages(chatId);
});
onContentYChanged: {
scrollDownButton.visible = (contentHeight - (scrollY + height) > 400)
if(scrollDownButton.visible && scrollY < 500){
loadMsgs();
}
}
model: messageListDelegate
section.property: "sectionIdentifier"
section.criteria: ViewSection.FullString
Component.onCompleted: scrollToBottom(true)
}
MessageDialog {
id: sendingMsgFailedPopup
standardButtons: StandardButton.Ok
//% "Failed to send message."
text: qsTrId("failed-to-send-message-")
icon: StandardIcon.Critical
}
Timer {
id: modelLoadingDelayTimer
interval: 1000
onTriggered: {
root.countOnStartUp = messageListDelegate.count;
}
}
DelegateModelGeneralized {
id: messageListDelegate
lessThan: [
function(left, right) { return left.clock > right.clock }
]
model: messages
delegate: MessageView {
id: msgDelegate
rootStore: root.store
messageStore: root.store.messageStore
/////////////TODO Remove
fromAuthor: model.fromAuthor
chatId: model.chatId
userName: model.userName
alias: model.alias
localName: model.localName
message: model.message
plainText: model.plainText
identicon: model.identicon
isCurrentUser: model.isCurrentUser
timestamp: model.timestamp
sticker: model.sticker
contentType: model.contentType
replaces: model.replaces
isEdited: model.isEdited
outgoingStatus: model.outgoingStatus
responseTo: model.responseTo
authorCurrentMsg: msgDelegate.ListView.section
// The previous message is actually the nextSection since we reversed the list order
authorPrevMsg: msgDelegate.ListView.nextSection
imageClick: imagePopup.openPopup.bind(imagePopup)
messageId: model.messageId
emojiReactions: model.emojiReactions
linkUrls: model.linkUrls
communityId: model.communityId
hasMention: model.hasMention
stickerPackId: model.stickerPackId
pinnedMessage: model.isPinned
pinnedBy: model.pinnedBy
gapFrom: model.gapFrom
gapTo: model.gapTo
visible: !model.hide
messageContextMenu: root.messageContextMenuInst
prevMessageIndex: {
// This is used in order to have access to the previous message and determine the timestamp
// we can't rely on the index because the sequence of messages is not ordered on the nim side
if (msgDelegate.DelegateModel.itemsIndex < messageListDelegate.items.count - 1) {
return messageListDelegate.items.get(msgDelegate.DelegateModel.itemsIndex + 1).model.index
}
return -1;
}
nextMessageIndex: {
if (msgDelegate.DelegateModel.itemsIndex < 1) {
return -1
}
return messageListDelegate.items.get(msgDelegate.DelegateModel.itemsIndex - 1).model.index
}
scrollToBottom: chatLogView.scrollToBottom
timeout: model.timeout
Component.onCompleted: {
if ((root.countOnStartUp > 0) && (root.countOnStartUp - 1) < index) {
//new message, increment z order
z = index;
}
messageStore.fromAuthor = model.fromAuthor;
messageStore.chatId = model.chatId;
messageStore.userName = model.userName;
messageStore.alias = model.alias;
messageStore.localName = model.localName;
messageStore.message = model.message;
messageStore.plainText = model.plainText;
messageStore.identicon = model.identicon;
messageStore.isCurrentUser = model.isCurrentUser;
messageStore.timestamp = model.timestamp;
messageStore.sticker = model.sticker;
messageStore.contentType = model.contentType;
messageStore.replaces = model.replaces;
messageStore.isEdited = model.isEdited;
messageStore.outgoingStatus = model.outgoingStatus;
messageStore.responseTo = model.responseTo;
messageStore.authorCurrentMsg = msgDelegate.ListView.section;
// The previous message is actually the nextSection since we reversed the list order
messageStore.authorPrevMsg = msgDelegate.ListView.nextSection;
messageStore.imageClick = imagePopup.openPopup.bind(imagePopup);
messageStore.messageId = model.messageId;
messageStore.emojiReactions = model.emojiReactions;
messageStore.linkUrls = model.linkUrls;
messageStore.communityId = model.communityId;
messageStore.hasMention = model.hasMention;
messageStore.stickerPackId = model.stickerPackId;
messageStore.pinnedMessage = model.isPinned;
messageStore.pinnedBy = model.pinnedBy;
messageStore.gapFrom = model.gapFrom;
messageStore.gapTo = model.gapTo;
messageStore.messageContextMenu = root.messageContextMenuInst;
messageStore.prevMessageIndex =
// This is used in order to have access to the previous message and determine the timestamp
// we can't rely on the index because the sequence of messages is not ordered on the nim side
(msgDelegate.DelegateModel.itemsIndex < messageListDelegate.items.count - 1) ?
messageListDelegate.items.get(msgDelegate.DelegateModel.itemsIndex + 1).model.index
: -1;
messageStore.nextMessageIndex = (msgDelegate.DelegateModel.itemsIndex < 1) ?
-1 : messageListDelegate.items.get(msgDelegate.DelegateModel.itemsIndex - 1).model.index;
messageStore.scrollToBottom = chatLogView.scrollToBottom;
messageStore.timeout = model.timeout;
}
}
Component.onCompleted: {
modelLoadingDelayTimer.start();
}
}
}