Refactor RoomPage into various component
This commit is contained in:
@@ -150,16 +150,20 @@ void MessageEventModel::setRoom(NeoChatRoom *room)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
m_initialized = true;
|
||||||
beginInsertRows({}, timelineBaseIndex(), timelineBaseIndex() + int(events.size()) - 1);
|
beginInsertRows({}, timelineBaseIndex(), timelineBaseIndex() + int(events.size()) - 1);
|
||||||
});
|
});
|
||||||
connect(m_currentRoom, &Room::aboutToAddHistoricalMessages, this, [this](RoomEventsRange events) {
|
connect(m_currentRoom, &Room::aboutToAddHistoricalMessages, this, [this](RoomEventsRange events) {
|
||||||
if (rowCount() > 0) {
|
if (rowCount() > 0) {
|
||||||
rowBelowInserted = rowCount() - 1; // See #312
|
rowBelowInserted = rowCount() - 1; // See #312
|
||||||
}
|
}
|
||||||
|
m_initialized = true;
|
||||||
beginInsertRows({}, rowCount(), rowCount() + int(events.size()) - 1);
|
beginInsertRows({}, rowCount(), rowCount() + int(events.size()) - 1);
|
||||||
});
|
});
|
||||||
connect(m_currentRoom, &Room::addedMessages, this, [this](int lowest, int biggest) {
|
connect(m_currentRoom, &Room::addedMessages, this, [this](int lowest, int biggest) {
|
||||||
endInsertRows();
|
if (m_initialized) {
|
||||||
|
endInsertRows();
|
||||||
|
}
|
||||||
if (!m_lastReadEventIndex.isValid()) {
|
if (!m_lastReadEventIndex.isValid()) {
|
||||||
// no read marker, so see if we need to create one.
|
// no read marker, so see if we need to create one.
|
||||||
#ifdef QUOTIENT_07
|
#ifdef QUOTIENT_07
|
||||||
@@ -177,6 +181,7 @@ void MessageEventModel::setRoom(NeoChatRoom *room)
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
connect(m_currentRoom, &Room::pendingEventAboutToAdd, this, [this] {
|
connect(m_currentRoom, &Room::pendingEventAboutToAdd, this, [this] {
|
||||||
|
m_initialized = true;
|
||||||
beginInsertRows({}, 0, 0);
|
beginInsertRows({}, 0, 0);
|
||||||
});
|
});
|
||||||
connect(m_currentRoom, &Room::pendingEventAdded, this, &MessageEventModel::endInsertRows);
|
connect(m_currentRoom, &Room::pendingEventAdded, this, &MessageEventModel::endInsertRows);
|
||||||
@@ -279,6 +284,7 @@ void MessageEventModel::moveReadMarker(const QString &toEventId)
|
|||||||
// need to be displayed.
|
// need to be displayed.
|
||||||
if (newRow > timelineBaseIndex()) {
|
if (newRow > timelineBaseIndex()) {
|
||||||
// The user didn't read all the messages yet.
|
// The user didn't read all the messages yet.
|
||||||
|
m_initialized = true;
|
||||||
beginInsertRows({}, newRow, newRow);
|
beginInsertRows({}, newRow, newRow);
|
||||||
m_lastReadEventIndex = QPersistentModelIndex(index(newRow, 0));
|
m_lastReadEventIndex = QPersistentModelIndex(index(newRow, 0));
|
||||||
endInsertRows();
|
endInsertRows();
|
||||||
|
|||||||
@@ -197,6 +197,8 @@ private:
|
|||||||
QVariantMap getMediaInfoFromFileInfo(const Quotient::EventContent::FileInfo *fileInfo, const QString &eventId, bool isThumbnail = false) const;
|
QVariantMap getMediaInfoFromFileInfo(const Quotient::EventContent::FileInfo *fileInfo, const QString &eventId, bool isThumbnail = false) const;
|
||||||
|
|
||||||
std::vector<Quotient::event_ptr_tt<Quotient::RoomEvent>> m_extraEvents;
|
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:
|
Q_SIGNALS:
|
||||||
void roomChanged();
|
void roomChanged();
|
||||||
|
|||||||
@@ -21,6 +21,9 @@ ColumnLayout {
|
|||||||
|
|
||||||
spacing: 0
|
spacing: 0
|
||||||
|
|
||||||
|
Kirigami.Theme.colorSet: Kirigami.Theme.View
|
||||||
|
Kirigami.Theme.inherit: false
|
||||||
|
|
||||||
Kirigami.InlineMessage {
|
Kirigami.InlineMessage {
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
Layout.leftMargin: 1 // So we can see the border
|
Layout.leftMargin: 1 // So we can see the border
|
||||||
|
|||||||
35
src/qml/Component/InvitationView.qml
Normal file
35
src/qml/Component/InvitationView.qml
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -26,6 +26,8 @@ Flow {
|
|||||||
|
|
||||||
background: Kirigami.ShadowedRectangle {
|
background: Kirigami.ShadowedRectangle {
|
||||||
color: checked ? Kirigami.Theme.positiveBackgroundColor : Kirigami.Theme.backgroundColor
|
color: checked ? Kirigami.Theme.positiveBackgroundColor : Kirigami.Theme.backgroundColor
|
||||||
|
Kirigami.Theme.inherit: false
|
||||||
|
Kirigami.Theme.colorSet: Kirigami.Theme.View
|
||||||
radius: height / 2
|
radius: height / 2
|
||||||
shadow.size: Kirigami.Units.smallSpacing
|
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)
|
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)
|
||||||
|
|||||||
@@ -17,8 +17,8 @@ QQC2.ItemDelegate {
|
|||||||
topPadding: Kirigami.Units.largeSpacing * 2
|
topPadding: Kirigami.Units.largeSpacing * 2
|
||||||
|
|
||||||
// extraWidth defines how the delegate can grow after the listView gets very wide
|
// 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 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: Config.compactLayout ? messageListView.width - Kirigami.Units.largeSpacing * 2 : Math.min(messageListView.width - Kirigami.Units.largeSpacing * 2, Kirigami.Units.gridUnit * 40 + extraWidth)
|
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
|
property bool isTemporaryHighlighted: false
|
||||||
|
|
||||||
@@ -78,6 +78,8 @@ QQC2.ItemDelegate {
|
|||||||
return Kirigami.Theme.backgroundColor
|
return Kirigami.Theme.backgroundColor
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Kirigami.Theme.inherit: false
|
||||||
|
Kirigami.Theme.colorSet: Kirigami.Theme.View
|
||||||
opacity: readMarkerDelegate.isTemporaryHighlighted ? 1 : 0.6
|
opacity: readMarkerDelegate.isTemporaryHighlighted ? 1 : 0.6
|
||||||
radius: Kirigami.Units.smallSpacing
|
radius: Kirigami.Units.smallSpacing
|
||||||
shadow.size: Kirigami.Units.smallSpacing
|
shadow.size: Kirigami.Units.smallSpacing
|
||||||
|
|||||||
@@ -15,8 +15,8 @@ QQC2.Control {
|
|||||||
readonly property bool sectionVisible: model.showSection
|
readonly property bool sectionVisible: model.showSection
|
||||||
|
|
||||||
// extraWidth defines how the delegate can grow after the listView gets very wide
|
// 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 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: Config.compactLayout ? messageListView.width: Math.min(messageListView.width, Kirigami.Units.gridUnit * 40 + extraWidth)
|
readonly property int delegateMaxWidth: parent ? (Config.compactLayout ? parent.width: Math.min(parent.width, Kirigami.Units.gridUnit * 40 + extraWidth)) : 0
|
||||||
|
|
||||||
width: delegateMaxWidth
|
width: delegateMaxWidth
|
||||||
|
|
||||||
|
|||||||
@@ -37,9 +37,9 @@ ColumnLayout {
|
|||||||
|
|
||||||
// The bubble and delegate widths are allowed to grow once the ListView gets beyond a certain size
|
// 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
|
// 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 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)
|
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
|
width: delegateWidth
|
||||||
|
|||||||
565
src/qml/Component/TimelineView.qml
Normal file
565
src/qml/Component/TimelineView.qml
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,8 +5,6 @@
|
|||||||
import QtQuick 2.15
|
import QtQuick 2.15
|
||||||
import QtQuick.Controls 2.15 as QQC2
|
import QtQuick.Controls 2.15 as QQC2
|
||||||
import QtQuick.Layouts 1.15
|
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 QtQuick.Window 2.15
|
||||||
|
|
||||||
import org.kde.kirigami 2.19 as Kirigami
|
import org.kde.kirigami 2.19 as Kirigami
|
||||||
@@ -14,100 +12,36 @@ import org.kde.kitemmodels 1.0
|
|||||||
|
|
||||||
import org.kde.neochat 1.0
|
import org.kde.neochat 1.0
|
||||||
|
|
||||||
Kirigami.ScrollablePage {
|
Kirigami.Page {
|
||||||
id: 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 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
|
/// Used to determine if scrolling to the bottom should mark the message as unread
|
||||||
property bool hasScrolledUpBefore: false;
|
property bool hasScrolledUpBefore: false;
|
||||||
|
|
||||||
/// Disable cancel shortcut. Used by the seperate window since it provide its own
|
/// Disable cancel shortcut. Used by the separate window since it provides its own cancel implementation.
|
||||||
/// cancel implementation.
|
|
||||||
property bool disableCancelShortcut: false
|
property bool disableCancelShortcut: false
|
||||||
|
|
||||||
title: currentRoom.displayName
|
title: currentRoom.displayName
|
||||||
|
focus: true
|
||||||
|
padding: 0
|
||||||
|
|
||||||
KeyNavigation.left: pageStack.get(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: {
|
onCurrentRoomChanged: {
|
||||||
|
if (!timelineViewLoader.item) {
|
||||||
|
return
|
||||||
|
}
|
||||||
applicationWindow().hoverLinkIndicator.text = "";
|
applicationWindow().hoverLinkIndicator.text = "";
|
||||||
messageListView.positionViewAtBeginning();
|
timelineViewLoader.item.positionViewAtBeginning();
|
||||||
hasScrolledUpBefore = false;
|
hasScrolledUpBefore = false;
|
||||||
if (!Kirigami.Settings.isMobile) {
|
if (!Kirigami.Settings.isMobile) {
|
||||||
chatBox.chatBar.forceActiveFocus();
|
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 {
|
header: QQC2.Control {
|
||||||
height: visible ? implicitHeight : 0
|
height: visible ? implicitHeight : 0
|
||||||
visible: false
|
visible: false
|
||||||
@@ -118,505 +52,122 @@ Kirigami.ScrollablePage {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Kirigami.LoadingPlaceholder {
|
Loader {
|
||||||
id: loadingIndicator
|
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
|
anchors.centerIn: parent
|
||||||
visible: loading
|
sourceComponent: InvitationView {
|
||||||
}
|
currentRoom: root.currentRoom
|
||||||
|
|
||||||
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
|
|
||||||
anchors.centerIn: parent
|
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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loader {
|
||||||
footer: ChatBox {
|
active: root.loading && !invitationLoader.active
|
||||||
id: chatBox
|
anchors.centerIn: parent
|
||||||
visible: !invitation.visible && !(messageListView.count === 0 && !currentRoom.allHistoryLoaded)
|
sourceComponent: Kirigami.LoadingPlaceholder {
|
||||||
width: parent.width
|
anchors.centerIn: parent
|
||||||
onMessageSent: {
|
|
||||||
if (!messageListView.atYEnd) {
|
|
||||||
goToLastMessage();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
background: Rectangle {
|
background: Rectangle {
|
||||||
color: Config.compactLayout ? Kirigami.Theme.backgroundColor : "transparent"
|
color: Config.compactLayout ? Kirigami.Theme.backgroundColor : "transparent"
|
||||||
|
}
|
||||||
|
|
||||||
FancyEffectsContainer {
|
footer: Loader {
|
||||||
id: fancyEffectsContainer
|
id: chatBoxLoader
|
||||||
anchors.fill: parent
|
active: timelineViewLoader.active
|
||||||
z: 100
|
sourceComponent: ChatBox {
|
||||||
|
id: chatBox
|
||||||
enabled: Config.showFancyEffects
|
width: parent.width
|
||||||
|
onMessageSent: {
|
||||||
function processFancyEffectsReason(fancyEffect) {
|
if (!timelineViewLoader.item.atYEnd) {
|
||||||
if (fancyEffect === "snowflake") {
|
timelineViewLoader.item.goToLastMessage();
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) {
|
function warning(title, message) {
|
||||||
|
<<<<<<< HEAD
|
||||||
page.header.contentItem.text = `${title}<br />${message}`;
|
page.header.contentItem.text = `${title}<br />${message}`;
|
||||||
page.header.contentItem.type = Kirigami.MessageType.Warning;
|
page.header.contentItem.type = Kirigami.MessageType.Warning;
|
||||||
page.header.visible = true;
|
page.header.visible = true;
|
||||||
@@ -700,5 +251,10 @@ Kirigami.ScrollablePage {
|
|||||||
plainMessage: plainMessage,
|
plainMessage: plainMessage,
|
||||||
});
|
});
|
||||||
contextMenu.open();
|
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -115,15 +115,6 @@ Kirigami.ApplicationWindow {
|
|||||||
user: user,
|
user: user,
|
||||||
}).open();
|
}).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) {
|
function pushReplaceLayer(page, args) {
|
||||||
|
|||||||
@@ -120,5 +120,7 @@
|
|||||||
<file alias="SearchPage.qml">qml/Page/SearchPage.qml</file>
|
<file alias="SearchPage.qml">qml/Page/SearchPage.qml</file>
|
||||||
<file alias="LocationDelegate.qml">qml/Component/Timeline/LocationDelegate.qml</file>
|
<file alias="LocationDelegate.qml">qml/Component/Timeline/LocationDelegate.qml</file>
|
||||||
<file alias="LocationChooser.qml">qml/Component/ChatBox/LocationChooser.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>
|
</qresource>
|
||||||
</RCC>
|
</RCC>
|
||||||
|
|||||||
@@ -144,6 +144,10 @@ void RoomManager::enterRoom(NeoChatRoom *room)
|
|||||||
Q_EMIT replaceRoom(m_currentRoom, QString());
|
Q_EMIT replaceRoom(m_currentRoom, QString());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (room && room->timelineSize() == 0) {
|
||||||
|
room->getPreviousContent(20);
|
||||||
|
}
|
||||||
|
|
||||||
// Save last open room
|
// Save last open room
|
||||||
m_lastRoomConfig.writeEntry(Controller::instance().activeConnection()->userId(), room->id());
|
m_lastRoomConfig.writeEntry(Controller::instance().activeConnection()->userId(), room->id());
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user