Refactor RoomPage into various component

This commit is contained in:
Tobias Fella
2023-05-06 15:09:00 +00:00
parent c9ddf2f04a
commit e0983fcc8c
13 changed files with 742 additions and 574 deletions

View File

@@ -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();

View File

@@ -197,6 +197,8 @@ private:
QVariantMap getMediaInfoFromFileInfo(const Quotient::EventContent::FileInfo *fileInfo, const QString &eventId, bool isThumbnail = false) const;
std::vector<Quotient::event_ptr_tt<Quotient::RoomEvent>> 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();

View File

@@ -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

View File

@@ -0,0 +1,35 @@
// SPDX-FileCopyrightText: 2023 Tobias Fella <tobias.fella@kde.org>
// 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();
}
}
}
}

View File

@@ -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)

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -0,0 +1,565 @@
// SPDX-FileCopyrightText: 2020 Carl Schwan <carl@carlschwan.eu>
// 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();
}
}

View File

@@ -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}<br />${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}<br />${message}`;
root.header.contentItem.type = Kirigami.MessageType.Warning;
root.header.visible = true;
>>>>>>> 48e95ac6 (Refactor RoomPage into various component)
}
}

View File

@@ -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) {

View File

@@ -120,5 +120,7 @@
<file alias="SearchPage.qml">qml/Page/SearchPage.qml</file>
<file alias="LocationDelegate.qml">qml/Component/Timeline/LocationDelegate.qml</file>
<file alias="LocationChooser.qml">qml/Component/ChatBox/LocationChooser.qml</file>
<file alias="TimelineView.qml">qml/Component/TimelineView.qml</file>
<file alias="InvitationView.qml">qml/Component/InvitationView.qml</file>
</qresource>
</RCC>

View File

@@ -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());
}