diff --git a/resources/qml/ChatPage.qml b/resources/qml/ChatPage.qml index 7f6758df..ae37187b 100644 --- a/resources/qml/ChatPage.qml +++ b/resources/qml/ChatPage.qml @@ -79,6 +79,7 @@ Rectangle { showBackButton: adaptiveView.singlePageMode room: Rooms.currentRoom + roomPreview: Rooms.currentRoomPreview.roomid ? Rooms.currentRoomPreview : null } } diff --git a/resources/qml/RoomList.qml b/resources/qml/RoomList.qml index c4a8bcfb..a92beb38 100644 --- a/resources/qml/RoomList.qml +++ b/resources/qml/RoomList.qml @@ -121,7 +121,7 @@ Page { states: [ State { name: "highlight" - when: hovered.hovered && !(Rooms.currentRoom && model.roomId == Rooms.currentRoom.roomId()) + when: hovered.hovered && !((Rooms.currentRoom && model.roomId == Rooms.currentRoom.roomId()) || Rooms.currentRoomPreview.roomid == model.roomId) PropertyChanges { target: roomItem @@ -135,7 +135,7 @@ Page { }, State { name: "selected" - when: Rooms.currentRoom && model.roomId == Rooms.currentRoom.roomId() + when: (Rooms.currentRoom && model.roomId == Rooms.currentRoom.roomId()) || Rooms.currentRoomPreview.roomid == model.roomId PropertyChanges { target: roomItem @@ -268,7 +268,7 @@ Page { RowLayout { Layout.fillWidth: true spacing: 0 - visible: !model.isInvite && !model.isSpace + visible: !model.isSpace height: visible ? 0 : undefined ElidedLabel { @@ -310,60 +310,6 @@ Page { } - RowLayout { - Layout.fillWidth: true - spacing: Nheko.paddingMedium - visible: model.isInvite - enabled: visible - height: visible ? 0 : undefined - - ElidedLabel { - elideWidth: textContent.width / 2 - 2 * Nheko.paddingMedium - fullText: qsTr("Accept") - horizontalAlignment: Text.AlignHCenter - verticalAlignment: Text.AlignVCenter - leftPadding: Nheko.paddingMedium - rightPadding: Nheko.paddingMedium - color: Nheko.colors.brightText - - TapHandler { - onSingleTapped: Rooms.acceptInvite(model.roomId) - } - - background: Rectangle { - color: Nheko.theme.alternateButton - radius: height / 2 - } - - } - - ElidedLabel { - Layout.alignment: Qt.AlignRight - elideWidth: textContent.width / 2 - 2 * Nheko.paddingMedium - fullText: qsTr("Decline") - horizontalAlignment: Text.AlignHCenter - verticalAlignment: Text.AlignVCenter - leftPadding: Nheko.paddingMedium - rightPadding: Nheko.paddingMedium - color: Nheko.colors.brightText - - TapHandler { - onSingleTapped: Rooms.declineInvite(model.roomId) - } - - background: Rectangle { - color: Nheko.theme.alternateButton - radius: height / 2 - } - - } - - Item { - Layout.fillWidth: true - } - - } - } } diff --git a/resources/qml/RoomSettings.qml b/resources/qml/RoomSettings.qml index d6e6c6a5..c852b837 100644 --- a/resources/qml/RoomSettings.qml +++ b/resources/qml/RoomSettings.qml @@ -117,9 +117,11 @@ ApplicationWindow { } ScrollView { - Layout.maximumHeight: 75 + Layout.fillHeight: true Layout.alignment: Qt.AlignHCenter - width: parent.width + Layout.fillWidth: true + Layout.leftMargin: Nheko.paddingLarge + Layout.rightMargin: Nheko.paddingLarge TextArea { text: TimelineManager.escapeEmoji(roomSettings.roomTopic) diff --git a/resources/qml/TimelineView.qml b/resources/qml/TimelineView.qml index 0209054d..46317b2c 100644 --- a/resources/qml/TimelineView.qml +++ b/resources/qml/TimelineView.qml @@ -2,6 +2,7 @@ // // SPDX-License-Identifier: GPL-3.0-or-later +import "./components" import "./delegates" import "./device-verification" import "./emoji" @@ -21,10 +22,11 @@ Item { id: timelineView property var room: null + property var roomPreview: null property bool showBackButton: false Label { - visible: !room && !TimelineManager.isInitialSync + visible: !room && !TimelineManager.isInitialSync && !roomPreview anchors.centerIn: parent text: qsTr("No room open") font.pointSize: 24 @@ -132,15 +134,25 @@ Item { } ColumnLayout { - visible: room != null && room.isSpace + id: preview + + property string roomName: room ? room.roomName : (roomPreview ? roomPreview.roomName : "") + property string roomTopic: room ? room.roomTopic : (roomPreview ? roomPreview.roomTopic : "") + property string avatarUrl: room ? room.roomAvatarUrl : (roomPreview ? roomPreview.roomAvatarUrl : "") + + visible: room != null && room.isSpace || roomPreview != null enabled: visible anchors.fill: parent anchors.margins: Nheko.paddingLarge spacing: Nheko.paddingLarge + Item { + Layout.fillHeight: true + } + Avatar { - url: room ? room.roomAvatarUrl.replace("mxc://", "image://MxcImage/") : "" - displayName: room ? room.roomName : "" + url: parent.avatarUrl.replace("mxc://", "image://MxcImage/") + displayName: parent.roomName height: 130 width: 130 Layout.alignment: Qt.AlignHCenter @@ -148,22 +160,25 @@ Item { } MatrixText { - text: room ? room.roomName : "" + text: parent.roomName font.pixelSize: 24 Layout.alignment: Qt.AlignHCenter } MatrixText { + visible: !!room text: qsTr("%1 member(s)").arg(room ? room.roomMemberCount : 0) Layout.alignment: Qt.AlignHCenter } ScrollView { Layout.alignment: Qt.AlignHCenter - width: timelineView.width - Nheko.paddingLarge * 2 + Layout.fillWidth: true + Layout.leftMargin: Nheko.paddingLarge + Layout.rightMargin: Nheko.paddingLarge TextArea { - text: TimelineManager.escapeEmoji(room ? room.roomTopic : "") + text: TimelineManager.escapeEmoji(preview.roomTopic) wrapMode: TextEdit.WordWrap textFormat: TextEdit.RichText readOnly: true @@ -182,6 +197,32 @@ Item { } + FlatButton { + visible: roomPreview && !roomPreview.isInvite + Layout.alignment: Qt.AlignHCenter + text: qsTr("join the conversation") + onClicked: Rooms.joinPreview(roomPreview.roomid) + } + + FlatButton { + visible: roomPreview && roomPreview.isInvite + Layout.alignment: Qt.AlignHCenter + text: qsTr("accept invite") + onClicked: Rooms.acceptInvite(roomPreview.roomid) + } + + FlatButton { + visible: roomPreview && roomPreview.isInvite + Layout.alignment: Qt.AlignHCenter + text: qsTr("decline invite") + onClicked: Rooms.declineInvite(roomPreview.roomid) + } + + Item { + visible: room != null + Layout.preferredHeight: Math.ceil(fontMetrics.lineSpacing * 2) + } + Item { Layout.fillHeight: true } diff --git a/resources/qml/TopBar.qml b/resources/qml/TopBar.qml index 30ab2e7c..82373023 100644 --- a/resources/qml/TopBar.qml +++ b/resources/qml/TopBar.qml @@ -12,6 +12,9 @@ Rectangle { id: topBar property bool showBackButton: false + property string roomName: room ? room.roomName : qsTr("No room selected") + property string avatarUrl: room ? room.roomAvatarUrl : "" + property string roomTopic: room ? room.roomTopic : "" Layout.fillWidth: true implicitHeight: topLayout.height + Nheko.paddingMedium * 2 @@ -20,15 +23,15 @@ Rectangle { TapHandler { onSingleTapped: { - room.openRoomSettings(); + if (room) + room.openRoomSettings(); + eventPoint.accepted = true; } gesturePolicy: TapHandler.ReleaseWithinBounds } GridLayout { - //Layout.margins: 8 - id: topLayout anchors.left: parent.left @@ -59,9 +62,13 @@ Rectangle { Layout.alignment: Qt.AlignVCenter width: Nheko.avatarSize height: Nheko.avatarSize - url: room ? room.roomAvatarUrl.replace("mxc://", "image://MxcImage/") : "" - displayName: room ? room.roomName : qsTr("No room selected") - onClicked: room.openRoomSettings() + url: avatarUrl.replace("mxc://", "image://MxcImage/") + displayName: roomName + onClicked: { + if (room) { + room.openRoomSettings(); + } + } } Label { @@ -70,7 +77,7 @@ Rectangle { Layout.row: 0 color: Nheko.colors.text font.pointSize: fontMetrics.font.pointSize * 1.1 - text: room ? room.roomName : qsTr("No room selected") + text: roomName maximumLineCount: 1 elide: Text.ElideRight textFormat: Text.RichText @@ -82,12 +89,13 @@ Rectangle { Layout.row: 1 Layout.maximumHeight: fontMetrics.lineSpacing * 2 // show 2 lines clip: true - text: room ? room.roomTopic : "" + text: roomTopic } ImageButton { id: roomOptionsButton + visible: !!room Layout.column: 3 Layout.row: 0 Layout.rowSpan: 2 diff --git a/resources/qml/components/FlatButton.qml b/resources/qml/components/FlatButton.qml new file mode 100644 index 00000000..77d97976 --- /dev/null +++ b/resources/qml/components/FlatButton.qml @@ -0,0 +1,47 @@ +// SPDX-FileCopyrightText: 2021 Nheko Contributors +// +// SPDX-License-Identifier: GPL-3.0-or-later + +import QtGraphicalEffects 1.12 +import QtQuick 2.9 +import QtQuick.Controls 2.5 +import im.nheko 1.0 + +Button { + id: control + + implicitHeight: Math.ceil(control.contentItem.implicitHeight * 1.5) + implicitWidth: Math.ceil(control.contentItem.implicitWidth + control.contentItem.implicitHeight) + hoverEnabled: true + + DropShadow { + anchors.fill: control.background + horizontalOffset: 3 + verticalOffset: 3 + radius: 8 + samples: 17 + cached: true + color: "#80000000" + source: control.background + } + + contentItem: Text { + text: control.text + //font: control.font + font.capitalization: Font.AllUppercase + font.pointSize: Math.ceil(fontMetrics.font.pointSize * 1.5) + //font.capitalization: Font.AllUppercase + color: Nheko.colors.light + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + elide: Text.ElideRight + } + + background: Rectangle { + //height: control.contentItem.implicitHeight * 2 + //width: control.contentItem.implicitWidth * 2 + radius: height / 6 + color: Qt.lighter(Nheko.colors.dark, control.down ? 1.4 : (control.hovered ? 1.2 : 1)) + } + +} diff --git a/resources/res.qrc b/resources/res.qrc index 9bb8ae2e..f41835f9 100644 --- a/resources/res.qrc +++ b/resources/res.qrc @@ -183,6 +183,7 @@ qml/voip/VideoCall.qml qml/components/AdaptiveLayout.qml qml/components/AdaptiveLayoutElement.qml + qml/components/FlatButton.qml media/ring.ogg diff --git a/src/timeline/RoomlistModel.cpp b/src/timeline/RoomlistModel.cpp index 87940948..e2901260 100644 --- a/src/timeline/RoomlistModel.cpp +++ b/src/timeline/RoomlistModel.cpp @@ -16,6 +16,8 @@ RoomlistModel::RoomlistModel(TimelineViewManager *parent) : QAbstractListModel(parent) , manager(parent) { + [[maybe_unused]] static auto id = qRegisterMetaType(); + connect(ChatPage::instance(), &ChatPage::decryptSidebarChanged, this, [this]() { auto decrypt = ChatPage::instance()->userSettings()->decryptSidebar(); QHash>::iterator i; @@ -137,7 +139,7 @@ RoomlistModel::data(const QModelIndex &index, int role) const case Roles::RoomName: return QString::fromStdString(room.name); case Roles::LastMessage: - return QString(); + return tr("Pending invite."); case Roles::Time: return QString(); case Roles::Timestamp: @@ -434,9 +436,11 @@ void RoomlistModel::sync(const mtx::responses::Rooms &rooms) { for (const auto &[room_id, room] : rooms.join) { + auto qroomid = QString::fromStdString(room_id); + // addRoom will only add the room, if it doesn't exist - addRoom(QString::fromStdString(room_id)); - const auto &room_model = models.value(QString::fromStdString(room_id)); + addRoom(qroomid); + const auto &room_model = models.value(qroomid); room_model->sync(room); // room_model->addEvents(room.timeline); connect(room_model.data(), @@ -465,14 +469,20 @@ RoomlistModel::sync(const mtx::responses::Rooms &rooms) for (const auto &[room_id, room] : rooms.leave) { (void)room; - auto idx = this->roomidToIndex(QString::fromStdString(room_id)); + auto qroomid = QString::fromStdString(room_id); + + if ((currentRoom_ && currentRoom_->roomId() == qroomid) || + (currentRoomPreview_ && currentRoomPreview_->roomid() == qroomid)) + resetCurrentRoom(); + + auto idx = this->roomidToIndex(qroomid); if (idx != -1) { beginRemoveRows(QModelIndex(), idx, idx); roomids.erase(roomids.begin() + idx); - if (models.contains(QString::fromStdString(room_id))) - models.remove(QString::fromStdString(room_id)); - else if (invites.contains(QString::fromStdString(room_id))) - invites.remove(QString::fromStdString(room_id)); + if (models.contains(qroomid)) + models.remove(qroomid); + else if (invites.contains(qroomid)) + invites.remove(qroomid); endRemoveRows(); } } @@ -529,6 +539,19 @@ RoomlistModel::clear() endResetModel(); } +void +RoomlistModel::joinPreview(QString roomid, QString parentSpace) +{ + if (previewedRooms.contains(roomid)) { + auto child = cache::client()->getStateEvent( + parentSpace.toStdString(), roomid.toStdString()); + ChatPage::instance()->joinRoomVia(roomid.toStdString(), + (child && child->content.via) + ? child->content.via.value() + : std::vector{}, + false); + } +} void RoomlistModel::acceptInvite(QString roomid) { @@ -581,6 +604,31 @@ RoomlistModel::setCurrentRoom(QString roomid) nhlog::ui()->debug("Trying to switch to: {}", roomid.toStdString()); if (models.contains(roomid)) { currentRoom_ = models.value(roomid); + currentRoomPreview_.reset(); + emit currentRoomChanged(); + nhlog::ui()->debug("Switched to: {}", roomid.toStdString()); + } else if (invites.contains(roomid) || previewedRooms.contains(roomid)) { + currentRoom_ = nullptr; + std::optional i; + + RoomPreview p; + + if (invites.contains(roomid)) { + i = invites.value(roomid); + p.isInvite_ = true; + } else { + i = previewedRooms.value(roomid); + p.isInvite_ = false; + } + + if (i) { + p.roomid_ = roomid; + p.roomName_ = QString::fromStdString(i->name); + p.roomTopic_ = QString::fromStdString(i->topic); + p.roomAvatarUrl_ = QString::fromStdString(i->avatar_url); + currentRoomPreview_ = std::move(p); + } + emit currentRoomChanged(); nhlog::ui()->debug("Switched to: {}", roomid.toStdString()); } diff --git a/src/timeline/RoomlistModel.h b/src/timeline/RoomlistModel.h index 2005c35e..6ac6da18 100644 --- a/src/timeline/RoomlistModel.h +++ b/src/timeline/RoomlistModel.h @@ -18,11 +18,35 @@ class TimelineViewManager; +class RoomPreview +{ + Q_GADGET + Q_PROPERTY(QString roomid READ roomid CONSTANT) + Q_PROPERTY(QString roomName READ roomName CONSTANT) + Q_PROPERTY(QString roomTopic READ roomTopic CONSTANT) + Q_PROPERTY(QString roomAvatarUrl READ roomAvatarUrl CONSTANT) + Q_PROPERTY(bool isInvite READ isInvite CONSTANT) + +public: + RoomPreview() {} + + QString roomid() const { return roomid_; } + QString roomName() const { return roomName_; } + QString roomTopic() const { return roomTopic_; } + QString roomAvatarUrl() const { return roomAvatarUrl_; } + bool isInvite() const { return isInvite_; } + + QString roomid_, roomName_, roomAvatarUrl_, roomTopic_; + bool isInvite_ = false; +}; + class RoomlistModel : public QAbstractListModel { Q_OBJECT Q_PROPERTY(TimelineModel *currentRoom READ currentRoom NOTIFY currentRoomChanged RESET resetCurrentRoom) + Q_PROPERTY(RoomPreview currentRoomPreview READ currentRoomPreview NOTIFY currentRoomChanged + RESET resetCurrentRoom) public: enum Roles { @@ -72,14 +96,20 @@ public slots: return -1; } + void joinPreview(QString roomid, QString parentSpace); void acceptInvite(QString roomid); void declineInvite(QString roomid); void leave(QString roomid); TimelineModel *currentRoom() const { return currentRoom_.get(); } + RoomPreview currentRoomPreview() const + { + return currentRoomPreview_.value_or(RoomPreview{}); + } void setCurrentRoom(QString roomid); void resetCurrentRoom() { currentRoom_ = nullptr; + currentRoomPreview_.reset(); emit currentRoomChanged(); } @@ -103,6 +133,7 @@ private: QHash> previewedRooms; QSharedPointer currentRoom_; + std::optional currentRoomPreview_; friend class FilteredRoomlistModel; }; @@ -112,6 +143,8 @@ class FilteredRoomlistModel : public QSortFilterProxyModel Q_OBJECT Q_PROPERTY(TimelineModel *currentRoom READ currentRoom NOTIFY currentRoomChanged RESET resetCurrentRoom) + Q_PROPERTY(RoomPreview currentRoomPreview READ currentRoomPreview NOTIFY currentRoomChanged + RESET resetCurrentRoom) public: FilteredRoomlistModel(RoomlistModel *model, QObject *parent = nullptr); bool lessThan(const QModelIndex &left, const QModelIndex &right) const override; @@ -123,12 +156,17 @@ public slots: return mapFromSource(roomlistmodel->index(roomlistmodel->roomidToIndex(roomid))) .row(); } + void joinPreview(QString roomid) + { + roomlistmodel->joinPreview(roomid, filterType == FilterBy::Space ? filterStr : ""); + } void acceptInvite(QString roomid) { roomlistmodel->acceptInvite(roomid); } void declineInvite(QString roomid) { roomlistmodel->declineInvite(roomid); } void leave(QString roomid) { roomlistmodel->leave(roomid); } void toggleTag(QString roomid, QString tag, bool on); TimelineModel *currentRoom() const { return roomlistmodel->currentRoom(); } + RoomPreview currentRoomPreview() const { return roomlistmodel->currentRoomPreview(); } void setCurrentRoom(QString roomid) { roomlistmodel->setCurrentRoom(std::move(roomid)); } void resetCurrentRoom() { roomlistmodel->resetCurrentRoom(); }