Room Drawer Media Tab

Add a tab bar to the room drawer which includes a new media tab in addition to the room information tab. This mr completes the architecture for adding others easily later e.g. message highlights or threads.

To put this together I had to make sure things like the menus and the maximize delegate were available to both the room drawer and page so there is some rework there to put it all together.

Wide\
![image](/uploads/b7d3a3ee00016f9ede5cf6fb93e7b40c/image.png)

Mobile\
![image](/uploads/aa02e23f79b37f6cad903d3f356e0ef4/image.png)
This commit is contained in:
James Graham
2023-09-03 10:25:04 +00:00
parent 54cc3ac761
commit 199772a013
17 changed files with 562 additions and 264 deletions

View File

@@ -2,12 +2,17 @@
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
#include "mediamessagefiltermodel.h"
#include "models/messageeventmodel.h"
#include <Quotient/room.h>
MediaMessageFilterModel::MediaMessageFilterModel(QObject *parent)
#include "messageeventmodel.h"
#include "messagefiltermodel.h"
MediaMessageFilterModel::MediaMessageFilterModel(QObject *parent, MessageFilterModel *sourceMediaModel)
: QSortFilterProxyModel(parent)
{
Q_ASSERT(sourceMediaModel);
setSourceModel(sourceMediaModel);
}
bool MediaMessageFilterModel::filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const
@@ -43,9 +48,9 @@ QVariant MediaMessageFilterModel::data(const QModelIndex &index, int role) const
}
if (role == TypeRole) {
if (mapToSource(index).data(MessageEventModel::DelegateTypeRole).toInt() == MessageEventModel::DelegateType::Image) {
return 0;
return MediaType::Image;
} else {
return 1;
return MediaType::Video;
}
}
if (role == CaptionRole) {
@@ -57,6 +62,13 @@ QVariant MediaMessageFilterModel::data(const QModelIndex &index, int role) const
if (role == SourceHeightRole) {
return mapToSource(index).data(MessageEventModel::MediaInfoRole).toMap()[QStringLiteral("height")].toFloat();
}
// We need to catch this one and return true if the next media object was
// on a different day.
if (role == MessageEventModel::ShowSectionRole) {
const auto day = mapToSource(index).data(MessageEventModel::TimeRole).toDateTime().toLocalTime().date();
const auto previousEventDay = mapToSource(this->index(index.row() + 1, 0)).data(MessageEventModel::TimeRole).toDateTime().toLocalTime().date();
return day != previousEventDay;
}
return sourceModel()->data(mapToSource(index), role);
}

View File

@@ -4,9 +4,12 @@
#pragma once
#include <QSortFilterProxyModel>
#include <qobjectdefs.h>
#include "models/messagefiltermodel.h"
class MessageFilterModel;
/**
* @class MediaMessageFilterModel
*
@@ -18,6 +21,12 @@ class MediaMessageFilterModel : public QSortFilterProxyModel
{
Q_OBJECT
public:
enum MediaType {
Image = 0,
Video,
};
Q_ENUM(MediaType)
/**
* @brief Defines the model roles.
*/
@@ -31,7 +40,7 @@ public:
};
Q_ENUM(Roles)
explicit MediaMessageFilterModel(QObject *parent = nullptr);
explicit MediaMessageFilterModel(QObject *parent = nullptr, MessageFilterModel *sourceMediaModel = nullptr);
/**
* @brief Custom filter to show only image and video messages.
@@ -43,7 +52,7 @@ public:
*
* @sa QSortFilterProxyModel::data
*/
[[nodiscard]] QVariant data(const QModelIndex &idx, int role = Qt::DisplayRole) const override;
[[nodiscard]] QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
/**
* @brief Returns a mapping from Role enum values to role names.

View File

@@ -10,9 +10,12 @@
using namespace Quotient;
MessageFilterModel::MessageFilterModel(QObject *parent)
MessageFilterModel::MessageFilterModel(QObject *parent, MessageEventModel *sourceMessageModel)
: QSortFilterProxyModel(parent)
{
Q_ASSERT(sourceMessageModel);
setSourceModel(sourceMessageModel);
connect(NeoChatConfig::self(), &NeoChatConfig::ShowStateEventChanged, this, [this] {
invalidateFilter();
});

View File

@@ -33,7 +33,7 @@ public:
LastRole, // Keep this last
};
explicit MessageFilterModel(QObject *parent = nullptr);
explicit MessageFilterModel(QObject *parent = nullptr, MessageEventModel *sourceMessageModel = nullptr);
/**
* @brief Custom filter function to remove hidden messages.

View File

@@ -100,6 +100,11 @@ Components.AlbumMaximizeComponent {
dialog.currentFile = dialog.folder + "/" + currentRoom.fileNameToDownload(root.currentEventId)
}
Component {
id: fileDelegateContextMenu
FileDelegateContextMenu {}
}
Component {
id: saveAsDialog
Platform.FileDialog {

View File

@@ -129,13 +129,19 @@ TimelineContainer {
TapHandler {
acceptedButtons: Qt.LeftButton
gesturePolicy: TapHandler.ReleaseWithinBounds | TapHandler.WithinBounds
onTapped: {
imageContainer.QQC2.ToolTip.hide()
if (root.mediaInfo.animated) {
imageContainer.imageItem.paused = true
}
root.ListView.view.interactive = false
root.ListView.view.showMaximizedMedia(root.index)
// We need to make sure the index is that of the MediaMessageFilterModel.
if (root.ListView.view.model instanceof MessageFilterModel) {
RoomManager.maximizeMedia(RoomManager.mediaMessageFilterModel.getRowForSourceItem(root.index))
} else {
RoomManager.maximizeMedia(root.index)
}
}
}
@@ -144,7 +150,7 @@ TimelineContainer {
openSavedFile()
} else {
openOnFinished = true
currentRoom.downloadFile(root.eventId, StandardPaths.writableLocation(StandardPaths.CacheLocation) + "/" + root.eventId.replace(":", "_").replace("/", "_").replace("+", "_") + currentRoom.fileNameToDownload(root.eventId))
ListView.view.currentRoom.downloadFile(root.eventId, StandardPaths.writableLocation(StandardPaths.CacheLocation) + "/" + root.eventId.replace(":", "_").replace("/", "_").replace("+", "_") + ListView.view.currentRoom.fileNameToDownload(root.eventId))
}
}

View File

@@ -16,6 +16,8 @@ QQC2.ItemDelegate {
property alias labelText: sectionLabel.text
property var maxWidth: Number.POSITIVE_INFINITY
property int colorSet: Kirigami.Theme.Window
topPadding: Kirigami.Units.largeSpacing
bottomPadding: 0 // Note not 0 by default
@@ -42,6 +44,6 @@ QQC2.ItemDelegate {
background: Rectangle {
color: Config.blur ? "transparent" : Kirigami.Theme.backgroundColor
Kirigami.Theme.inherit: false
Kirigami.Theme.colorSet: Config.compactLayout ? Kirigami.Theme.View : Kirigami.Theme.Window
Kirigami.Theme.colorSet: sectionDelegate.colorSet
}
}

View File

@@ -65,6 +65,16 @@ ColumnLayout {
*/
required property bool showAuthor
/**
* @brief Whether the author should always be shown.
*
* This is primarily used when these delegates are used in a filtered list of
* events rather than a sequential timeline, e.g. the media model view.
*
* @note This setting still respects the avatar configuration settings.
*/
property bool alwaysShowAuthor: false
/**
* @brief The delegate type of the message.
*/
@@ -262,12 +272,17 @@ ColumnLayout {
*/
property bool cardBackground: true
/**
* @brief Whether the delegate should always stretch to the maximum availabel width.
*/
property bool alwaysMaxWidth: false
/**
* @brief Whether local user messages should be aligned right.
*
* TODO: make private
*/
property bool showUserMessageOnRight: Config.showLocalMessagesOnRight && root.author.isLocalUser && !Config.compactLayout
property bool showUserMessageOnRight: Config.showLocalMessagesOnRight && root.author.isLocalUser && !Config.compactLayout && !alwaysMaxWidth
/**
* @brief Whether the message should be highlighted.
@@ -296,7 +311,7 @@ ColumnLayout {
width: parent ? timelineDelegateSizeHelper.currentWidth : 0
spacing: Kirigami.Units.smallSpacing
state: Config.compactLayout ? "alignLeft" : "alignCenter"
state: Config.compactLayout || root.alwaysMaxWidth ? "alignLeft" : "alignCenter"
// Align left when in compact mode and center when using bubbles
states: [
State {
@@ -325,21 +340,21 @@ ColumnLayout {
SectionDelegate {
id: sectionDelegate
Layout.fillWidth: true
visible: root.showSection
labelText: root.section
colorSet: Config.compactLayout || root.alwaysMaxWidth ? Kirigami.Theme.View : Kirigami.Theme.Window
}
QQC2.ItemDelegate {
id: mainContainer
Layout.fillWidth: true
Layout.topMargin: root.showAuthor ? Kirigami.Units.largeSpacing : (Config.compactLayout ? 1 : Kirigami.Units.smallSpacing)
Layout.topMargin: root.showAuthor || root.alwaysShowAuthor ? Kirigami.Units.largeSpacing : (Config.compactLayout ? 1 : Kirigami.Units.smallSpacing)
Layout.leftMargin: Kirigami.Units.smallSpacing
Layout.rightMargin: Kirigami.Units.smallSpacing
implicitHeight: Math.max(root.showAuthor ? avatar.implicitHeight : 0, bubble.height)
implicitHeight: Math.max(root.showAuthor || root.alwaysShowAuthor ? avatar.implicitHeight : 0, bubble.height)
Component.onCompleted: {
if (root.isReply && root.reply === undefined) {
@@ -365,7 +380,7 @@ ColumnLayout {
topMargin: Kirigami.Units.smallSpacing
}
visible: root.showAuthor &&
visible: (root.showAuthor || root.alwaysShowAuthor) &&
Config.showAvatarInTimeline &&
(Config.compactLayout || !showUserMessageOnRight)
name: root.author.displayName
@@ -395,7 +410,7 @@ ColumnLayout {
rightMargin: Kirigami.Units.largeSpacing
}
// HACK: anchoring didn't reset anchors.right when switching from parent.right to undefined reliably
width: Config.compactLayout ? mainContainer.width - (Config.showAvatarInTimeline ? Kirigami.Units.gridUnit * 2 : 0) + Kirigami.Units.largeSpacing * 2 : implicitWidth
width: Config.compactLayout || root.alwaysMaxWidth ? mainContainer.width - (Config.showAvatarInTimeline ? Kirigami.Units.gridUnit * 2 : 0) + Kirigami.Units.largeSpacing * 2 : implicitWidth
state: showUserMessageOnRight ? "userMessageOnRight" : "userMessageOnLeft"
// states for anchor animations on window resize
@@ -440,7 +455,7 @@ ColumnLayout {
id: rowLayout
spacing: Kirigami.Units.smallSpacing
visible: root.showAuthor
visible: root.showAuthor || root.alwaysShowAuthor
QQC2.Label {
id: nameLabel
@@ -535,7 +550,7 @@ ColumnLayout {
}
background: Rectangle {
visible: mainContainer.hovered && Config.compactLayout
visible: mainContainer.hovered && (Config.compactLayout || root.alwaysMaxWidth)
color: Kirigami.ColorUtils.tintWithAlpha(Kirigami.Theme.backgroundColor, Kirigami.Theme.highlightColor, 0.15)
radius: Kirigami.Units.smallSpacing
}
@@ -577,6 +592,16 @@ ColumnLayout {
return (yoff + height > 0 && yoff < ListView.view.height)
}
Component {
id: messageDelegateContextMenu
MessageDelegateContextMenu {}
}
Component {
id: fileDelegateContextMenu
FileDelegateContextMenu {}
}
/// Open message context dialog for file and videos
function openFileContext(file) {
const contextMenu = fileDelegateContextMenu.createObject(root, {
@@ -616,8 +641,8 @@ ColumnLayout {
startBreakpoint: Kirigami.Units.gridUnit * 46
endBreakpoint: Kirigami.Units.gridUnit * 66
startPercentWidth: 100
endPercentWidth: Config.compactLayout ? 100 : 85
maxWidth: Config.compactLayout ? -1 : Kirigami.Units.gridUnit * 60
endPercentWidth: Config.compactLayout || root.alwaysMaxWidth ? 100 : 85
maxWidth: Config.compactLayout || root.alwaysMaxWidth ? -1 : Kirigami.Units.gridUnit * 60
parentWidth: root.parent ? root.parent.width - (Config.compactLayout && root.ListView.view.width >= Kirigami.Units.gridUnit * 20 ? Kirigami.Units.gridUnit * 2 + Kirigami.Units.largeSpacing : 0) : 0
}
@@ -625,8 +650,8 @@ ColumnLayout {
id: bubbleSizeHelper
startBreakpoint: Kirigami.Units.gridUnit * 25
endBreakpoint: Kirigami.Units.gridUnit * 40
startPercentWidth: Config.compactLayout ? 100 : 90
endPercentWidth: Config.compactLayout ? 100 : 60
startPercentWidth: Config.compactLayout || root.alwaysMaxWidth ? 100 : 90
endPercentWidth: Config.compactLayout || root.alwaysMaxWidth ? 100 : 60
parentWidth: mainContainer.availableWidth - (Config.showAvatarInTimeline ? avatar.width + bubble.anchors.leftMargin : 0)
}

View File

@@ -292,7 +292,12 @@ TimelineContainer {
onTriggered: {
root.ListView.view.interactive = false
vid.pause()
root.ListView.view.showMaximizedMedia(root.index)
// We need to make sure the index is that of the MediaMessageFilterModel.
if (root.ListView.view.model instanceof MessageFilterModel) {
RoomManager.maximizeMedia(RoomManager.mediaMessageFilterModel.getRowForSourceItem(root.index))
} else {
RoomManager.maximizeMedia(root.index)
}
}
}
}
@@ -328,6 +333,7 @@ TimelineContainer {
TapHandler {
acceptedButtons: Qt.LeftButton
gesturePolicy: TapHandler.ReleaseWithinBounds | TapHandler.WithinBounds
onTapped: if (root.progressInfo.completed) {
if (vid.playbackState == MediaPlayer.PlayingState) {
vid.pause()
@@ -352,7 +358,7 @@ TimelineContainer {
playSavedFile()
} else {
playOnFinished = true
currentRoom.downloadFile(root.eventId, Platform.StandardPaths.writableLocation(Platform.StandardPaths.CacheLocation) + "/" + root.eventId.replace(":", "_").replace("/", "_").replace("+", "_") + currentRoom.fileNameToDownload(root.eventId))
ListView.view.currentRoom.downloadFile(root.eventId, Platform.StandardPaths.writableLocation(Platform.StandardPaths.CacheLocation) + "/" + root.eventId.replace(":", "_").replace("/", "_").replace("+", "_") + ListView.view.currentRoom.fileNameToDownload(root.eventId))
}
}

View File

@@ -32,6 +32,9 @@ QQC2.ScrollView {
ListView {
id: messageListView
// So that delegates can access the current room properly.
readonly property NeoChatRoom currentRoom: root.currentRoom
readonly property int largestVisibleIndex: count > 0 ? indexAt(contentX + (width / 2), contentY + height - 1) : -1
readonly property var sectionBannerItem: contentHeight >= height ? itemAtIndex(sectionBannerIndex()) : undefined
@@ -47,33 +50,23 @@ QQC2.ScrollView {
interactive: Kirigami.Settings.isMobile
bottomMargin: Kirigami.Units.largeSpacing + Math.round(Kirigami.Theme.defaultFont.pointSize * 2)
model: sortedMessageEventModel
MessageEventModel {
id: messageEventModel
room: root.currentRoom
}
MessageFilterModel {
id: sortedMessageEventModel
sourceModel: messageEventModel
}
model: RoomManager.messageFilterModel
Timer {
interval: 1000
running: messageListView.atYBeginning
triggeredOnStart: true
onTriggered: {
if (messageListView.atYBeginning && messageEventModel.canFetchMore(messageEventModel.index(0, 0))) {
messageEventModel.fetchMore(messageEventModel.index(0, 0));
if (messageListView.atYBeginning && RoomManager.messageEventModel.canFetchMore(RoomManager.messageEventModel.index(0, 0))) {
RoomManager.messageEventModel.fetchMore(RoomManager.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));
onAtYBeginningChanged: if (atYBeginning && RoomManager.messageEventModel.canFetchMore(RoomManager.messageEventModel.index(0, 0))) {
RoomManager.messageEventModel.fetchMore(RoomManager.messageEventModel.index(0, 0));
}
Timer {
@@ -207,18 +200,6 @@ QQC2.ScrollView {
}
}
Component {
id: messageDelegateContextMenu
MessageDelegateContextMenu {}
}
Component {
id: fileDelegateContextMenu
FileDelegateContextMenu {}
}
TypingPane {
id: typingPane
visible: root.currentRoom && root.currentRoom.usersTyping.length > 0
@@ -285,7 +266,7 @@ QQC2.ScrollView {
}
Connections {
target: messageEventModel
target: RoomManager.messageEventModel
function onRowsInserted() {
markReadIfVisibleTimer.restart()
@@ -326,7 +307,7 @@ QQC2.ScrollView {
Connections {
//enabled: Config.showFancyEffects
target: messageEventModel
target: RoomManager.messageEventModel
function onFancyEffectsReasonFound(fancyEffect) {
fancyEffectsContainer.processFancyEffectsReason(fancyEffect)
@@ -344,21 +325,23 @@ QQC2.ScrollView {
}
}
MediaMessageFilterModel {
id: mediaMessageFilterModel
sourceModel: sortedMessageEventModel
}
Component {
id: maximizeComponent
NeochatMaximizeComponent {
model: mediaMessageFilterModel
model: RoomManager.mediaMessageFilterModel
}
}
Connections {
target: RoomManager
function onShowMaximizedMedia(index) {
messageListView.showMaximizedMedia(index)
}
}
function showMaximizedMedia(index) {
var popup = maximizeComponent.createObject(QQC2.ApplicationWindow.overlay, {
initialIndex: index === -1 ? -1 : mediaMessageFilterModel.getRowForSourceItem(index)
initialIndex: index
})
popup.closed.connect(() => {
messageListView.interactive = true
@@ -374,10 +357,10 @@ QQC2.ScrollView {
}
function eventToIndex(eventID) {
const index = messageEventModel.eventIdToRow(eventID)
const index = RoomManager.messageEventModel.eventIdToRow(eventID)
if (index === -1)
return -1
return sortedMessageEventModel.mapFromSource(messageEventModel.index(index, 0)).row
return RoomManager.messageFilterModel.mapFromSource(RoomManager.messageEventModel.index(index, 0)).row
}
function firstVisibleIndex() {

View File

@@ -65,9 +65,12 @@ Kirigami.OverlayDrawer {
onAnimatingChanged: if (dim === false) dim = undefined
topPadding: 0
bottomPadding: 0
leftPadding: 0
rightPadding: 0
Kirigami.Theme.colorSet: Kirigami.Theme.View
contentItem: Loader {
id: loader
active: root.drawerOpen
@@ -75,6 +78,8 @@ Kirigami.OverlayDrawer {
sourceComponent: ColumnLayout {
spacing: 0
Component.onCompleted: infoAction.toggle()
QQC2.ToolBar {
Layout.fillWidth: true
@@ -83,7 +88,7 @@ Kirigami.OverlayDrawer {
contentItem: RowLayout {
Kirigami.Heading {
Layout.fillWidth: true
text: i18n("Room information")
text: drawerItemLoader.item ? drawerItemLoader.item.title : ""
}
QQC2.ToolButton {
@@ -102,18 +107,47 @@ Kirigami.OverlayDrawer {
}
}
QQC2.ScrollView {
Loader {
id: drawerItemLoader
Layout.fillWidth: true
Layout.fillHeight: true
sourceComponent: roomInformation
}
// HACK: Hide unnecessary horizontal scrollbar (https://bugreports.qt.io/browse/QTBUG-83890)
QQC2.ScrollBar.horizontal.policy: QQC2.ScrollBar.AlwaysOff
Component {
id: roomInformation
RoomInformation {
id: roomInformation
room: root.room
}
}
Component {
id: roomMedia
RoomMedia {
currentRoom: root.room
}
}
Kirigami.NavigationTabBar {
id: navigationBar
Layout.fillWidth: true
Kirigami.Theme.colorSet: Kirigami.Theme.Window
Kirigami.Theme.inherit: false
actions: [
Kirigami.Action {
id: infoAction
text: i18n("Information")
icon.name: "documentinfo"
onTriggered: drawerItemLoader.sourceComponent = roomInformation
},
Kirigami.Action {
text: i18n("Media")
icon.name: "mail-attachment-symbollic"
onTriggered: drawerItemLoader.sourceComponent = roomMedia
}
]
}
}
}
}

View File

@@ -18,7 +18,7 @@ import org.kde.neochat 1.0
*
* @sa RoomDrawer
*/
Kirigami.ScrollablePage {
Kirigami.Page {
id: root
/**
@@ -26,18 +26,66 @@ Kirigami.ScrollablePage {
*/
readonly property NeoChatRoom room: RoomManager.currentRoom
title: roomInformation.title
title: drawerItemLoader.item ? drawerItemLoader.item.title : ""
topPadding: 0
bottomPadding: 0
leftPadding: 0
rightPadding: 0
Kirigami.Theme.colorSet: Kirigami.Theme.View
Kirigami.Theme.inherit: false
Component.onCompleted: infoAction.toggle()
actions {
main: Kirigami.Action {
displayHint: Kirigami.DisplayHint.IconOnly
text: i18n("Settings")
icon.name: "settings-configure"
onTriggered: applicationWindow().pageStack.pushDialogLayer('qrc:/Categories.qml', {room: root.room}, { title: i18n("Room Settings") })
}
}
RoomInformation {
Loader {
id: drawerItemLoader
width: parent.width
height: parent.height
sourceComponent: roomInformation
}
Component {
id: roomInformation
room: root.room
RoomInformation {
room: root.room
}
}
Component {
id: roomMedia
RoomMedia {
currentRoom: root.room
}
}
footer: Kirigami.NavigationTabBar {
id: navigationBar
Kirigami.Theme.colorSet: Kirigami.Theme.Window
Kirigami.Theme.inherit: false
actions: [
Kirigami.Action {
id: infoAction
text: i18n("Information")
icon.name: "documentinfo"
onTriggered: drawerItemLoader.sourceComponent = roomInformation
},
Kirigami.Action {
text: i18n("Media")
icon.name: "mail-attachment-symbollic"
onTriggered: drawerItemLoader.sourceComponent = roomMedia
}
]
}
Connections {

View File

@@ -24,7 +24,7 @@ import org.kde.neochat 1.0
*
* @sa RoomDrawer, RoomDrawerPage
*/
ListView {
QQC2.ScrollView {
id: root
/**
@@ -37,206 +37,212 @@ ListView {
*/
readonly property string title: i18nc("@action:title", "Room information")
header: ColumnLayout {
id: columnLayout
// HACK: Hide unnecessary horizontal scrollbar (https://bugreports.qt.io/browse/QTBUG-83890)
QQC2.ScrollBar.horizontal.policy: QQC2.ScrollBar.AlwaysOff
property alias userListSearchField: userListSearchField
ListView {
id: userList
header: ColumnLayout {
id: columnLayout
spacing: 0
width: root.width
property alias userListSearchField: userListSearchField
Loader {
active: true
Layout.fillWidth: true
Layout.topMargin: Kirigami.Units.smallSpacing
sourceComponent: root.room.isDirectChat() ? directChatDrawerHeader : groupChatDrawerHeader
onItemChanged: if (item) {
root.positionViewAtBeginning();
}
}
Kirigami.ListSectionHeader {
label: i18n("Options")
activeFocusOnTab: false
Layout.fillWidth: true
}
Delegates.RoundedItemDelegate {
id: devtoolsButton
icon.name: "tools"
text: i18n("Open developer tools")
visible: Config.developerTools
Layout.fillWidth: true
onClicked: {
applicationWindow().pageStack.pushDialogLayer("qrc:/DevtoolsPage.qml", {room: root.room}, {title: i18n("Developer Tools")})
}
}
Delegates.RoundedItemDelegate {
id: searchButton
icon.name: "search"
text: i18n("Search in this room")
Layout.fillWidth: true
onClicked: {
pageStack.pushDialogLayer("qrc:/SearchPage.qml", {
currentRoom: root.room
}, {
title: i18nc("@action:title", "Search")
})
}
}
Delegates.RoundedItemDelegate {
id: favouriteButton
icon.name: root.room && root.room.isFavourite ? "rating" : "rating-unrated"
text: root.room && root.room.isFavourite ? i18n("Remove room from favorites") : i18n("Make room favorite")
onClicked: root.room.isFavourite ? root.room.removeTag("m.favourite") : root.room.addTag("m.favourite", 1.0)
Layout.fillWidth: true
}
Delegates.RoundedItemDelegate {
id: locationsButton
icon.name: "map-flat"
text: i18n("Show locations for this room")
onClicked: pageStack.pushDialogLayer("qrc:/LocationsPage.qml", {
room: root.room
}, {
title: i18nc("Locations on a map", "Locations")
})
Layout.fillWidth: true
}
Kirigami.ListSectionHeader {
label: i18n("Members")
activeFocusOnTab: false
spacing: 0
visible: !root.room.isDirectChat()
width: root.width
Layout.fillWidth: true
QQC2.ToolButton {
id: memberSearchToggle
checkable: true
icon.name: "search"
QQC2.ToolTip.text: i18n("Search user in room")
QQC2.ToolTip.visible: hovered
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
onToggled: {
userListSearchField.text = "";
Loader {
active: true
Layout.fillWidth: true
Layout.topMargin: Kirigami.Units.smallSpacing
sourceComponent: root.room.isDirectChat() ? directChatDrawerHeader : groupChatDrawerHeader
onItemChanged: if (item) {
userList.positionViewAtBeginning();
}
}
QQC2.ToolButton {
visible: root.room.canSendState("invite")
icon.name: "list-add-user"
onClicked: {
applicationWindow().pageStack.pushDialogLayer("qrc:/InviteUserPage.qml", {room: root.room}, {title: i18nc("@title", "Invite a User")})
}
QQC2.ToolTip.text: i18n("Invite user to room")
QQC2.ToolTip.visible: hovered
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
}
QQC2.Label {
Layout.alignment: Qt.AlignRight
text: root.room ? i18np("%1 member", "%1 members", root.room.joinedCount) : i18n("No member count")
}
}
Kirigami.SearchField {
id: userListSearchField
visible: memberSearchToggle.checked
onVisibleChanged: if (visible) forceActiveFocus()
Layout.fillWidth: true
Layout.leftMargin: Kirigami.Units.largeSpacing - 1
Layout.rightMargin: Kirigami.Units.largeSpacing - 1
Layout.bottomMargin: Kirigami.Units.smallSpacing
focusSequence: "Ctrl+Shift+F"
onAccepted: sortedMessageEventModel.filterString = text;
}
}
KSortFilterProxyModel {
id: sortedMessageEventModel
sourceModel: UserListModel {
room: root.room
}
sortRole: "powerLevel"
sortOrder: Qt.DescendingOrder
filterRole: "name"
filterCaseSensitivity: Qt.CaseInsensitive
}
model: root.room.isDirectChat() ? 0 : sortedMessageEventModel
clip: true
activeFocusOnTab: true
delegate: Delegates.RoundedItemDelegate {
id: userDelegate
required property string name
required property string userId
required property string avatar
required property int powerLevel
required property string powerLevelString
implicitHeight: Kirigami.Units.gridUnit * 2
text: name
onClicked: {
userDelegate.highlighted = true;
RoomManager.visitUser(room.getUser(userDelegate.userId).object, "mention")
}
contentItem: RowLayout {
KirigamiComponents.Avatar {
implicitWidth: height
sourceSize {
height: Kirigami.Units.gridUnit + Kirigami.Units.smallSpacing * 2.5
width: Kirigami.Units.gridUnit + Kirigami.Units.smallSpacing * 2.5
}
source: userDelegate.avatar
name: userDelegate.userId
Layout.fillHeight: true
}
QQC2.Label {
text: userDelegate.name
textFormat: Text.PlainText
elide: Text.ElideRight
Kirigami.ListSectionHeader {
label: i18n("Options")
activeFocusOnTab: false
Layout.fillWidth: true
}
QQC2.Label {
visible: userDelegate.powerLevel > 0
Delegates.RoundedItemDelegate {
id: devtoolsButton
text: userDelegate.powerLevelString
color: Kirigami.Theme.disabledTextColor
textFormat: Text.PlainText
icon.name: "tools"
text: i18n("Open developer tools")
visible: Config.developerTools
Layout.fillWidth: true
onClicked: {
applicationWindow().pageStack.pushDialogLayer("qrc:/DevtoolsPage.qml", {room: root.room}, {title: i18n("Developer Tools")})
}
}
Delegates.RoundedItemDelegate {
id: searchButton
icon.name: "search"
text: i18n("Search in this room")
Layout.fillWidth: true
onClicked: {
pageStack.pushDialogLayer("qrc:/SearchPage.qml", {
currentRoom: root.room
}, {
title: i18nc("@action:title", "Search")
})
}
}
Delegates.RoundedItemDelegate {
id: favouriteButton
icon.name: root.room && root.room.isFavourite ? "rating" : "rating-unrated"
text: root.room && root.room.isFavourite ? i18n("Remove room from favorites") : i18n("Make room favorite")
onClicked: root.room.isFavourite ? root.room.removeTag("m.favourite") : root.room.addTag("m.favourite", 1.0)
Layout.fillWidth: true
}
Delegates.RoundedItemDelegate {
id: locationsButton
icon.name: "map-flat"
text: i18n("Show locations for this room")
onClicked: pageStack.pushDialogLayer("qrc:/LocationsPage.qml", {
room: root.room
}, {
title: i18nc("Locations on a map", "Locations")
})
Layout.fillWidth: true
}
Kirigami.ListSectionHeader {
label: i18n("Members")
activeFocusOnTab: false
spacing: 0
visible: !root.room.isDirectChat()
Layout.fillWidth: true
QQC2.ToolButton {
id: memberSearchToggle
checkable: true
icon.name: "search"
QQC2.ToolTip.text: i18n("Search user in room")
QQC2.ToolTip.visible: hovered
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
onToggled: {
userListSearchField.text = "";
}
}
QQC2.ToolButton {
visible: root.room.canSendState("invite")
icon.name: "list-add-user"
onClicked: {
applicationWindow().pageStack.pushDialogLayer("qrc:/InviteUserPage.qml", {room: root.room}, {title: i18nc("@title", "Invite a User")})
}
QQC2.ToolTip.text: i18n("Invite user to room")
QQC2.ToolTip.visible: hovered
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
}
QQC2.Label {
Layout.alignment: Qt.AlignRight
text: root.room ? i18np("%1 member", "%1 members", root.room.joinedCount) : i18n("No member count")
}
}
Kirigami.SearchField {
id: userListSearchField
visible: memberSearchToggle.checked
onVisibleChanged: if (visible) forceActiveFocus()
Layout.fillWidth: true
Layout.leftMargin: Kirigami.Units.largeSpacing - 1
Layout.rightMargin: Kirigami.Units.largeSpacing - 1
Layout.bottomMargin: Kirigami.Units.smallSpacing
focusSequence: "Ctrl+Shift+F"
onAccepted: sortedMessageEventModel.filterString = text;
}
}
KSortFilterProxyModel {
id: sortedMessageEventModel
sourceModel: UserListModel {
room: root.room
}
sortRole: "powerLevel"
sortOrder: Qt.DescendingOrder
filterRole: "name"
filterCaseSensitivity: Qt.CaseInsensitive
}
model: root.room.isDirectChat() ? 0 : sortedMessageEventModel
clip: true
activeFocusOnTab: true
delegate: Delegates.RoundedItemDelegate {
id: userDelegate
required property string name
required property string userId
required property string avatar
required property int powerLevel
required property string powerLevelString
implicitHeight: Kirigami.Units.gridUnit * 2
text: name
onClicked: {
userDelegate.highlighted = true;
RoomManager.visitUser(room.getUser(userDelegate.userId).object, "mention")
}
contentItem: RowLayout {
KirigamiComponents.Avatar {
implicitWidth: height
sourceSize {
height: Kirigami.Units.gridUnit + Kirigami.Units.smallSpacing * 2.5
width: Kirigami.Units.gridUnit + Kirigami.Units.smallSpacing * 2.5
}
source: userDelegate.avatar
name: userDelegate.userId
Layout.fillHeight: true
}
QQC2.Label {
text: userDelegate.name
textFormat: Text.PlainText
elide: Text.ElideRight
Layout.fillWidth: true
}
QQC2.Label {
visible: userDelegate.powerLevel > 0
text: userDelegate.powerLevelString
color: Kirigami.Theme.disabledTextColor
textFormat: Text.PlainText
}
}
}
}
@@ -255,6 +261,6 @@ ListView {
if (root.headerItem) {
root.headerItem.userListSearchField.text = "";
}
root.currentIndex = -1
userList.currentIndex = -1
}
}

View File

@@ -0,0 +1,71 @@
// SPDX-FileCopyrightText: 2023 James Graham <james.h.graham@protonmail.com>
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
import QtQuick 2.15
import QtQuick.Controls 2.15 as QQC2
import QtQuick.Layouts 1.15
import Qt.labs.qmlmodels 1.0
import org.kde.kirigami 2.20 as Kirigami
import org.kde.neochat 1.0
/**
* @brief Component for visualising the loaded media items in the room.
*
* The component is a simple list of media delegates (videos or images) with the
* ability to open them in the mamimize component.
*
* @note This component is only the contents, it will need to be placed in either
* a drawer (desktop) or page (mobile) to be used.
*
* @sa RoomDrawer, RoomDrawerPage
*/
QQC2.ScrollView {
id: root
/**
* @brief The title that should be displayed for this component if available.
*/
readonly property string title: i18nc("@action:title", "Room Media")
/**
* @brief The current room that user is viewing.
*/
required property NeoChatRoom currentRoom
// HACK: Hide unnecessary horizontal scrollbar (https://bugreports.qt.io/browse/QTBUG-83890)
QQC2.ScrollBar.horizontal.policy: QQC2.ScrollBar.AlwaysOff
ListView {
// So that delegates can access current room properly.
readonly property NeoChatRoom currentRoom: root.currentRoom
clip: true
verticalLayoutDirection: ListView.BottomToTop
model: RoomManager.mediaMessageFilterModel
delegate: DelegateChooser {
role: "type"
DelegateChoice {
roleValue: 0//MediaMessageFilterModel.Image
delegate: ImageDelegate {
alwaysShowAuthor: true
alwaysMaxWidth: true
cardBackground: false
}
}
DelegateChoice {
roleValue: 1//MediaMessageFilterModel.Video
delegate: VideoDelegate {
alwaysShowAuthor: true
alwaysMaxWidth: true
cardBackground: false
}
}
}
}
}

View File

@@ -141,6 +141,7 @@
<file alias="DirectChatDrawerHeader.qml">qml/RoomDrawer/DirectChatDrawerHeader.qml</file>
<file alias="GroupChatDrawerHeader.qml">qml/RoomDrawer/GroupChatDrawerHeader.qml</file>
<file alias="RoomInformation.qml">qml/RoomDrawer/RoomInformation.qml</file>
<file alias="RoomMedia.qml">qml/RoomDrawer/RoomMedia.qml</file>
<file alias="ChooseRoomDialog.qml">qml/Page/ChooseRoomDialog.qml</file>
</qresource>
</RCC>

View File

@@ -5,6 +5,7 @@
#include "roommanager.h"
#include "controller.h"
#include "models/messageeventmodel.h"
#include "neochatconfig.h"
#include "neochatroom.h"
#include <KLocalizedString>
@@ -26,8 +27,15 @@ RoomManager::RoomManager(QObject *parent)
, m_currentRoom(nullptr)
, m_lastCurrentRoom(nullptr)
, m_config(KConfig(QStringLiteral("data"), KConfig::SimpleConfig, QStandardPaths::AppDataLocation))
, m_messageEventModel(new MessageEventModel(this))
, m_messageFilterModel(new MessageFilterModel(this, m_messageEventModel))
, m_mediaMessageFilterModel(new MediaMessageFilterModel(this, m_messageFilterModel))
{
m_lastRoomConfig = m_config.group(QStringLiteral("LastOpenRoom"));
connect(this, &RoomManager::currentRoomChanged, this, [this]() {
m_messageEventModel->setRoom(m_currentRoom);
});
}
RoomManager::~RoomManager()
@@ -45,6 +53,21 @@ NeoChatRoom *RoomManager::currentRoom() const
return m_currentRoom;
}
MessageEventModel *RoomManager::messageEventModel() const
{
return m_messageEventModel;
}
MessageFilterModel *RoomManager::messageFilterModel() const
{
return m_messageFilterModel;
}
MediaMessageFilterModel *RoomManager::mediaMessageFilterModel() const
{
return m_mediaMessageFilterModel;
}
void RoomManager::openResource(const QString &idOrUri, const QString &action)
{
Uri uri{idOrUri};
@@ -72,6 +95,14 @@ void RoomManager::openResource(const QString &idOrUri, const QString &action)
}
}
void RoomManager::maximizeMedia(int index)
{
if (index < -1 || index > m_mediaMessageFilterModel->rowCount()) {
return;
}
Q_EMIT showMaximizedMedia(index);
}
bool RoomManager::hasOpenRoom() const
{
return m_currentRoom != nullptr;
@@ -266,7 +297,6 @@ void RoomManager::leaveRoom(NeoChatRoom *room)
if (m_currentRoom && m_currentRoom->id() == room->id()) {
m_currentRoom = m_lastCurrentRoom;
m_lastCurrentRoom = nullptr;
Q_EMIT currentRoomChanged();
}

View File

@@ -9,6 +9,9 @@
#include <Quotient/uriresolver.h>
#include "chatdocumenthandler.h"
#include "models/mediamessagefiltermodel.h"
#include "models/messageeventmodel.h"
#include "models/messagefiltermodel.h"
class NeoChatRoom;
@@ -36,6 +39,34 @@ class RoomManager : public QObject, public UriResolverBase
*/
Q_PROPERTY(NeoChatRoom *currentRoom READ currentRoom NOTIFY currentRoomChanged)
/**
* @brief The MessageEventModel that should be used for room message visualisation.
*
* The room object the model uses to get the data will be updated by this class
* so there is no need to do this manually or replace the model when a room
* changes.
*
* @note Available here so that the room page and drawer both have access to the
* same model.
*/
Q_PROPERTY(MessageEventModel *messageEventModel READ messageEventModel CONSTANT)
/**
* @brief The MessageFilterModel that should be used for room message visualisation.
*
* @note Available here so that the room page and drawer both have access to the
* same model.
*/
Q_PROPERTY(MessageFilterModel *messageFilterModel READ messageFilterModel CONSTANT)
/**
* @brief The MediaMessageFilterModel that should be used for room media message visualisation.
*
* @note Available here so that the room page and drawer both have access to the
* same model.
*/
Q_PROPERTY(MediaMessageFilterModel *mediaMessageFilterModel READ mediaMessageFilterModel CONSTANT)
/**
* @brief Whether a room is currently open in NeoChat.
*
@@ -57,6 +88,10 @@ public:
NeoChatRoom *currentRoom() const;
MessageEventModel *messageEventModel() const;
MessageFilterModel *messageFilterModel() const;
MediaMessageFilterModel *mediaMessageFilterModel() const;
bool hasOpenRoom() const;
/**
@@ -151,6 +186,15 @@ public:
*/
Q_INVOKABLE void openResource(const QString &idOrUri, const QString &action = {});
/**
* @brief Show a media item maximized.
*
* @param index the index to open the maximize delegate model at. This is the
* index in the MediaMessageFilterModel owned by this RoomManager. A value
* of -1 opens a the default item.
*/
Q_INVOKABLE void maximizeMedia(int index);
/**
* @brief Call this when the current used connection is dropped.
*/
@@ -208,6 +252,15 @@ Q_SIGNALS:
*/
void showUserDetail(const Quotient::User *user);
/**
* @brief Request a media item is shown maximized.
*
* @param index the index to open the maximize delegate model at. This is the
* index in the MediaMessageFilterModel owned by this RoomManager. A value
* of -1 opens a the default item.
*/
void showMaximizedMedia(int index);
/**
* @brief Show the direct chat confirmation dialog.
*
@@ -232,4 +285,8 @@ private:
KConfig m_config;
KConfigGroup m_lastRoomConfig;
QPointer<ChatDocumentHandler> m_chatDocumentHandler;
MessageEventModel *m_messageEventModel;
MessageFilterModel *m_messageFilterModel;
MediaMessageFilterModel *m_mediaMessageFilterModel;
};