From e0983fcc8c1a339cb209f9f7b08a17819a1a0b06 Mon Sep 17 00:00:00 2001 From: Tobias Fella Date: Sat, 6 May 2023 15:09:00 +0000 Subject: [PATCH] Refactor RoomPage into various component --- src/models/messageeventmodel.cpp | 8 +- src/models/messageeventmodel.h | 2 + src/qml/Component/ChatBox/ChatBox.qml | 3 + src/qml/Component/InvitationView.qml | 35 + .../Component/Timeline/ReactionDelegate.qml | 2 + .../Component/Timeline/ReadMarkerDelegate.qml | 6 +- src/qml/Component/Timeline/StateDelegate.qml | 4 +- .../Component/Timeline/TimelineContainer.qml | 4 +- src/qml/Component/TimelineView.qml | 565 +++++++++++++++ src/qml/Page/RoomPage.qml | 672 +++--------------- src/qml/main.qml | 9 - src/res.qrc | 2 + src/roommanager.cpp | 4 + 13 files changed, 742 insertions(+), 574 deletions(-) create mode 100644 src/qml/Component/InvitationView.qml create mode 100644 src/qml/Component/TimelineView.qml diff --git a/src/models/messageeventmodel.cpp b/src/models/messageeventmodel.cpp index 64ef1ad94..007c07edc 100644 --- a/src/models/messageeventmodel.cpp +++ b/src/models/messageeventmodel.cpp @@ -150,16 +150,20 @@ void MessageEventModel::setRoom(NeoChatRoom *room) } } } + m_initialized = true; beginInsertRows({}, timelineBaseIndex(), timelineBaseIndex() + int(events.size()) - 1); }); connect(m_currentRoom, &Room::aboutToAddHistoricalMessages, this, [this](RoomEventsRange events) { if (rowCount() > 0) { rowBelowInserted = rowCount() - 1; // See #312 } + m_initialized = true; beginInsertRows({}, rowCount(), rowCount() + int(events.size()) - 1); }); connect(m_currentRoom, &Room::addedMessages, this, [this](int lowest, int biggest) { - endInsertRows(); + if (m_initialized) { + endInsertRows(); + } if (!m_lastReadEventIndex.isValid()) { // no read marker, so see if we need to create one. #ifdef QUOTIENT_07 @@ -177,6 +181,7 @@ void MessageEventModel::setRoom(NeoChatRoom *room) } }); connect(m_currentRoom, &Room::pendingEventAboutToAdd, this, [this] { + m_initialized = true; beginInsertRows({}, 0, 0); }); connect(m_currentRoom, &Room::pendingEventAdded, this, &MessageEventModel::endInsertRows); @@ -279,6 +284,7 @@ void MessageEventModel::moveReadMarker(const QString &toEventId) // need to be displayed. if (newRow > timelineBaseIndex()) { // The user didn't read all the messages yet. + m_initialized = true; beginInsertRows({}, newRow, newRow); m_lastReadEventIndex = QPersistentModelIndex(index(newRow, 0)); endInsertRows(); diff --git a/src/models/messageeventmodel.h b/src/models/messageeventmodel.h index 10f8e86a9..36bd60b7c 100644 --- a/src/models/messageeventmodel.h +++ b/src/models/messageeventmodel.h @@ -197,6 +197,8 @@ private: QVariantMap getMediaInfoFromFileInfo(const Quotient::EventContent::FileInfo *fileInfo, const QString &eventId, bool isThumbnail = false) const; std::vector> m_extraEvents; + // Hack to ensure that we don't call endInsertRows when we haven't called beginInsertRows + bool m_initialized = false; Q_SIGNALS: void roomChanged(); diff --git a/src/qml/Component/ChatBox/ChatBox.qml b/src/qml/Component/ChatBox/ChatBox.qml index 0755ee073..41b194315 100644 --- a/src/qml/Component/ChatBox/ChatBox.qml +++ b/src/qml/Component/ChatBox/ChatBox.qml @@ -21,6 +21,9 @@ ColumnLayout { spacing: 0 + Kirigami.Theme.colorSet: Kirigami.Theme.View + Kirigami.Theme.inherit: false + Kirigami.InlineMessage { Layout.fillWidth: true Layout.leftMargin: 1 // So we can see the border diff --git a/src/qml/Component/InvitationView.qml b/src/qml/Component/InvitationView.qml new file mode 100644 index 000000000..4a87373ab --- /dev/null +++ b/src/qml/Component/InvitationView.qml @@ -0,0 +1,35 @@ +// SPDX-FileCopyrightText: 2023 Tobias Fella +// SPDX-License-Identifier: GPL-2.0-or-later + +import QtQuick 2.15 +import QtQuick.Controls 2.15 as QQC2 +import QtQuick.Layouts 1.15 + +import org.kde.kirigami 2.20 as Kirigami + +import org.kde.neochat 1.0 + +Kirigami.PlaceholderMessage { + id: root + + required property var currentRoom + + text: i18n("Accept this invitation?") + RowLayout { + QQC2.Button { + Layout.alignment : Qt.AlignHCenter + text: i18n("Reject") + + onClicked: RoomManager.leaveRoom(root.currentRoom); + } + + QQC2.Button { + Layout.alignment : Qt.AlignHCenter + text: i18n("Accept") + + onClicked: { + root.currentRoom.acceptInvitation(); + } + } + } +} \ No newline at end of file diff --git a/src/qml/Component/Timeline/ReactionDelegate.qml b/src/qml/Component/Timeline/ReactionDelegate.qml index ee6206a86..f0875913f 100644 --- a/src/qml/Component/Timeline/ReactionDelegate.qml +++ b/src/qml/Component/Timeline/ReactionDelegate.qml @@ -26,6 +26,8 @@ Flow { background: Kirigami.ShadowedRectangle { color: checked ? Kirigami.Theme.positiveBackgroundColor : Kirigami.Theme.backgroundColor + Kirigami.Theme.inherit: false + Kirigami.Theme.colorSet: Kirigami.Theme.View radius: height / 2 shadow.size: Kirigami.Units.smallSpacing shadow.color: !model.isHighlighted ? Qt.rgba(0.0, 0.0, 0.0, 0.10) : Qt.rgba(Kirigami.Theme.textColor.r, Kirigami.Theme.textColor.g, Kirigami.Theme.textColor.b, 0.10) diff --git a/src/qml/Component/Timeline/ReadMarkerDelegate.qml b/src/qml/Component/Timeline/ReadMarkerDelegate.qml index acb293a69..dbe84c825 100644 --- a/src/qml/Component/Timeline/ReadMarkerDelegate.qml +++ b/src/qml/Component/Timeline/ReadMarkerDelegate.qml @@ -17,8 +17,8 @@ QQC2.ItemDelegate { topPadding: Kirigami.Units.largeSpacing * 2 // extraWidth defines how the delegate can grow after the listView gets very wide - readonly property int extraWidth: messageListView.width >= Kirigami.Units.gridUnit * 46 ? Math.min((messageListView.width - Kirigami.Units.gridUnit * 46), Kirigami.Units.gridUnit * 20) : 0 - readonly property int delegateMaxWidth: Config.compactLayout ? messageListView.width - Kirigami.Units.largeSpacing * 2 : Math.min(messageListView.width - Kirigami.Units.largeSpacing * 2, Kirigami.Units.gridUnit * 40 + extraWidth) + readonly property int extraWidth: parent ? (parent.width >= Kirigami.Units.gridUnit * 46 ? Math.min((parent.width - Kirigami.Units.gridUnit * 46), Kirigami.Units.gridUnit * 20) : 0) : 0 + readonly property int delegateMaxWidth: parent ? (Config.compactLayout ? parent.width - Kirigami.Units.largeSpacing * 2 : Math.min(parent.width - Kirigami.Units.largeSpacing * 2, Kirigami.Units.gridUnit * 40 + extraWidth)) : 0 property bool isTemporaryHighlighted: false @@ -78,6 +78,8 @@ QQC2.ItemDelegate { return Kirigami.Theme.backgroundColor } } + Kirigami.Theme.inherit: false + Kirigami.Theme.colorSet: Kirigami.Theme.View opacity: readMarkerDelegate.isTemporaryHighlighted ? 1 : 0.6 radius: Kirigami.Units.smallSpacing shadow.size: Kirigami.Units.smallSpacing diff --git a/src/qml/Component/Timeline/StateDelegate.qml b/src/qml/Component/Timeline/StateDelegate.qml index a2c106d52..4ed626ff8 100644 --- a/src/qml/Component/Timeline/StateDelegate.qml +++ b/src/qml/Component/Timeline/StateDelegate.qml @@ -15,8 +15,8 @@ QQC2.Control { readonly property bool sectionVisible: model.showSection // extraWidth defines how the delegate can grow after the listView gets very wide - readonly property int extraWidth: messageListView.width >= Kirigami.Units.gridUnit * 46 ? Math.min((messageListView.width - Kirigami.Units.gridUnit * 46), Kirigami.Units.gridUnit * 20) : 0 - readonly property int delegateMaxWidth: Config.compactLayout ? messageListView.width: Math.min(messageListView.width, Kirigami.Units.gridUnit * 40 + extraWidth) + readonly property int extraWidth: parent ? (parent.width >= Kirigami.Units.gridUnit * 46 ? Math.min((parent.width - Kirigami.Units.gridUnit * 46), Kirigami.Units.gridUnit * 20) : 0) : 0 + readonly property int delegateMaxWidth: parent ? (Config.compactLayout ? parent.width: Math.min(parent.width, Kirigami.Units.gridUnit * 40 + extraWidth)) : 0 width: delegateMaxWidth diff --git a/src/qml/Component/Timeline/TimelineContainer.qml b/src/qml/Component/Timeline/TimelineContainer.qml index 4a8ed471d..43b9e7636 100644 --- a/src/qml/Component/Timeline/TimelineContainer.qml +++ b/src/qml/Component/Timeline/TimelineContainer.qml @@ -37,9 +37,9 @@ ColumnLayout { // The bubble and delegate widths are allowed to grow once the ListView gets beyond a certain size // extraWidth defines this as the excess after a certain ListView width, capped to a max value - readonly property int extraWidth: messageListView.width >= Kirigami.Units.gridUnit * 46 ? Math.min((messageListView.width - Kirigami.Units.gridUnit * 46), Kirigami.Units.gridUnit * 20) : 0 + readonly property int extraWidth: parent ? (parent.width >= Kirigami.Units.gridUnit * 46 ? Math.min((parent.width - Kirigami.Units.gridUnit * 46), Kirigami.Units.gridUnit * 20) : 0) : 0 readonly property int bubbleMaxWidth: Kirigami.Units.gridUnit * 20 + extraWidth * 0.5 - readonly property int delegateWidth: Config.compactLayout ? messageListView.width - Kirigami.Units.smallSpacing - (ListView.view.width >= Kirigami.Units.gridUnit * 20 ? Kirigami.Units.gridUnit * 2 + Kirigami.Units.largeSpacing : 0) : Math.min(messageListView.width, Kirigami.Units.gridUnit * 40 + extraWidth) + readonly property int delegateWidth: parent ? (Config.compactLayout ? parent.width - Kirigami.Units.smallSpacing - (ListView.view.width >= Kirigami.Units.gridUnit * 20 ? Kirigami.Units.gridUnit * 2 + Kirigami.Units.largeSpacing : 0) : Math.min(parent.width, Kirigami.Units.gridUnit * 40 + extraWidth)) : 0 readonly property int contentMaxWidth: Config.compactLayout ? width - (Config.showAvatarInTimeline ? Kirigami.Units.gridUnit * 2 : 0) - Kirigami.Units.largeSpacing * 4 : Math.min(width - Kirigami.Units.gridUnit * 2 - Kirigami.Units.largeSpacing * 6, bubbleMaxWidth) width: delegateWidth diff --git a/src/qml/Component/TimelineView.qml b/src/qml/Component/TimelineView.qml new file mode 100644 index 000000000..d995c74b6 --- /dev/null +++ b/src/qml/Component/TimelineView.qml @@ -0,0 +1,565 @@ +// SPDX-FileCopyrightText: 2020 Carl Schwan +// SPDX-License-Identifier: GPL-2.0-or-later + +import QtQuick 2.15 +import QtQuick.Controls 2.15 as QQC2 +import QtQuick.Layouts 1.15 +import Qt.labs.platform 1.1 as Platform +import Qt.labs.qmlmodels 1.0 +import QtQuick.Window 2.15 + +import org.kde.kirigami 2.19 as Kirigami +import org.kde.kitemmodels 1.0 + +import org.kde.neochat 1.0 + +QQC2.ScrollView { + id: root + required property var currentRoom + readonly property bool isLoaded: root.width * root.height > 10 + readonly property bool atYEnd: messageListView.atYEnd + + ListView { + id: messageListView + readonly property int largestVisibleIndex: count > 0 ? indexAt(contentX + (width / 2), contentY + height - 1) : -1 + readonly property var sectionBannerItem: contentHeight >= height ? itemAtIndex(sectionBannerIndex()) : undefined + + // Spacing needs to be zero or the top sectionLabel overlay will be disrupted. + // This is because itemAt returns null in the spaces. + // All spacing should be handled by the delegates themselves + spacing: 0 + // Ensures that the top item is not covered by sectionBanner if the page is scrolled all the way up + // topMargin: sectionBanner.height + verticalLayoutDirection: ListView.BottomToTop + highlightMoveDuration: 500 + clip: true + + model: !isLoaded ? undefined : collapseStateProxyModel + + CollapseStateProxyModel { + id: collapseStateProxyModel + sourceModel: MessageFilterModel { + id: sortedMessageEventModel + sourceModel: MessageEventModel { + id: messageEventModel + room: root.currentRoom + } + } + } + + Timer { + interval: 1000 + running: messageListView.atYBeginning + triggeredOnStart: true + onTriggered: { + if (messageListView.atYBeginning && messageEventModel.canFetchMore(messageEventModel.index(0, 0))) { + messageEventModel.fetchMore(messageEventModel.index(0, 0)); + } + } + repeat: true + } + + // HACK: The view should do this automatically but doesn't. + onAtYBeginningChanged: if (atYBeginning && messageEventModel.canFetchMore(messageEventModel.index(0, 0))) { + messageEventModel.fetchMore(messageEventModel.index(0, 0)); + } + + onAtYEndChanged: if (atYEnd && hasScrolledUpBefore) { + if (QQC2.ApplicationWindow.window && (QQC2.ApplicationWindow.window.visibility !== QQC2.ApplicationWindow.Hidden)) { + root.currentRoom.markAllMessagesAsRead(); + } + hasScrolledUpBefore = false; + } else if (!atYEnd) { + hasScrolledUpBefore = true; + } + + // Not rendered because the sections are part of the TimelineContainer.qml, this is only so that items have the section property available for use by sectionBanner. + // This is due to the fact that the ListView verticalLayout is BottomToTop. + // This also flips the sections which would appear at the bottom but for a timeline they still need to be at the top (bottom from the qml perspective). + // There is currently no option to put section headings at the bottom in qml. + section.property: "section" + + function sectionBannerIndex() { + let center = messageListView.x + messageListView.width / 2; + let yStart = messageListView.y + messageListView.contentY; + let index = -1; + let i = 0; + while (index === -1 && i < 100) { + index = messageListView.indexAt(center, yStart + i); + i++; + } + return index; + } + + footer: SectionDelegate { + id: sectionBanner + + anchors.left: parent.left + anchors.leftMargin: messageListView.sectionBannerItem ? messageListView.sectionBannerItem.x : 0 + anchors.right: parent.right + + maxWidth: Config.compactLayout ? messageListView.width : (messageListView.sectionBannerItem ? messageListView.sectionBannerItem.width - Kirigami.Units.largeSpacing * 2 : 0) + z: 3 + visible: !!messageListView.sectionBannerItem && messageListView.sectionBannerItem.ListView.section !== "" && !Config.blur + labelText: messageListView.sectionBannerItem ? messageListView.sectionBannerItem.ListView.section : "" + } + footerPositioning: ListView.OverlayHeader + + QQC2.Popup { + anchors.centerIn: parent + + id: attachDialog + + padding: 16 + + contentItem: RowLayout { + QQC2.ToolButton { + Layout.preferredWidth: 160 + Layout.fillHeight: true + + icon.name: 'mail-attachment' + + text: i18n("Choose local file") + + onClicked: { + attachDialog.close() + + var fileDialog = openFileDialog.createObject(QQC2.ApplicationWindow.overlay) + + fileDialog.chosen.connect(function (path) { + if (!path) { + return; + } + root.currentRoom.chatBoxAttachmentPath = path; + }) + + fileDialog.open() + } + } + + Kirigami.Separator { + } + + QQC2.ToolButton { + Layout.preferredWidth: 160 + Layout.fillHeight: true + + padding: 16 + + icon.name: 'insert-image' + text: i18n("Clipboard image") + onClicked: { + const localPath = Platform.StandardPaths.writableLocation(Platform.StandardPaths.CacheLocation) + "/screenshots/" + (new Date()).getTime() + ".png" + if (!Clipboard.saveImage(localPath)) { + return; + } + root.currentRoom.chatBoxAttachmentPath = localPath; + attachDialog.close(); + } + } + } + } + + Component { + id: openFileDialog + + OpenFileDialog { + parentWindow: Window.window + } + } + + delegate: EventDelegate { + } + + QQC2.RoundButton { + id: goReadMarkerFab + + anchors.right: parent.right + anchors.top: parent.top + anchors.topMargin: Kirigami.Units.largeSpacing + anchors.rightMargin: Kirigami.Units.largeSpacing + implicitWidth: Kirigami.Units.gridUnit * 2 + implicitHeight: Kirigami.Units.gridUnit * 2 + + z: 2 + visible: root.currentRoom && root.currentRoom.hasUnreadMessages && root.currentRoom.readMarkerLoaded + action: Kirigami.Action { + onTriggered: { + if (!Kirigami.Settings.isMobile) { + chatBox.chatBar.forceActiveFocus(); + } + messageListView.goToEvent(root.currentRoom.readMarkerEventId) + } + icon.name: "go-up" + shortcut: "Shift+PgUp" + } + + QQC2.ToolTip { + text: i18n("Jump to first unread message") + } + } + QQC2.RoundButton { + id: goMarkAsReadFab + anchors.right: parent.right + anchors.bottom: parent.bottom + anchors.bottomMargin: Kirigami.Units.largeSpacing + anchors.rightMargin: Kirigami.Units.largeSpacing + implicitWidth: Kirigami.Units.gridUnit * 2 + implicitHeight: Kirigami.Units.gridUnit * 2 + + z: 2 + visible: !messageListView.atYEnd + action: Kirigami.Action { + onTriggered: { + messageListView.goToLastMessage(); + root.currentRoom.markAllMessagesAsRead(); + } + icon.name: "go-down" + } + + QQC2.ToolTip { + text: i18n("Jump to latest message") + } + } + + Component.onCompleted: { + positionViewAtBeginning(); + } + + DropArea { + id: dropAreaFile + anchors.fill: parent + onDropped: root.currentRoom.chatBoxAttachmentPath = drop.urls[0] + ; + enabled: !Controller.isFlatpak + } + + QQC2.Pane { + visible: dropAreaFile.containsDrag + anchors { + fill: parent + margins: Kirigami.Units.gridUnit + } + + Kirigami.PlaceholderMessage { + anchors.centerIn: parent + width: parent.width - (Kirigami.Units.largeSpacing * 4) + text: i18n("Drag items here to share them") + } + } + + Component { + id: messageDelegateContextMenu + + MessageDelegateContextMenu {} + } + + Component { + id: fileDelegateContextMenu + + FileDelegateContextMenu {} + } + + Component { + id: userDetailDialog + + UserDetailDialog {} + } + + TypingPane { + id: typingPane + visible: root.currentRoom && root.currentRoom.usersTyping.length > 0 + labelText: visible ? i18ncp( + "Message displayed when some users are typing", "%2 is typing", "%2 are typing", + root.currentRoom.usersTyping.length, + root.currentRoom.usersTyping.map(user => user.displayName).join(", ") + ) : + "" + anchors.left: parent.left + anchors.bottom: parent.bottom + height: visible ? implicitHeight : 0 + Behavior on height { + NumberAnimation { + property: "height" + duration: Kirigami.Units.shortDuration + easing.type: Easing.OutCubic + } + } + z: 2 + } + + function goToEvent(eventID) { + const index = eventToIndex(eventID) + messageListView.positionViewAtIndex(index, ListView.Center) + itemAtIndex(index).isTemporaryHighlighted = true + } + + Item { + id: hoverActions + property var event: null + property bool userMsg: event && event.author.id === Controller.activeConnection.localUserId + property bool showEdit: event && (userMsg && (event.eventType === MessageEventModel.Emote || event.eventType === MessageEventModel.Message)) + property var delegate: null + property var bubble: null + property var hovered: bubble && bubble.hovered + property var visibleDelayed: (hovered || hoverHandler.hovered) && !Kirigami.Settings.isMobile + property var updateFunction + onVisibleDelayedChanged: if (visibleDelayed) { + visible = true; + } else { + // HACK: delay disapearing by 200ms, otherwise this can create some glitches + // See https://invent.kde.org/network/neochat/-/issues/333 + hoverActionsTimer.restart(); + } + Timer { + id: hoverActionsTimer + interval: 200 + onTriggered: hoverActions.visible = hoverActions.visibleDelayed + ; + } + + property int childOffset: userMsg && Config.showLocalMessagesOnRight && !Config.compactLayout ? (bubble ? bubble.width : 0) - childWidth : Math.max((bubble ? bubble.width : 0) - childWidth, 0) + x: delegate && bubble ? (delegate.x + bubble.x + Kirigami.Units.largeSpacing + childOffset - (Config.compactLayout ? Kirigami.Units.gridUnit * 3 : 0) - (userMsg && !Config.compactLayout ? Kirigami.Units.gridUnit : 0)) : 0 + y: bubble ? bubble.mapToItem(parent, 0, 0).y - hoverActions.childHeight + Kirigami.Units.smallSpacing : 0 + ; + + visible: false + + property alias childWidth: hoverActionsRow.width + property alias childHeight: hoverActionsRow.height + + RowLayout { + id: hoverActionsRow + z: 4 + spacing: 0 + HoverHandler { + id: hoverHandler + margin: Kirigami.Units.smallSpacing + } + Kirigami.Icon { + source: "security-high" + width: height + height: parent.height + visible: hoverActions.event ? hoverActions.event.verified : false + HoverHandler { + id: hover + } + QQC2.ToolTip.text: i18n("This message was sent from a verified device") + QQC2.ToolTip.visible: hover.hovered + QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay + } + + QQC2.Button { + QQC2.ToolTip.text: i18n("React") + QQC2.ToolTip.visible: hovered + QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay + icon.name: "preferences-desktop-emoticons" + + onClicked: emojiDialog.open() + ; + EmojiDialog { + id: emojiDialog + showQuickReaction: true + onChosen: { + root.currentRoom.toggleReaction(hoverActions.event.eventId, emoji); + if (!Kirigami.Settings.isMobile) { + chatBox.chatBar.forceActiveFocus(); + } + } + } + } + QQC2.Button { + QQC2.ToolTip.text: i18n("Edit") + QQC2.ToolTip.visible: hovered + QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay + visible: hoverActions.showEdit + icon.name: "document-edit" + onClicked: { + root.currentRoom.chatBoxEditId = hoverActions.event.eventId; + root.currentRoom.chatBoxReplyId = ""; + } + } + QQC2.Button { + QQC2.ToolTip.text: i18n("Reply") + QQC2.ToolTip.visible: hovered + QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay + icon.name: "mail-replied-symbolic" + onClicked: { + root.currentRoom.chatBoxReplyId = hoverActions.event.eventId; + root.currentRoom.chatBoxEditId = ""; + chatBox.chatBar.forceActiveFocus(); + } + } + } + } + + // hover actions on a delegate, activated in TimelineContainer.qml + Connections { + target: root.parent.flickable ?? null + enabled : hoverActions.visible + + function onContentYChanged() { + hoverActions.updateFunction(); + } + } + + Connections { + target: messageEventModel + + function onRowsInserted() { + markReadIfVisibleTimer.restart() + } + } + + Timer { + id: markReadIfVisibleTimer + interval: 1000 + onTriggered: { + if (loading || !root.currentRoom.readMarkerLoaded || !applicationWindow().active) { + restart() + } else { + messageListView.markReadIfVisible() + } + } + } + + Rectangle { + FancyEffectsContainer { + id: fancyEffectsContainer + anchors.fill: parent + z: 100 + + enabled: Config.showFancyEffects + + function processFancyEffectsReason(fancyEffect) { + if (fancyEffect === "snowflake") { + fancyEffectsContainer.showSnowEffect() + } + if (fancyEffect === "fireworks") { + fancyEffectsContainer.showFireworksEffect() + } + if (fancyEffect === "confetti") { + fancyEffectsContainer.showConfettiEffect() + } + } + + Connections { + //enabled: Config.showFancyEffects + target: messageEventModel + + function onFancyEffectsReasonFound(fancyEffect) { + fancyEffectsContainer.processFancyEffectsReason(fancyEffect) + } + } + + Connections { + enabled: Config.showFancyEffects + target: actionsHandler + + function onShowEffect(fancyEffect) { + fancyEffectsContainer.processFancyEffectsReason(fancyEffect) + } + } + } + } + + + function showUserDetail(user) { + userDetailDialog.createObject(QQC2.ApplicationWindow.overlay, { + room: root.currentRoom, + user: user, + }).open(); + } + + function goToLastMessage() { + root.currentRoom.markAllMessagesAsRead() + // scroll to the very end, i.e to messageListView.YEnd + messageListView.positionViewAtIndex(0, ListView.End) + } + + function eventToIndex(eventID) { + const index = messageEventModel.eventIDToIndex(eventID) + if (index === -1) + return -1 + return sortedMessageEventModel.mapFromSource(messageEventModel.index(index, 0)).row + } + + function firstVisibleIndex() { + let center = messageListView.x + messageListView.width / 2; + let index = -1 + let i = 0 + while (index === -1 && i < 100) { + index = messageListView.indexAt(center, messageListView.y + messageListView.contentY + i); + i++; + } + return index + } + + function lastVisibleIndex() { + let center = messageListView.x + messageListView.width / 2; + let index = -1 + let i = 0 + while (index === -1 && i < 100) { + index = messageListView.indexAt(center, messageListView.y + messageListView.contentY + messageListView.height - i); + i++ + } + return index; + } + + // Mark all messages as read if all unread messages are visible to the user + function markReadIfVisible() { + let readMarkerRow = eventToIndex(root.currentRoom.readMarkerEventId) + if (readMarkerRow >= 0 && readMarkerRow < firstVisibleIndex() && messageListView.atYEnd) { + root.currentRoom.markAllMessagesAsRead() + } + } + + /// Open message context dialog for file and videos + function openFileContext(event, file) { + const contextMenu = fileDelegateContextMenu.createObject(root, { + author: event.author, + message: event.message, + eventId: event.eventId, + source: event.source, + file: file, + mimeType: event.mimeType, + progressInfo: event.progressInfo, + plainMessage: event.message, + }); + contextMenu.open(); + } + + /// Open context menu for normal message + function openMessageContext(event, selectedText, plainMessage) { + const contextMenu = messageDelegateContextMenu.createObject(root, { + selectedText: selectedText, + author: event.author, + message: event.display, + eventId: event.eventId, + formattedBody: event.formattedBody, + source: event.source, + eventType: event.eventType, + plainMessage: plainMessage, + }); + contextMenu.open(); + } + } + + function goToLastMessage() { + messageListView.goToLastMessage() + } + + function pageUp() { + const newContentY = messageListView.contentY - messageListView.height / 2; + const minContentY = messageListView.originY + messageListView.topMargin; + messageListView.contentY = Math.max(newContentY, minContentY); + messageListView.returnToBounds(); + } + + function pageDown() { + const newContentY = messageListView.contentY + messageListView.height / 2; + const maxContentY = messageListView.originY + messageListView.bottomMargin + messageListView.contentHeight - messageListView.height; + messageListView.contentY = Math.min(newContentY, maxContentY); + messageListView.returnToBounds(); + } +} diff --git a/src/qml/Page/RoomPage.qml b/src/qml/Page/RoomPage.qml index d5b74aed6..49919afb6 100644 --- a/src/qml/Page/RoomPage.qml +++ b/src/qml/Page/RoomPage.qml @@ -5,8 +5,6 @@ import QtQuick 2.15 import QtQuick.Controls 2.15 as QQC2 import QtQuick.Layouts 1.15 -import Qt.labs.platform 1.1 as Platform -import Qt.labs.qmlmodels 1.0 import QtQuick.Window 2.15 import org.kde.kirigami 2.19 as Kirigami @@ -14,100 +12,36 @@ import org.kde.kitemmodels 1.0 import org.kde.neochat 1.0 -Kirigami.ScrollablePage { - id: page +Kirigami.Page { + id: root - /// It's not readonly because of the seperate window view. + /// Not readonly because of the separate window view. property var currentRoom: RoomManager.currentRoom - property bool loading: page.currentRoom === null || (messageListView.count === 0 && !page.currentRoom.allHistoryLoaded && !page.currentRoom.isInvite) + property bool loading: !root.currentRoom || (root.currentRoom.timelineSize === 0 && !root.currentRoom.allHistoryLoaded) /// Used to determine if scrolling to the bottom should mark the message as unread property bool hasScrolledUpBefore: false; - /// Disable cancel shortcut. Used by the seperate window since it provide its own - /// cancel implementation. + /// Disable cancel shortcut. Used by the separate window since it provides its own cancel implementation. property bool disableCancelShortcut: false title: currentRoom.displayName + focus: true + padding: 0 KeyNavigation.left: pageStack.get(0) - Connections { - target: RoomManager - function onCurrentRoomChanged() { - if(!RoomManager.currentRoom) { - if(pageStack.lastItem == page) { - pageStack.pop() - } - } else if (page.currentRoom.isInvite) { - page.currentRoom.clearInvitationNotification(); - } - } - } - onCurrentRoomChanged: { + if (!timelineViewLoader.item) { + return + } applicationWindow().hoverLinkIndicator.text = ""; - messageListView.positionViewAtBeginning(); + timelineViewLoader.item.positionViewAtBeginning(); hasScrolledUpBefore = false; if (!Kirigami.Settings.isMobile) { chatBox.chatBar.forceActiveFocus(); } } - Connections { - target: messageEventModel - function onRowsInserted() { - markReadIfVisibleTimer.restart() - } - } - - Timer { - id: markReadIfVisibleTimer - interval: 1000 - onTriggered: { - if (loading || !currentRoom.readMarkerLoaded || !applicationWindow().active) { - restart() - } else { - markReadIfVisible() - } - } - } - - ActionsHandler { - id: actionsHandler - room: page.currentRoom - } - - Shortcut { - sequence: StandardKey.Cancel - onActivated: { - if (!messageListView.atYEnd || currentRoom.hasUnreadMessages) { - goToLastMessage(); - currentRoom.markAllMessagesAsRead(); - } else { - applicationWindow().pageStack.get(0).forceActiveFocus(); - } - } - enabled: !page.disableCancelShortcut - } - - Connections { - target: Controller.activeConnection - function onJoinedRoom(room, invited) { - if(page.currentRoom.id === invited.id) { - RoomManager.enterRoom(room); - } - } - } - - Connections { - target: currentRoom - function onShowMessage(messageType, message) { - page.header.contentItem.text = message; - page.header.contentItem.type = messageType === ActionsHandler.Error ? Kirigami.MessageType.Error : messageType === ActionsHandler.Positive ? Kirigami.MessageType.Positive : Kirigami.MessageType.Information; - page.header.visible = true; - } - } - header: QQC2.Control { height: visible ? implicitHeight : 0 visible: false @@ -118,505 +52,122 @@ Kirigami.ScrollablePage { } } - Kirigami.LoadingPlaceholder { - id: loadingIndicator + Loader { + id: timelineViewLoader + anchors.fill: parent + active: currentRoom && !currentRoom.isInvite && !root.loading + sourceComponent: TimelineView { + id: timelineView + currentRoom: root.currentRoom + } + } + + Loader { + id: invitationLoader + active: currentRoom && currentRoom.isInvite anchors.centerIn: parent - visible: loading - } - - focus: true - - Keys.onPressed: { - if (event.key === Qt.Key_PageUp) { - event.accepted = true; - const newContentY = messageListView.contentY - messageListView.height / 2; - const minContentY = messageListView.originY + messageListView.topMargin; - messageListView.contentY = Math.max(newContentY, minContentY); - messageListView.returnToBounds(); - } else if (event.key === Qt.Key_PageDown) { - event.accepted = true; - const newContentY = messageListView.contentY + messageListView.height / 2; - const maxContentY = messageListView.originY + messageListView.bottomMargin + messageListView.contentHeight - messageListView.height; - messageListView.contentY = Math.min(newContentY, maxContentY); - messageListView.returnToBounds(); - } else if (!(event.modifiers & Qt.ControlModifier) && event.key < Qt.Key_Escape) { - event.accepted = true; - chatBox.chatBar.insertText(event.text); - chatBox.chatBar.forceActiveFocus(); - return; - } - } - - // hover actions on a delegate, activated in TimelineContainer.qml - Connections { - target: page.flickable - enabled: hoverActions.visible - function onContentYChanged() { - hoverActions.updateFunction(); - } - } - - CollapseStateProxyModel { - id: collapseStateProxyModel - sourceModel: sortedMessageEventModel - } - - ListView { - id: messageListView - - readonly property int largestVisibleIndex: count > 0 ? indexAt(contentX + (width / 2), contentY + height - 1) : -1 - readonly property bool isLoaded: page.width * page.height > 10 - - // Spacing needs to be zero or the top sectionLabel overlay will be disrupted. - // This is because itemAt returns null in the spaces. - // All spacing should be handled by the delegates themselves - spacing: 0 - // Ensures that the top item is not covered by sectionBanner if the page is scrolled all the way up - // topMargin: sectionBanner.height - verticalLayoutDirection: ListView.BottomToTop - highlightMoveDuration: 500 - - // HACK: Needs to be here because the flickable in ScrollablePage gets all mouse events. - Kirigami.PlaceholderMessage { - id: invitation - - visible: currentRoom && currentRoom.isInvite + sourceComponent: InvitationView { + currentRoom: root.currentRoom anchors.centerIn: parent - text: i18n("Accept this invitation?") - RowLayout { - QQC2.Button { - Layout.alignment : Qt.AlignHCenter - text: i18n("Reject") - - onClicked: RoomManager.leaveRoom(page.currentRoom); - } - - QQC2.Button { - Layout.alignment : Qt.AlignHCenter - text: i18n("Accept") - - onClicked: { - currentRoom.acceptInvitation(); - } - } - } - } - - model: !isLoaded ? undefined : collapseStateProxyModel - - MessageEventModel { - id: messageEventModel - - room: currentRoom - } - - Timer { - interval: 1000 - running: messageListView.atYBeginning - triggeredOnStart: true - onTriggered: { - if (messageListView.atYBeginning && messageEventModel.canFetchMore(messageEventModel.index(0, 0))) { - messageEventModel.fetchMore(messageEventModel.index(0, 0)); - } - } - repeat: true - } - - // HACK: The view should do this automatically but doesn't. - onAtYBeginningChanged: if (atYBeginning && messageEventModel.canFetchMore(messageEventModel.index(0, 0))) { - messageEventModel.fetchMore(messageEventModel.index(0, 0)); - } - - onAtYEndChanged: if (atYEnd && hasScrolledUpBefore) { - if (QQC2.ApplicationWindow.window && (QQC2.ApplicationWindow.window.visibility !== QQC2.ApplicationWindow.Hidden) && messageListView.count > 0) { - currentRoom.markAllMessagesAsRead(); - } - hasScrolledUpBefore = false; - } else if (!atYEnd) { - hasScrolledUpBefore = true; - } - - // Not rendered because the sections are part of the TimelineContainer.qml, this is only so that items have the section property available for use by sectionBanner. - // This is due to the fact that the ListView verticalLayout is BottomToTop. - // This also flips the sections which would appear at the bottom but for a timeline they still need to be at the top (bottom from the qml perspective). - // There is currently no option to put section headings at the bottom in qml. - section.property: "section" - - readonly property var sectionBannerItem: contentHeight >= height ? itemAtIndex(sectionBannerIndex()) : undefined - - function sectionBannerIndex() { - let center = messageListView.x + messageListView.width / 2; - let yStart = messageListView.y + messageListView.contentY; - let index = -1 - let i = 0 - while (index === -1 && i < 100) { - index = messageListView.indexAt(center, yStart + i); - i++; - } - return index - } - - footer: SectionDelegate { - id: sectionBanner - - anchors.left: parent.left - anchors.leftMargin: messageListView.sectionBannerItem ? messageListView.sectionBannerItem.x : 0 - anchors.right: parent.right - - maxWidth: Config.compactLayout ? messageListView.width : (messageListView.sectionBannerItem ? messageListView.sectionBannerItem.width - Kirigami.Units.largeSpacing * 2 : 0) - z: 3 - visible: messageListView.sectionBannerItem != undefined && messageListView.sectionBannerItem.ListView.section != "" && !Config.blur - labelText: messageListView.sectionBannerItem ? messageListView.sectionBannerItem.ListView.section : "" - } - footerPositioning: ListView.OverlayHeader - - QQC2.Popup { - anchors.centerIn: parent - - id: attachDialog - - padding: 16 - - contentItem: RowLayout { - QQC2.ToolButton { - Layout.preferredWidth: 160 - Layout.fillHeight: true - - icon.name: 'mail-attachment' - - text: i18n("Choose local file") - - onClicked: { - attachDialog.close() - - var fileDialog = openFileDialog.createObject(QQC2.ApplicationWindow.overlay) - - fileDialog.chosen.connect(function(path) { - if (!path) { - return; - } - currentRoom.chatBoxAttachmentPath = path; - }) - - fileDialog.open() - } - } - - Kirigami.Separator {} - - QQC2.ToolButton { - Layout.preferredWidth: 160 - Layout.fillHeight: true - - padding: 16 - - icon.name: 'insert-image' - text: i18n("Clipboard image") - onClicked: { - const localPath = Platform.StandardPaths.writableLocation(Platform.StandardPaths.CacheLocation) + "/screenshots/" + (new Date()).getTime() + ".png" - if (!Clipboard.saveImage(localPath)) { - return; - } - currentRoom.chatBoxAttachmentPath = localPath; - attachDialog.close(); - } - } - } - } - - Component { - id: openFileDialog - - OpenFileDialog { - parentWindow: page.Window.window - } - } - - - MessageFilterModel { - id: sortedMessageEventModel - - sourceModel: messageEventModel - } - - delegate: EventDelegate {} - - QQC2.RoundButton { - anchors.right: parent.right - anchors.top: parent.top - anchors.topMargin: Kirigami.Units.largeSpacing - anchors.rightMargin: Kirigami.Units.largeSpacing - implicitWidth: Kirigami.Units.gridUnit * 2 - implicitHeight: Kirigami.Units.gridUnit * 2 - - id: goReadMarkerFab - - z: 2 - visible: currentRoom && currentRoom.hasUnreadMessages && currentRoom.readMarkerLoaded - action: Kirigami.Action { - onTriggered: { - if (!Kirigami.Settings.isMobile) { - chatBox.chatBar.forceActiveFocus(); - } - messageListView.goToEvent(currentRoom.readMarkerEventId) - } - icon.name: "go-up" - shortcut: "Shift+PgUp" - } - - QQC2.ToolTip { - text: i18n("Jump to first unread message") - } - } - QQC2.RoundButton { - anchors.right: parent.right - anchors.bottom: parent.bottom - anchors.bottomMargin: Kirigami.Units.largeSpacing - anchors.rightMargin: Kirigami.Units.largeSpacing - implicitWidth: Kirigami.Units.gridUnit * 2 - implicitHeight: Kirigami.Units.gridUnit * 2 - - id: goMarkAsReadFab - - z: 2 - visible: !messageListView.atYEnd - action: Kirigami.Action { - onTriggered: { - if (!Kirigami.Settings.isMobile) { - chatBox.chatBar.forceActiveFocus(); - } - goToLastMessage(); - currentRoom.markAllMessagesAsRead(); - } - icon.name: "go-down" - } - - QQC2.ToolTip { - text: i18n("Jump to latest message") - } - } - - Component.onCompleted: { - positionViewAtBeginning(); - } - - DropArea { - id: dropAreaFile - anchors.fill: parent - onDropped: currentRoom.chatBoxAttachmentPath = drop.urls[0]; - enabled: !Controller.isFlatpak - } - - QQC2.Pane { - visible: dropAreaFile.containsDrag - anchors { - fill: parent - margins: Kirigami.Units.gridUnit - } - - Kirigami.PlaceholderMessage { - anchors.centerIn: parent - width: parent.width - (Kirigami.Units.largeSpacing * 4) - text: i18n("Drag items here to share them") - } - } - - Component { - id: messageDelegateContextMenu - - MessageDelegateContextMenu {} - } - - Component { - id: fileDelegateContextMenu - - FileDelegateContextMenu {} - } - - Component { - id: userDetailDialog - - UserDetailDialog {} - } - - TypingPane { - id: typingPane - visible: !loadingIndicator.visible && currentRoom && currentRoom.usersTyping.length > 0 - labelText: visible ? i18ncp( - "Message displayed when some users are typing", "%2 is typing", "%2 are typing", - currentRoom.usersTyping.length, - currentRoom.usersTyping.map(user => user.displayName).join(", ") - ) : "" - anchors.left: parent.left - anchors.bottom: parent.bottom - height: visible ? implicitHeight : 0 - Behavior on height { - NumberAnimation { - property: "height" - duration: Kirigami.Units.shortDuration - easing.type: Easing.OutCubic - } - } - z: 2 - } - - function goToEvent(eventID) { - const index = eventToIndex(eventID) - messageListView.positionViewAtIndex(index, ListView.Center) - itemAtIndex(index).isTemporaryHighlighted = true - } - - Item { - id: hoverActions - property var event: null - property bool userMsg: event && event.author.id === Controller.activeConnection.localUserId - property bool showEdit: event && (userMsg && (event.delegateType === MessageEventModel.Emote || event.delegateType === MessageEventModel.Message)) - property var delegate: null - property var bubble: null - property var hovered: bubble && bubble.hovered - property var visibleDelayed: (hovered || hoverHandler.hovered) && !Kirigami.Settings.isMobile - property var updateFunction - onVisibleDelayedChanged: if (visibleDelayed) { - visible = true; - } else { - // HACK: delay disapearing by 200ms, otherwise this can create some glitches - // See https://invent.kde.org/network/neochat/-/issues/333 - hoverActionsTimer.restart(); - } - Timer { - id: hoverActionsTimer - interval: 200 - onTriggered: hoverActions.visible = hoverActions.visibleDelayed; - } - - property int childOffset: userMsg && Config.showLocalMessagesOnRight && !Config.compactLayout ? (bubble ? bubble.width : 0) - childWidth : Math.max((bubble ? bubble.width : 0) - childWidth, 0) - x: delegate && bubble ? (delegate.x + bubble.x + Kirigami.Units.largeSpacing + childOffset - (Config.compactLayout ? Kirigami.Units.gridUnit * 3: 0) - (userMsg && !Config.compactLayout ? Kirigami.Units.gridUnit : 0)) : 0 - y: bubble ? bubble.mapToItem(parent, 0, 0).y - hoverActions.childHeight + Kirigami.Units.smallSpacing: 0; - - visible: false - - property alias childWidth: hoverActionsRow.width - property alias childHeight: hoverActionsRow.height - - RowLayout { - id: hoverActionsRow - z: 4 - spacing: 0 - HoverHandler { - id: hoverHandler - margin: Kirigami.Units.smallSpacing - } - Kirigami.Icon { - source: "security-high" - width: height - height: parent.height - visible: hoverActions.event ? hoverActions.event.verified : false - HoverHandler { - id: hover - } - QQC2.ToolTip.text: i18n("This message was sent from a verified device") - QQC2.ToolTip.visible: hover.hovered - QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay - } - - QQC2.Button { - QQC2.ToolTip.text: i18n("React") - QQC2.ToolTip.visible: hovered - QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay - icon.name: "preferences-desktop-emoticons" - - onClicked: emojiDialog.open(); - EmojiDialog { - id: emojiDialog - showQuickReaction: true - onChosen: { - page.currentRoom.toggleReaction(hoverActions.event.eventId, emoji); - if (!Kirigami.Settings.isMobile) { - chatBox.chatBar.forceActiveFocus(); - } - } - } - } - QQC2.Button { - QQC2.ToolTip.text: i18n("Edit") - QQC2.ToolTip.visible: hovered - QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay - visible: hoverActions.showEdit - icon.name: "document-edit" - onClicked: { - currentRoom.chatBoxEditId = hoverActions.event.eventId; - currentRoom.chatBoxReplyId = ""; - } - } - QQC2.Button { - QQC2.ToolTip.text: i18n("Reply") - QQC2.ToolTip.visible: hovered - QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay - icon.name: "mail-replied-symbolic" - onClicked: { - currentRoom.chatBoxReplyId = hoverActions.event.eventId; - currentRoom.chatBoxEditId = ""; - chatBox.chatBar.forceActiveFocus(); - } - } - } } } - - footer: ChatBox { - id: chatBox - visible: !invitation.visible && !(messageListView.count === 0 && !currentRoom.allHistoryLoaded) - width: parent.width - onMessageSent: { - if (!messageListView.atYEnd) { - goToLastMessage(); - } + Loader { + active: root.loading && !invitationLoader.active + anchors.centerIn: parent + sourceComponent: Kirigami.LoadingPlaceholder { + anchors.centerIn: parent } } background: Rectangle { color: Config.compactLayout ? Kirigami.Theme.backgroundColor : "transparent" + } - FancyEffectsContainer { - id: fancyEffectsContainer - anchors.fill: parent - z: 100 - - enabled: Config.showFancyEffects - - function processFancyEffectsReason(fancyEffect) { - if (fancyEffect === "snowflake") { - fancyEffectsContainer.showSnowEffect() - } - if (fancyEffect === "fireworks") { - fancyEffectsContainer.showFireworksEffect() - } - if (fancyEffect === "confetti") { - fancyEffectsContainer.showConfettiEffect() - } - } - - Connections { - enabled: Config.showFancyEffects - target: messageEventModel - function onFancyEffectsReasonFound(fancyEffect) { - fancyEffectsContainer.processFancyEffectsReason(fancyEffect) - } - } - - Connections { - enabled: Config.showFancyEffects - target: actionsHandler - function onShowEffect(fancyEffect) { - fancyEffectsContainer.processFancyEffectsReason(fancyEffect) + footer: Loader { + id: chatBoxLoader + active: timelineViewLoader.active + sourceComponent: ChatBox { + id: chatBox + width: parent.width + onMessageSent: { + if (!timelineViewLoader.item.atYEnd) { + timelineViewLoader.item.goToLastMessage(); } } } } + Connections { + target: RoomManager + function onCurrentRoomChanged() { + if(!RoomManager.currentRoom) { + if(pageStack.lastItem === root) { + pageStack.pop() + } + } else if (root.currentRoom.isInvite) { + root.currentRoom.clearInvitationNotification(); + } + } + + function onWarning(title, message) { + root.warning(title, message); + } + } + + ActionsHandler { + id: actionsHandler + room: root.currentRoom + } + + Shortcut { + sequence: StandardKey.Cancel + onActivated: { + if (!timelineViewLoader.item.atYEnd || currentRoom.hasUnreadMessages) { + goToLastMessage(); + currentRoom.markAllMessagesAsRead(); + } else { + applicationWindow().pageStack.get(0).forceActiveFocus(); + } + } + enabled: !root.disableCancelShortcut + } + + Connections { + target: Controller.activeConnection + function onJoinedRoom(room, invited) { + if(root.currentRoom.id === invited.id) { + RoomManager.enterRoom(room); + } + } + } + + Keys.onPressed: { + if (!(event.modifiers & Qt.ControlModifier) && event.key < Qt.Key_Escape) { + event.accepted = true; + chatBoxLoader.item.chatBar.insertText(event.text); + chatBoxLoader.item.chatBar.forceActiveFocus(); + return; + } else if (event.key === Qt.Key_PageUp) { + event.accepted = true; + timelineViewLoader.item.pageUp() + } else if (event.key === Qt.Key_PageDown) { + event.accepted = true; + timelineViewLoader.item.pageDown() + } + } + + Connections { + target: currentRoom + function onShowMessage(messageType, message) { + root.header.contentItem.text = message; + root.header.contentItem.type = messageType === ActionsHandler.Error ? Kirigami.MessageType.Error : messageType === ActionsHandler.Positive ? Kirigami.MessageType.Positive : Kirigami.MessageType.Information; + root.header.visible = true; + } + } function warning(title, message) { +<<<<<<< HEAD page.header.contentItem.text = `${title}
${message}`; page.header.contentItem.type = Kirigami.MessageType.Warning; page.header.visible = true; @@ -700,5 +251,10 @@ Kirigami.ScrollablePage { plainMessage: plainMessage, }); contextMenu.open(); +======= + root.header.contentItem.text = `${title}
${message}`; + root.header.contentItem.type = Kirigami.MessageType.Warning; + root.header.visible = true; +>>>>>>> 48e95ac6 (Refactor RoomPage into various component) } } diff --git a/src/qml/main.qml b/src/qml/main.qml index cbfd3a6b9..583a2f8c8 100644 --- a/src/qml/main.qml +++ b/src/qml/main.qml @@ -115,15 +115,6 @@ Kirigami.ApplicationWindow { user: user, }).open(); } - - function onWarning(title, message) { - if (RoomManager.currentRoom) { - const roomItem = pageStack.get(pageStack.depth - 1); - roomItem.warning(title, message); - } else { - showPassiveNotification(i18n("Warning: %1", message)); - } - } } function pushReplaceLayer(page, args) { diff --git a/src/res.qrc b/src/res.qrc index 6f056cc3f..e95793009 100644 --- a/src/res.qrc +++ b/src/res.qrc @@ -120,5 +120,7 @@ qml/Page/SearchPage.qml qml/Component/Timeline/LocationDelegate.qml qml/Component/ChatBox/LocationChooser.qml + qml/Component/TimelineView.qml + qml/Component/InvitationView.qml diff --git a/src/roommanager.cpp b/src/roommanager.cpp index b1b1ce1cb..852ca6feb 100644 --- a/src/roommanager.cpp +++ b/src/roommanager.cpp @@ -144,6 +144,10 @@ void RoomManager::enterRoom(NeoChatRoom *room) Q_EMIT replaceRoom(m_currentRoom, QString()); } + if (room && room->timelineSize() == 0) { + room->getPreviousContent(20); + } + // Save last open room m_lastRoomConfig.writeEntry(Controller::instance().activeConnection()->userId(), room->id()); }