401 lines
16 KiB
QML
401 lines
16 KiB
QML
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();
|
||
}
|
||
}
|
||
}
|