diff --git a/.appveyor.yml b/.appveyor.yml index f7e237e..de56e05 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -3,7 +3,7 @@ image: Visual Studio 2017 environment: DEPLOY_DIR: Spectral-%APPVEYOR_BUILD_VERSION% matrix: - - QTDIR: C:\Qt\5.12.1\msvc2017_64 + - QTDIR: C:\Qt\5.12\msvc2017_64 VCVARS: "C:\\Program Files (x86)\\Microsoft Visual Studio\\2017\\Community\\VC\\Auxiliary\\Build\\vcvars64.bat" PLATFORM: diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 9ef3ac9..c2bcee1 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -3,30 +3,42 @@ stages: - deploy build-flatpak: - image: black0/flatpak + image: registry.gitlab.com/b0/flatpak-kde-docker stage: build before_script: - git submodule update --init --recursive script: - cd flatpak - - flatpak-builder --force-clean --repo=repo build-dir org.eu.encom.spectral.yaml + - flatpak-builder --force-clean --ccache --repo=repo build-dir org.eu.encom.spectral.yaml - flatpak build-bundle repo spectral.flatpak org.eu.encom.spectral - cd ../ + cache: + key: "flatpak-$CI_COMMIT_REF_SLUG" + paths: + - flatpak/.flatpak-builder artifacts: paths: - flatpak/spectral.flatpak build-appimage: - image: black0/qt + image: registry.gitlab.com/b0/qt-docker stage: build before_script: - git submodule update --init --recursive script: + - mkdir -p ccache + - export CCACHE_BASEDIR=${CI_PROJECT_DIR} + - export CCACHE_DIR=${CI_PROJECT_DIR}/ccache - /opt/qt512/bin/qt512-env.sh - - /opt/qt512/bin/qmake CONFIG+=debug CONFIG+=qml_debug PREFIX=/usr + - /opt/qt512/bin/qmake CONFIG+=debug CONFIG+=qml_debug CONFIG+=ccache PREFIX=/usr - make - make INSTALL_ROOT=appdir install - /usr/bin/linuxdeployqt-continuous-x86_64.AppImage appdir/usr/share/applications/org.eu.encom.spectral.desktop -appimage -qmldir=qml -qmldir=imports -qmake=/opt/qt512/bin/qmake + cache: + key: "appimage-$CI_COMMIT_REF_SLUG" + paths: + - ccache/ + artifacts: paths: - Spectral*.AppImage diff --git a/README.md b/README.md index dabae33..c83270e 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ Spectral is a glossy cross-platform client for Matrix, the decentralized communi There is a separate document for Spectral, including installing, compiling, etc. -It is at [Spectral Doc](https://doc.spectral.encom.eu.org/) +It is at [Spectral Doc](https://b0.gitlab.io/spectral-doc/) ## Contact @@ -31,6 +31,10 @@ This program uses libqmatrixclient library and some C++ models from Quaternion. [libqmatrixclient](https://github.com/QMatrixClient/libqmatrixclient) +This program includes the source code of hoedown. + +[Hoedown](https://github.com/hoedown/hoedown) + ## Donation Donations are welcome! My Bitcoin wallet address is 1AmNvttxJ6zne8f2GEH8zMAMQuT4cMdnDN @@ -41,4 +45,4 @@ Donations are welcome! My Bitcoin wallet address is 1AmNvttxJ6zne8f2GEH8zMAMQuT4 This program is licensed under GNU General Public License, Version 3. -Exceptions are src/notifications/wintoastlib.c and wintoastlib.h, copied from https://github.com/mohabouje/WinToast and licensed under MIT. \ No newline at end of file +Exceptions are src/notifications/wintoastlib.c and wintoastlib.h, which are from https://github.com/mohabouje/WinToast and licensed under MIT. diff --git a/assets/font/roboto.ttf b/assets/font/roboto.ttf deleted file mode 100644 index 2c97eea..0000000 Binary files a/assets/font/roboto.ttf and /dev/null differ diff --git a/assets/font/twemoji.ttf b/assets/font/twemoji.ttf deleted file mode 100644 index b8a19d0..0000000 Binary files a/assets/font/twemoji.ttf and /dev/null differ diff --git a/assets/img/roompanel-dark.svg b/assets/img/roompanel-dark.svg deleted file mode 100644 index d240920..0000000 --- a/assets/img/roompanel-dark.svg +++ /dev/null @@ -1,219 +0,0 @@ - - - - - - - - - - image/svg+xml - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/assets/img/roompanel.svg b/assets/img/roompanel.svg deleted file mode 100644 index d01c669..0000000 --- a/assets/img/roompanel.svg +++ /dev/null @@ -1,219 +0,0 @@ - - - - - - - - - - image/svg+xml - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/flatpak/org.eu.encom.spectral.yaml b/flatpak/org.eu.encom.spectral.yaml index 644b4b5..0efaac7 100644 --- a/flatpak/org.eu.encom.spectral.yaml +++ b/flatpak/org.eu.encom.spectral.yaml @@ -11,12 +11,9 @@ finish-args: - --device=dri - --filesystem=xdg-download - --talk-name=org.freedesktop.Notifications -- --talk-name=org.kde.StatusNotifierWatcher modules: - name: spectral buildsystem: qmake - config-opts: - - "BUNDLE_FONT=true" sources: - type: dir path: ../ diff --git a/font.qrc b/font.qrc deleted file mode 100644 index cfda4ec..0000000 --- a/font.qrc +++ /dev/null @@ -1,6 +0,0 @@ - - - assets/font/roboto.ttf - assets/font/twemoji.ttf - - diff --git a/imports/Spectral/Component/AutoMouseArea.qml b/imports/Spectral/Component/AutoMouseArea.qml index 124625b..ac5e63e 100644 --- a/imports/Spectral/Component/AutoMouseArea.qml +++ b/imports/Spectral/Component/AutoMouseArea.qml @@ -6,8 +6,8 @@ MouseArea { signal primaryClicked() signal secondaryClicked() - acceptedButtons: MSettings.pressAndHold ? Qt.LeftButton : (Qt.LeftButton | Qt.RightButton) + acceptedButtons: Qt.LeftButton | Qt.RightButton onClicked: mouse.button == Qt.RightButton ? secondaryClicked() : primaryClicked() - onPressAndHold: MSettings.pressAndHold ? secondaryClicked() : {} + onPressAndHold: secondaryClicked() } diff --git a/imports/Spectral/Component/AutoTextField.qml b/imports/Spectral/Component/AutoTextField.qml index 274b236..1925826 100644 --- a/imports/Spectral/Component/AutoTextField.qml +++ b/imports/Spectral/Component/AutoTextField.qml @@ -1,6 +1,62 @@ import QtQuick 2.12 import QtQuick.Controls 2.12 +import QtQuick.Controls.Material 2.3 + TextField { + id: textField + selectByMouse: true + + topPadding: 8 + bottomPadding: 8 + + background: Item { + Label { + id: floatingPlaceholder + + anchors.top: parent.top + anchors.left: parent.left + anchors.topMargin: textField.topPadding + anchors.leftMargin: textField.leftPadding + transformOrigin: Item.TopLeft + visible: false + color: Material.accent + + states: [ + State { + name: "shown" + when: textField.text.length !== 0 + PropertyChanges { target: floatingPlaceholder; scale: 0.8 } + PropertyChanges { target: floatingPlaceholder; anchors.topMargin: -floatingPlaceholder.height * 0.4 } + } + ] + + transitions: [ + Transition { + to: "shown" + SequentialAnimation { + PropertyAction { target: floatingPlaceholder; property: "text"; value: textField.placeholderText } + PropertyAction { target: floatingPlaceholder; property: "visible"; value: true } + PropertyAction { target: textField; property: "placeholderTextColor"; value: "transparent" } + ParallelAnimation { + NumberAnimation { target: floatingPlaceholder; property: "scale"; duration: 250; easing.type: Easing.InOutQuad } + NumberAnimation { target: floatingPlaceholder; property: "anchors.topMargin"; duration: 250; easing.type: Easing.InOutQuad } + } + } + }, + Transition { + from: "shown" + SequentialAnimation { + ParallelAnimation { + NumberAnimation { target: floatingPlaceholder; property: "scale"; duration: 250; easing.type: Easing.InOutQuad } + NumberAnimation { target: floatingPlaceholder; property: "anchors.topMargin"; duration: 250; easing.type: Easing.InOutQuad } + } + PropertyAction { target: textField; property: "placeholderTextColor"; value: "grey" } + PropertyAction { target: floatingPlaceholder; property: "visible"; value: false } + } + } + ] + } + } } diff --git a/imports/Spectral/Component/Avatar.qml b/imports/Spectral/Component/Avatar.qml index 399f465..fe546eb 100644 --- a/imports/Spectral/Component/Avatar.qml +++ b/imports/Spectral/Component/Avatar.qml @@ -36,8 +36,8 @@ Item { visible: !realSource || image.status != Image.Ready radius: height / 2 - color: stringToColor(hint) + antialiasing: true Label { anchors.centerIn: parent diff --git a/imports/Spectral/Component/FullScreenImage.qml b/imports/Spectral/Component/FullScreenImage.qml new file mode 100644 index 0000000..0fb9a31 --- /dev/null +++ b/imports/Spectral/Component/FullScreenImage.qml @@ -0,0 +1,49 @@ +import QtQuick 2.12 +import QtQuick.Controls 2.12 + +ApplicationWindow { + property string eventId + property url localPath + + id: root + + flags: Qt.FramelessWindowHint | Qt.WA_TranslucentBackground + visible: true + visibility: Qt.WindowFullScreen + + title: "Image View - " + eventId + + color: "#BB000000" + + Shortcut { + sequence: "Escape" + onActivated: root.destroy() + } + + AnimatedImage { + anchors.centerIn: parent + + width: Math.min(sourceSize.width, root.width) + height: Math.min(sourceSize.height, root.height) + + fillMode: Image.PreserveAspectFit + cache: false + + source: localPath + } + + ItemDelegate { + anchors.top: parent.top + anchors.right: parent.right + + width: 64 + height: 64 + + contentItem: MaterialIcon { + icon: "\ue5cd" + color: "white" + } + + onClicked: root.destroy() + } +} diff --git a/imports/Spectral/Component/SplitView.qml b/imports/Spectral/Component/SplitView.qml deleted file mode 100644 index a29e14e..0000000 --- a/imports/Spectral/Component/SplitView.qml +++ /dev/null @@ -1,535 +0,0 @@ -/**************************************************************************** -** -** Copyright (C) 2016 The Qt Company Ltd. -** Contact: https://www.qt.io/licensing/ -** -** This file is part of the Qt Quick Controls module of the Qt Toolkit. -** -** $QT_BEGIN_LICENSE:LGPL$ -** Commercial License Usage -** Licensees holding valid commercial Qt licenses may use this file in -** accordance with the commercial license agreement provided with the -** Software or, alternatively, in accordance with the terms contained in -** a written agreement between you and The Qt Company. For licensing terms -** and conditions see https://www.qt.io/terms-conditions. For further -** information use the contact form at https://www.qt.io/contact-us. -** -** GNU Lesser General Public License Usage -** Alternatively, this file may be used under the terms of the GNU Lesser -** General Public License version 3 as published by the Free Software -** Foundation and appearing in the file LICENSE.LGPL3 included in the -** packaging of this file. Please review the following information to -** ensure the GNU Lesser General Public License version 3 requirements -** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. -** -** GNU General Public License Usage -** Alternatively, this file may be used under the terms of the GNU -** General Public License version 2.0 or (at your option) the GNU General -** Public license version 3 or any later version approved by the KDE Free -** Qt Foundation. The licenses are as published by the Free Software -** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 -** included in the packaging of this file. Please review the following -** information to ensure the GNU General Public License requirements will -** be met: https://www.gnu.org/licenses/gpl-2.0.html and -** https://www.gnu.org/licenses/gpl-3.0.html. -** -** $QT_END_LICENSE$ -** -****************************************************************************/ - -import QtQuick 2.12 -import QtQuick.Controls 2.12 -import QtQuick.Layouts 1.12 -import QtQuick.Window 2.1 -import Spectral.Setting 0.1 - -Item { - id: root - - property int orientation: Qt.Horizontal - - /*! - This property holds the delegate that will be instantiated between each - child item. Inside the delegate the following properties are available: - - \table - \row \li readonly property bool styleData.index \li Specifies the index of the splitter handle. The handle - between the first and the second item will get index 0, - the next handle index 1 etc. - \row \li readonly property bool styleData.hovered \li The handle is being hovered. - \row \li readonly property bool styleData.pressed \li The handle is being pressed. - \row \li readonly property bool styleData.resizing \li The handle is being dragged. - \endtable - -*/ - property Component handleDelegate: Rectangle { - width: 1 - height: 1 - color: MSettings.darkTheme ? "#424242" : "#E1E1E1" - } - - /*! - This propery is \c true when the user is resizing any of the items by - dragging on the splitter handles. - */ - property bool resizing: false - - /*! \internal */ - default property alias __contents: contents.data - /*! \internal */ - property alias __items: splitterItems.children - /*! \internal */ - property alias __handles: splitterHandles.children - - clip: true - Component.onCompleted: d.init() - onWidthChanged: d.updateLayout() - onHeightChanged: d.updateLayout() - onOrientationChanged: d.changeOrientation() - - /*! \qmlmethod void SplitView::addItem(Item item) - Add an item to the end of the view. - \since QtQuick.Controls 1.12 */ - function addItem(item) { - d.updateLayoutGuard = true - d.addItem_impl(item) - d.calculateImplicitSize() - d.updateLayoutGuard = false - d.updateFillIndex() - } - - /*! \qmlmethod void SplitView::removeItem(Item item) - Remove \a item from the view. - \since QtQuick.Controls 1.4 */ - function removeItem(item) { - d.updateLayoutGuard = true - var result = d.removeItem_impl(item) - if (result !== null) { - d.calculateImplicitSize() - d.updateLayoutGuard = false - d.updateFillIndex() - } - else { - d.updateLayoutGuard = false - } - } - - SystemPalette { id: pal } - - QtObject { - id: d - - readonly property string leftMargin: horizontal ? "leftMargin" : "topMargin" - readonly property string topMargin: horizontal ? "topMargin" : "leftMargin" - readonly property string rightMargin: horizontal ? "rightMargin" : "bottomMargin" - - property bool horizontal: orientation == Qt.Horizontal - readonly property string minimum: horizontal ? "minimumWidth" : "minimumHeight" - readonly property string maximum: horizontal ? "maximumWidth" : "maximumHeight" - readonly property string otherMinimum: horizontal ? "minimumHeight" : "minimumWidth" - readonly property string otherMaximum: horizontal ? "maximumHeight" : "maximumWidth" - readonly property string offset: horizontal ? "x" : "y" - readonly property string otherOffset: horizontal ? "y" : "x" - readonly property string size: horizontal ? "width" : "height" - readonly property string otherSize: horizontal ? "height" : "width" - readonly property string implicitSize: horizontal ? "implicitWidth" : "implicitHeight" - readonly property string implicitOtherSize: horizontal ? "implicitHeight" : "implicitWidth" - - property int fillIndex: -1 - property bool updateLayoutGuard: true - - function extraMarginSize(item, other) { - if (typeof(other) === 'undefined') - other = false; - if (other === horizontal) - // vertical - return item.Layout.topMargin + item.Layout.bottomMargin - return item.Layout.leftMargin + item.Layout.rightMargin - } - - function addItem_impl(item) - { - // temporarily set fillIndex to new item - fillIndex = __items.length - if (splitterItems.children.length > 0) - handleLoader.createObject(splitterHandles, {"__handleIndex":splitterItems.children.length - 1}) - - item.parent = splitterItems - d.initItemConnections(item) - } - - function initItemConnections(item) - { - // should match disconnections in terminateItemConnections - item.widthChanged.connect(d.updateLayout) - item.heightChanged.connect(d.updateLayout) - item.Layout.maximumWidthChanged.connect(d.updateLayout) - item.Layout.minimumWidthChanged.connect(d.updateLayout) - item.Layout.maximumHeightChanged.connect(d.updateLayout) - item.Layout.minimumHeightChanged.connect(d.updateLayout) - item.Layout.leftMarginChanged.connect(d.updateLayout) - item.Layout.topMarginChanged.connect(d.updateLayout) - item.Layout.rightMarginChanged.connect(d.updateLayout) - item.Layout.bottomMarginChanged.connect(d.updateLayout) - item.visibleChanged.connect(d.updateFillIndex) - item.Layout.fillWidthChanged.connect(d.updateFillIndex) - item.Layout.fillHeightChanged.connect(d.updateFillIndex) - } - - function terminateItemConnections(item) - { - // should match connections in initItemConnections - item.widthChanged.disconnect(d.updateLayout) - item.heightChanged.disconnect(d.updateLayout) - item.Layout.maximumWidthChanged.disconnect(d.updateLayout) - item.Layout.minimumWidthChanged.disconnect(d.updateLayout) - item.Layout.maximumHeightChanged.disconnect(d.updateLayout) - item.Layout.minimumHeightChanged.disconnect(d.updateLayout) - item.visibleChanged.disconnect(d.updateFillIndex) - item.Layout.fillWidthChanged.disconnect(d.updateFillIndex) - item.Layout.fillHeightChanged.disconnect(d.updateFillIndex) - } - - function removeItem_impl(item) - { - var pos = itemPos(item) - - // Check pos range - if (pos < 0 || pos >= __items.length) - return null - - // Temporary unset the fillIndex - fillIndex = __items.length - 1 - - // Remove the handle at the left/right of the item that - // is going to be removed - var handlePos = -1 - var hasPrevious = pos > 0 - var hasNext = (pos + 1) < __items.length - - if (hasPrevious) - handlePos = pos-1 - else if (hasNext) - handlePos = pos - if (handlePos >= 0) { - var handle = __handles[handlePos] - handle.visible = false - handle.parent = null - handle.destroy() - for (var i = handlePos; i < __handles.length; ++i) - __handles[i].__handleIndex = i - } - - // Remove the item. - // Disconnect the item to be removed - terminateItemConnections(item) - item.parent = null - - return item - } - - function itemPos(item) - { - for (var i = 0; i < __items.length; ++i) - if (item === __items[i]) - return i - return -1 - } - - function init() - { - for (var i=0; i<__contents.length; ++i) { - var item = __contents[i]; - if (!item.hasOwnProperty("x")) - continue - addItem_impl(item) - i-- // item was removed from list - } - - d.calculateImplicitSize() - d.updateLayoutGuard = false - d.updateFillIndex() - } - - function updateFillIndex() - { - if (lastItem.visible !== root.visible) - return - var policy = (root.orientation === Qt.Horizontal) ? "fillWidth" : "fillHeight" - for (var i=0; i<__items.length-1; ++i) { - if (__items[i].Layout[policy] === true) - break; - } - - d.fillIndex = i - d.updateLayout() - } - - function changeOrientation() - { - if (__items.length == 0) - return; - d.updateLayoutGuard = true - - // Swap width/height for items and handles: - for (var i=0; i<__items.length; ++i) { - var item = __items[i] - var tmp = item.x - item.x = item.y - item.y = tmp - tmp = item.width - item.width = item.height - item.height = tmp - - var handle = __handles[i] - if (handle) { - tmp = handle.x - handle.x = handle.y - handle.y = handle.x - tmp = handle.width - handle.width = handle.height - handle.height = tmp - } - } - - // Change d.horizontal explicit, since the binding will change too late: - d.horizontal = orientation == Qt.Horizontal - d.updateLayoutGuard = false - d.updateFillIndex() - } - - function calculateImplicitSize() - { - var implicitSize = 0 - var implicitOtherSize = 0 - - for (var i=0; i<__items.length; ++i) { - var item = __items[i]; - implicitSize += clampedMinMax(item[d.size], item.Layout[minimum], item.Layout[maximum]) + extraMarginSize(item) - var os = clampedMinMax(item[otherSize], item.Layout[otherMinimum], item.Layout[otherMaximum]) + extraMarginSize(item, true) - implicitOtherSize = Math.max(implicitOtherSize, os) - - var handle = __handles[i] - if (handle) - implicitSize += handle[d.size] //### Can handles have margins?? - } - - root[d.implicitSize] = implicitSize - root[d.implicitOtherSize] = implicitOtherSize - } - - function clampedMinMax(value, minimum, maximum) - { - if (value < minimum) - value = minimum - if (value > maximum) - value = maximum - return value - } - - function accumulatedSize(firstIndex, lastIndex, includeFillItemMinimum) - { - // Go through items and handles, and - // calculate their accummulated width. - var w = 0 - for (var i=firstIndex; i __handleIndex) - visible: __items[__handleIndex + (resizeLeftItem ? 0 : 1)].visible - sourceComponent: handleDelegate - onWidthChanged: d.updateLayout() - onHeightChanged: d.updateLayout() - onXChanged: moveHandle() - onYChanged: moveHandle() - - MouseArea { - id: mouseArea - anchors.fill: parent - property real defaultMargin: Screen.pixelDensity * 2 - anchors.leftMargin: (parent.width <= 1) ? -defaultMargin : 0 - anchors.rightMargin: (parent.width <= 1) ? -defaultMargin : 0 - anchors.topMargin: (parent.height <= 1) ? -defaultMargin : 0 - anchors.bottomMargin: (parent.height <= 1) ? -defaultMargin : 0 - hoverEnabled: true - drag.threshold: 0 - drag.target: parent - drag.axis: root.orientation === Qt.Horizontal ? Drag.XAxis : Drag.YAxis - cursorShape: root.orientation === Qt.Horizontal ? Qt.SplitHCursor : Qt.SplitVCursor - } - - function moveHandle() { - // Moving the handle means resizing an item. Which one, - // left or right, depends on where the fillItem is. - // 'updateLayout' will be overridden in case new width violates max/min. - // 'updateLayout' will be triggered when an item changes width. - if (d.updateLayoutGuard) - return - - var leftHandle, leftItem, rightItem, rightHandle - var leftEdge, rightEdge, newWidth, leftStopX, rightStopX - var i - - if (resizeLeftItem) { - // Ensure that the handle is not crossing other handles. So - // find the first visible handle to the left to determine the left edge: - leftEdge = 0 - for (i=__handleIndex-1; i>=0; --i) { - leftHandle = __handles[i] - if (leftHandle.visible) { - leftEdge = leftHandle[d.offset] + leftHandle[d.size] - break; - } - } - - // Ensure: leftStopX >= itemHandle[d.offset] >= rightStopX - var min = d.accumulatedSize(__handleIndex+1, __items.length, true) - rightStopX = root[d.size] - min - itemHandle[d.size] - leftStopX = Math.max(leftEdge, itemHandle[d.offset]) - itemHandle[d.offset] = Math.min(rightStopX, Math.max(leftStopX, itemHandle[d.offset])) - - newWidth = itemHandle[d.offset] - leftEdge - leftItem = __items[__handleIndex] - // The next line will trigger 'updateLayout': - leftItem[d.size] = newWidth - } else { - // Resize item to the right. - // Ensure that the handle is not crossing other handles. So - // find the first visible handle to the right to determine the right edge: - rightEdge = root[d.size] - for (i=__handleIndex+1; i<__handles.length; ++i) { - rightHandle = __handles[i] - if (rightHandle.visible) { - rightEdge = rightHandle[d.offset] - break; - } - } - - // Ensure: leftStopX <= itemHandle[d.offset] <= rightStopX - min = d.accumulatedSize(0, __handleIndex+1, true) - leftStopX = min - itemHandle[d.size] - rightStopX = Math.min((rightEdge - itemHandle[d.size]), itemHandle[d.offset]) - itemHandle[d.offset] = Math.max(leftStopX, Math.min(itemHandle[d.offset], rightStopX)) - - newWidth = rightEdge - (itemHandle[d.offset] + itemHandle[d.size]) - rightItem = __items[__handleIndex+1] - // The next line will trigger 'updateLayout': - rightItem[d.size] = newWidth - } - } - } - } - - Item { - id: contents - visible: false - anchors.fill: parent - } - Item { - id: splitterItems - anchors.fill: parent - } - Item { - id: splitterHandles - anchors.fill: parent - } - - Item { - id: lastItem - onVisibleChanged: d.updateFillIndex() - } - - Component.onDestruction: { - for (var i=0; ia{color: white;} .user-pill{}" + (replyDisplay || "") wrapMode: Label.Wrap textFormat: Label.RichText @@ -178,6 +184,14 @@ ColumnLayout { } } + Rectangle { + Layout.fillWidth: true + Layout.preferredHeight: 1 + + visible: replyEventId || "" + color: "white" + } + TextEdit { Layout.fillWidth: true @@ -187,7 +201,7 @@ ColumnLayout { color: "white" - font.family: CommonFont.font.family + font.family: window.font.family font.pixelSize: 14 selectByMouse: true readOnly: true diff --git a/imports/Spectral/Component/Timeline/StateDelegate.qml b/imports/Spectral/Component/Timeline/StateDelegate.qml index 9e6fef7..ca17279 100644 --- a/imports/Spectral/Component/Timeline/StateDelegate.qml +++ b/imports/Spectral/Component/Timeline/StateDelegate.qml @@ -23,5 +23,6 @@ Label { background: Rectangle { color: MPalette.banner radius: 4 + antialiasing: true } } diff --git a/imports/Spectral/Component/qmldir b/imports/Spectral/Component/qmldir index be035c4..4cbc3dd 100644 --- a/imports/Spectral/Component/qmldir +++ b/imports/Spectral/Component/qmldir @@ -5,5 +5,5 @@ SideNavButton 2.0 SideNavButton.qml ScrollHelper 2.0 ScrollHelper.qml AutoListView 2.0 AutoListView.qml AutoTextField 2.0 AutoTextField.qml -SplitView 2.0 SplitView.qml Avatar 2.0 Avatar.qml +FullScreenImage 2.0 FullScreenImage.qml diff --git a/imports/Spectral/Dialog/AcceptInvitationDialog.qml b/imports/Spectral/Dialog/AcceptInvitationDialog.qml new file mode 100644 index 0000000..b57c99b --- /dev/null +++ b/imports/Spectral/Dialog/AcceptInvitationDialog.qml @@ -0,0 +1,50 @@ +import QtQuick 2.12 +import QtQuick.Controls 2.12 + +Dialog { + property var room + + anchors.centerIn: parent + width: 360 + + id: root + + title: "Invitation Received" + modal: true + + contentItem: Label { + text: "Accept this invitation?" + } + + footer: DialogButtonBox { + Button { + text: "Accept" + flat: true + + onClicked: { + room.acceptInvitation() + close() + } + } + + Button { + text: "Reject" + flat: true + + onClicked: { + room.forget() + close() + } + } + + Button { + text: "Cancel" + flat: true + + onClicked: close() + } + } + + onClosed: destroy() +} + diff --git a/imports/Spectral/Dialog/AccountDetailDialog.qml b/imports/Spectral/Dialog/AccountDetailDialog.qml new file mode 100644 index 0000000..959c9ec --- /dev/null +++ b/imports/Spectral/Dialog/AccountDetailDialog.qml @@ -0,0 +1,345 @@ +import QtQuick 2.12 +import QtQuick.Controls 2.12 +import QtQuick.Layouts 1.12 + +import Spectral.Component 2.0 +import Spectral.Effect 2.0 + +import Spectral 0.1 +import Spectral.Setting 0.1 + +Dialog { + anchors.centerIn: parent + + width: 480 + + id: root + + contentItem: Column { + id: detailColumn + + spacing: 0 + + Repeater { + model: AccountListModel{ + controller: spectralController + } + + delegate: Item { + width: detailColumn.width + height: 72 + + RowLayout { + anchors.fill: parent + anchors.margins: 12 + + spacing: 12 + + Avatar { + Layout.preferredWidth: height + Layout.fillHeight: true + + source: user.avatarMediaId + hint: user.displayName || "No Name" + } + + ColumnLayout { + Layout.fillWidth: true + Layout.alignment: Qt.AlignHCenter + + Label { + Layout.fillWidth: true + + text: user.displayName || "No Name" + color: MPalette.foreground + font.pixelSize: 16 + font.bold: true + elide: Text.ElideRight + wrapMode: Text.NoWrap + } + + Label { + Layout.fillWidth: true + + text: connection === spectralController.connection ? "Active" : "Online" + color: MPalette.lighter + font.pixelSize: 13 + elide: Text.ElideRight + wrapMode: Text.NoWrap + } + } + } + + Menu { + id: contextMenu + + MenuItem { + text: "Logout" + + onClicked: spectralController.logout(connection) + } + } + + RippleEffect { + anchors.fill: parent + + onPrimaryClicked: spectralController.connection = connection + onSecondaryClicked: contextMenu.popup() + } + } + } + + RowLayout { + width: parent.width + + MenuSeparator { + Layout.fillWidth: true + } + + ToolButton { + Layout.preferredWidth: 48 + Layout.preferredHeight: 48 + + contentItem: MaterialIcon { + icon: "\ue145" + color: MPalette.lighter + } + + onClicked: loginDialog.createObject(ApplicationWindow.overlay).open() + } + } + + Control { + width: parent.width + + contentItem: RowLayout { + MaterialIcon { + Layout.preferredWidth: 48 + Layout.preferredHeight: 48 + + color: MPalette.foreground + icon: "\ue7ff" + } + + Label { + Layout.fillWidth: true + + color: MPalette.foreground + text: "Start a Chat" + } + } + + RippleEffect { + anchors.fill: parent + + onPrimaryClicked: joinRoomDialog.createObject(ApplicationWindow.overlay).open() + } + } + + Control { + width: parent.width + + contentItem: RowLayout { + MaterialIcon { + Layout.preferredWidth: 48 + Layout.preferredHeight: 48 + + color: MPalette.foreground + icon: "\ue7fc" + } + + Label { + Layout.fillWidth: true + + color: MPalette.foreground + text: "Create a Room" + } + } + + RippleEffect { + anchors.fill: parent + + onPrimaryClicked: createRoomDialog.createObject(ApplicationWindow.overlay).open() + } + } + + MenuSeparator { + width: parent.width + } + + Control { + width: parent.width + + contentItem: RowLayout { + MaterialIcon { + Layout.preferredWidth: 48 + Layout.preferredHeight: 48 + + color: MPalette.foreground + icon: "\ue3a9" + } + + Label { + Layout.fillWidth: true + + color: MPalette.foreground + text: "Night Mode" + } + + Switch { + id: darkThemeSwitch + + checked: MSettings.darkTheme + onCheckedChanged: MSettings.darkTheme = checked + } + } + + RippleEffect { + anchors.fill: parent + + onPrimaryClicked: darkThemeSwitch.checked = !darkThemeSwitch.checked + } + } + + Control { + width: parent.width + + contentItem: RowLayout { + MaterialIcon { + Layout.preferredWidth: 48 + Layout.preferredHeight: 48 + + color: MPalette.foreground + icon: "\ue5d2" + } + + Label { + Layout.fillWidth: true + + color: MPalette.foreground + text: "Enable System Tray" + } + + Switch { + id: trayIconSwitch + + checked: MSettings.showTray + onCheckedChanged: MSettings.showTray = checked + } + } + + RippleEffect { + anchors.fill: parent + + onPrimaryClicked: trayIconSwitch.checked = !trayIconSwitch.checked + } + } + + Control { + width: parent.width + + contentItem: RowLayout { + MaterialIcon { + Layout.preferredWidth: 48 + Layout.preferredHeight: 48 + + color: MPalette.foreground + icon: "\ue7f5" + } + + Label { + Layout.fillWidth: true + + color: MPalette.foreground + text: "Enable Notifications" + } + + Switch { + id: notificationsSwitch + + checked: MSettings.showNotification + onCheckedChanged: MSettings.showNotification = checked + } + } + + RippleEffect { + anchors.fill: parent + + onPrimaryClicked: notificationsSwitch.checked = !notificationsSwitch.checked + } + } + + MenuSeparator { + width: parent.width + } + + Control { + width: parent.width + + contentItem: RowLayout { + MaterialIcon { + Layout.preferredWidth: 48 + Layout.preferredHeight: 48 + + color: MPalette.foreground + icon: "\ue167" + } + + Label { + Layout.fillWidth: true + + color: MPalette.foreground + text: "Font Family" + } + } + + RippleEffect { + anchors.fill: parent + + onPrimaryClicked: fontFamilyDialog.createObject(ApplicationWindow.overlay).open() + } + } + + Control { + width: parent.width + + contentItem: RowLayout { + MaterialIcon { + Layout.preferredWidth: 48 + Layout.preferredHeight: 48 + + color: MPalette.foreground + icon: "\ue8aa" + } + + Label { + Layout.fillWidth: true + + color: MPalette.foreground + text: "Chat Background" + } + } + + RippleEffect { + anchors.fill: parent + + onPrimaryClicked: { + var fileDialog = chatBackgroundDialog.createObject(ApplicationWindow.overlay) + + fileDialog.chosen.connect(function(path) { + if (!path) return + + MSettings.timelineBackground = path + }) + fileDialog.rejected.connect(function(path) { + MSettings.timelineBackground = "" + }) + + fileDialog.open() + } + } + } + } + + onClosed: destroy() +} diff --git a/imports/Spectral/Dialog/CreateRoomDialog.qml b/imports/Spectral/Dialog/CreateRoomDialog.qml new file mode 100644 index 0000000..e6efb64 --- /dev/null +++ b/imports/Spectral/Dialog/CreateRoomDialog.qml @@ -0,0 +1,38 @@ +import QtQuick 2.12 +import QtQuick.Controls 2.12 +import QtQuick.Layouts 1.12 + +import Spectral.Component 2.0 + +Dialog { + anchors.centerIn: parent + width: 360 + + id: root + + title: "Create a Room" + + contentItem: ColumnLayout { + AutoTextField { + Layout.fillWidth: true + + id: roomNameField + + placeholderText: "Room Name" + } + + AutoTextField { + Layout.fillWidth: true + + id: roomTopicField + + placeholderText: "Room Topic" + } + } + + standardButtons: Dialog.Ok | Dialog.Cancel + + onAccepted: spectralController.createRoom(spectralController.connection, roomNameField.text, roomTopicField.text) + + onClosed: destroy() +} diff --git a/imports/Spectral/Dialog/FontFamilyDialog.qml b/imports/Spectral/Dialog/FontFamilyDialog.qml new file mode 100644 index 0000000..7fda066 --- /dev/null +++ b/imports/Spectral/Dialog/FontFamilyDialog.qml @@ -0,0 +1,30 @@ +import QtQuick 2.12 +import QtQuick.Controls 2.12 +import QtQuick.Layouts 1.12 + +import Spectral.Component 2.0 +import Spectral.Setting 0.1 + +Dialog { + anchors.centerIn: parent + width: 360 + + id: root + + title: "Enter Font Family" + + contentItem: AutoTextField { + Layout.fillWidth: true + + id:fontFamilyField + + text: MSettings.fontFamily + placeholderText: "Font Family" + } + + standardButtons: Dialog.Ok | Dialog.Cancel + + onAccepted: MSettings.fontFamily = fontFamilyField.text + + onClosed: destroy() +} diff --git a/imports/Spectral/Dialog/InviteUserDialog.qml b/imports/Spectral/Dialog/InviteUserDialog.qml new file mode 100644 index 0000000..f56858a --- /dev/null +++ b/imports/Spectral/Dialog/InviteUserDialog.qml @@ -0,0 +1,28 @@ +import QtQuick 2.12 +import QtQuick.Controls 2.12 +import QtQuick.Layouts 1.12 + +import Spectral.Component 2.0 + +Dialog { + property var room + + anchors.centerIn: parent + width: 360 + + id: root + + title: "Invite User" + + modal: true + standardButtons: Dialog.Ok | Dialog.Cancel + + contentItem: AutoTextField { + id: inviteUserDialogTextField + placeholderText: "User ID" + } + + onAccepted: room.inviteToRoom(inviteUserDialogTextField.text) + + onClosed: destroy() +} diff --git a/imports/Spectral/Dialog/JoinRoomDialog.qml b/imports/Spectral/Dialog/JoinRoomDialog.qml new file mode 100644 index 0000000..e819cf5 --- /dev/null +++ b/imports/Spectral/Dialog/JoinRoomDialog.qml @@ -0,0 +1,38 @@ +import QtQuick 2.12 +import QtQuick.Controls 2.12 +import QtQuick.Layouts 1.12 + +import Spectral.Component 2.0 + +Dialog { + anchors.centerIn: parent + width: 360 + + id: root + + title: "Start a Chat" + + contentItem: ColumnLayout { + AutoTextField { + Layout.fillWidth: true + + id: identifierField + + placeholderText: "Room Alias/User ID" + } + } + + standardButtons: Dialog.Ok | Dialog.Cancel + + onAccepted: { + var identifier = identifierField.text + var firstChar = identifier.charAt(0) + if (firstChar == "@") { + spectralController.createDirectChat(spectralController.connection, identifier) + } else if (firstChar == "!" || firstChar == "#") { + spectralController.joinRoom(spectralController.connection, identifier) + } + } + + onClosed: destroy() +} diff --git a/imports/Spectral/Dialog/LoginDialog.qml b/imports/Spectral/Dialog/LoginDialog.qml new file mode 100644 index 0000000..199cbf5 --- /dev/null +++ b/imports/Spectral/Dialog/LoginDialog.qml @@ -0,0 +1,56 @@ +import QtQuick 2.12 +import QtQuick.Controls 2.12 +import QtQuick.Layouts 1.12 + +import Spectral.Component 2.0 + +Dialog { + anchors.centerIn: parent + width: 360 + + id: root + + title: "Login" + + standardButtons: Dialog.Ok | Dialog.Cancel + + onAccepted: doLogin() + + contentItem: ColumnLayout { + AutoTextField { + Layout.fillWidth: true + + id: serverField + + placeholderText: "Server Address" + text: "https://matrix.org" + } + + AutoTextField { + Layout.fillWidth: true + + id: usernameField + + placeholderText: "Username" + + onAccepted: passwordField.forceActiveFocus() + } + + AutoTextField { + Layout.fillWidth: true + + id: passwordField + + placeholderText: "Password" + echoMode: TextInput.Password + + onAccepted: root.accept() + } + } + + function doLogin() { + spectralController.loginWithCredentials(serverField.text, usernameField.text, passwordField.text) + } + + onClosed: destroy() +} diff --git a/imports/Spectral/Dialog/MessageSourceDialog.qml b/imports/Spectral/Dialog/MessageSourceDialog.qml new file mode 100644 index 0000000..e9cea1c --- /dev/null +++ b/imports/Spectral/Dialog/MessageSourceDialog.qml @@ -0,0 +1,27 @@ +import QtQuick 2.12 +import QtQuick.Controls 2.12 + +Popup { + property string sourceText + + anchors.centerIn: parent + width: 480 + + id: root + + modal: true + padding: 16 + + closePolicy: Dialog.CloseOnEscape | Dialog.CloseOnPressOutside + + contentItem: ScrollView { + clip: true + + Label { + text: sourceText + } + } + + onClosed: destroy() +} + diff --git a/imports/Spectral/Dialog/OpenFileDialog.qml b/imports/Spectral/Dialog/OpenFileDialog.qml new file mode 100644 index 0000000..ec9e8e7 --- /dev/null +++ b/imports/Spectral/Dialog/OpenFileDialog.qml @@ -0,0 +1,13 @@ +import QtQuick 2.12 +import QtQuick.Dialogs 1.2 + +FileDialog { + signal chosen(string path) + + id: root + + title: "Please choose a file" + selectMultiple: false + + onAccepted: chosen(selectFolder ? folder : fileUrl) +} diff --git a/imports/Spectral/Dialog/RoomSettingsDialog.qml b/imports/Spectral/Dialog/RoomSettingsDialog.qml new file mode 100644 index 0000000..c71db94 --- /dev/null +++ b/imports/Spectral/Dialog/RoomSettingsDialog.qml @@ -0,0 +1,218 @@ +import QtQuick 2.12 +import QtQuick.Controls 2.12 +import QtQuick.Layouts 1.12 + +import Spectral.Component 2.0 +import Spectral.Effect 2.0 +import Spectral.Setting 0.1 + +Dialog { + property var room + + anchors.centerIn: parent + width: 480 + + id: root + + title: "Room Settings - " + (room ? room.displayName : "") + modal: true + + contentItem: ColumnLayout { + RowLayout { + Layout.fillWidth: true + + spacing: 16 + + Avatar { + Layout.preferredWidth: 72 + Layout.preferredHeight: 72 + Layout.alignment: Qt.AlignTop + + hint: room ? room.displayName : "No name" + source: room ? room.avatarMediaId : null + } + + ColumnLayout { + Layout.fillWidth: true + Layout.margins: 4 + + AutoTextField { + Layout.fillWidth: true + + text: room ? room.name : "" + placeholderText: "Room Name" + } + + AutoTextField { + Layout.fillWidth: true + + text: room ? room.topic : "" + placeholderText: "Room Topic" + } + } + } + + Control { + Layout.fillWidth: true + + visible: room ? room.predecessorId : false + + padding: 8 + + contentItem: RowLayout { + MaterialIcon { + Layout.preferredWidth: 48 + Layout.preferredHeight: 48 + + icon: "\ue8d4" + } + + ColumnLayout { + Layout.fillWidth: true + + spacing: 0 + + Label { + Layout.fillWidth: true + + font.bold: true + color: MPalette.foreground + text: "This room is a continuation of another conversation." + } + + Label { + Layout.fillWidth: true + + color: MPalette.lighter + text: "Click here to see older messages." + } + } + } + + background: Rectangle { + color: MPalette.banner + + RippleEffect { + anchors.fill: parent + + onClicked: { + roomListForm.enteredRoom = spectralController.connection.room(room.predecessorId) + root.close() + } + } + } + } + + Control { + Layout.fillWidth: true + + visible: room ? room.successorId : false + + padding: 8 + + contentItem: RowLayout { + MaterialIcon { + Layout.preferredWidth: 48 + Layout.preferredHeight: 48 + + icon: "\ue8d4" + } + + ColumnLayout { + Layout.fillWidth: true + + spacing: 0 + + Label { + Layout.fillWidth: true + + font.bold: true + color: MPalette.foreground + text: "This room has been replaced and is no longer active." + } + + Label { + Layout.fillWidth: true + + color: MPalette.lighter + text: "The conversation continues here." + } + } + } + + background: Rectangle { + color: MPalette.banner + + RippleEffect { + anchors.fill: parent + + onClicked: { + roomListForm.enteredRoom = spectralController.connection.room(room.successorId) + root.close() + } + } + } + } + + MenuSeparator { + Layout.fillWidth: true + } + + ColumnLayout { + Layout.fillWidth: true + + RowLayout { + Layout.fillWidth: true + + Label { + Layout.preferredWidth: 100 + + wrapMode: Label.Wrap + text: "Main Alias" + color: MPalette.lighter + } + + ComboBox { + Layout.fillWidth: true + + model: room ? room.aliases : null + + currentIndex: room ? room.aliases.indexOf(room.canonicalAlias) : -1 + } + } + + RowLayout { + Layout.fillWidth: true + + Label { + Layout.preferredWidth: 100 + Layout.alignment: Qt.AlignTop + + wrapMode: Label.Wrap + text: "Aliases" + color: MPalette.lighter + } + + ColumnLayout { + Layout.fillWidth: true + + Repeater { + model: room ? room.aliases : null + + delegate: Label { + Layout.fillWidth: true + + text: modelData + + font.pixelSize: 12 + color: MPalette.lighter + } + } + } + } + } + } + + onClosed: destroy() +} + diff --git a/imports/Spectral/Dialog/UserDetailDialog.qml b/imports/Spectral/Dialog/UserDetailDialog.qml new file mode 100644 index 0000000..7d5526f --- /dev/null +++ b/imports/Spectral/Dialog/UserDetailDialog.qml @@ -0,0 +1,164 @@ +import QtQuick 2.12 +import QtQuick.Controls 2.12 +import QtQuick.Layouts 1.12 + +import Spectral.Component 2.0 +import Spectral.Effect 2.0 +import Spectral.Setting 0.1 + +Dialog { + property var room + property var user + + anchors.centerIn: parent + width: 360 + + id: root + + modal: true + + contentItem: ColumnLayout { + RowLayout { + Layout.fillWidth: true + + spacing: 16 + + Avatar { + Layout.preferredWidth: 72 + Layout.preferredHeight: 72 + + hint: user ? user.displayName : "No name" + source: user ? user.avatarMediaId : null + } + + ColumnLayout { + Layout.fillWidth: true + + Label { + Layout.fillWidth: true + + font.pixelSize: 18 + font.bold: true + + elide: Text.ElideRight + wrapMode: Text.NoWrap + text: user ? user.displayName : "No Name" + color: MPalette.foreground + } + + Label { + Layout.fillWidth: true + + text: "Online" + color: MPalette.lighter + } + } + } + + MenuSeparator { + Layout.fillWidth: true + } + + RowLayout { + Layout.fillWidth: true + + spacing: 8 + + MaterialIcon { + Layout.preferredWidth: 32 + Layout.preferredHeight: 32 + Layout.alignment: Qt.AlignTop + + icon: "\ue88f" + color: MPalette.lighter + } + + ColumnLayout { + Layout.fillWidth: true + + Label { + Layout.fillWidth: true + + elide: Text.ElideRight + wrapMode: Text.NoWrap + text: user ? user.id : "No ID" + color: MPalette.accent + } + + Label { + Layout.fillWidth: true + + wrapMode: Label.Wrap + text: "User ID" + color: MPalette.lighter + } + } + } + + MenuSeparator { + Layout.fillWidth: true + } + + Control { + Layout.fillWidth: true + + contentItem: RowLayout { + MaterialIcon { + Layout.preferredWidth: 32 + Layout.preferredHeight: 32 + Layout.alignment: Qt.AlignTop + + icon: room.connection.isIgnored(user) ? "\ue7f5" : "\ue7f6" + color: MPalette.lighter + } + + Label { + Layout.fillWidth: true + + wrapMode: Label.Wrap + text: room.connection.isIgnored(user) ? "Unignore this user" : "Ignore this user" + + color: MPalette.accent + } + } + + background: RippleEffect { + onPrimaryClicked: { + root.close() + room.connection.isIgnored(user) ? room.connection.removeFromIgnoredUsers(user) : room.connection.addToIgnoredUsers(user) + } + } + } + + Control { + Layout.fillWidth: true + + contentItem: RowLayout { + MaterialIcon { + Layout.preferredWidth: 32 + Layout.preferredHeight: 32 + Layout.alignment: Qt.AlignTop + + icon: "\ue5d9" + color: MPalette.lighter + } + + Label { + Layout.fillWidth: true + + wrapMode: Label.Wrap + text: "Kick this user" + + color: MPalette.accent + } + } + + background: RippleEffect { + onPrimaryClicked: room.kickMember(user.id) + } + } + } + + onClosed: destroy() +} + diff --git a/imports/Spectral/Dialog/qmldir b/imports/Spectral/Dialog/qmldir new file mode 100644 index 0000000..0b27eaa --- /dev/null +++ b/imports/Spectral/Dialog/qmldir @@ -0,0 +1,12 @@ +module Spectral.Dialog +RoomSettingsDialog 2.0 RoomSettingsDialog.qml +UserDetailDialog 2.0 UserDetailDialog.qml +MessageSourceDialog 2.0 MessageSourceDialog.qml +LoginDialog 2.0 LoginDialog.qml +CreateRoomDialog 2.0 CreateRoomDialog.qml +JoinRoomDialog 2.0 JoinRoomDialog.qml +InviteUserDialog 2.0 InviteUserDialog.qml +AcceptInvitationDialog 2.0 AcceptInvitationDialog.qml +FontFamilyDialog 2.0 FontFamilyDialog.qml +AccountDetailDialog 2.0 AccountDetailDialog.qml +OpenFileDialog 2.0 OpenFileDialog.qml diff --git a/imports/Spectral/Font/CommonFont.qml b/imports/Spectral/Font/CommonFont.qml deleted file mode 100644 index 6205c2e..0000000 --- a/imports/Spectral/Font/CommonFont.qml +++ /dev/null @@ -1,5 +0,0 @@ -pragma Singleton -import QtQuick 2.12 -import QtQuick.Controls 2.12 - -Label {} diff --git a/imports/Spectral/Font/qmldir b/imports/Spectral/Font/qmldir index bdd2daa..96721b2 100644 --- a/imports/Spectral/Font/qmldir +++ b/imports/Spectral/Font/qmldir @@ -1,3 +1,2 @@ module Spectral.Font singleton MaterialFont 0.1 MaterialFont.qml -singleton CommonFont 0.1 CommonFont.qml diff --git a/imports/Spectral/Menu/RoomListContextMenu.qml b/imports/Spectral/Menu/RoomListContextMenu.qml new file mode 100644 index 0000000..b44903e --- /dev/null +++ b/imports/Spectral/Menu/RoomListContextMenu.qml @@ -0,0 +1,42 @@ +import QtQuick 2.12 +import QtQuick.Controls 2.12 +import QtQuick.Controls.Material 2.12 + +Menu { + property var room + + id: root + + MenuItem { + text: "Favourite" + checkable: true + checked: room.isFavourite + + onTriggered: room.isFavourite ? room.removeTag("m.favourite") : room.addTag("m.favourite", 1.0) + } + + MenuItem { + text: "Deprioritize" + checkable: true + checked: room.isLowPriority + + onTriggered: room.isLowPriority ? room.removeTag("m.lowpriority") : room.addTag("m.lowpriority", 1.0) + } + + MenuSeparator {} + + MenuItem { + text: "Mark as Read" + + onTriggered: room.markAllMessagesAsRead() + } + + MenuItem { + text: "Leave Room" + Material.foreground: Material.Red + + onTriggered: room.forget() + } + + onClosed: destroy() +} diff --git a/imports/Spectral/Menu/Timeline/FileDelegateContextMenu.qml b/imports/Spectral/Menu/Timeline/FileDelegateContextMenu.qml new file mode 100644 index 0000000..650e573 --- /dev/null +++ b/imports/Spectral/Menu/Timeline/FileDelegateContextMenu.qml @@ -0,0 +1,46 @@ +import QtQuick 2.12 +import QtQuick.Controls 2.12 + +import Spectral.Dialog 2.0 + +Menu { + signal viewSource() + signal downloadAndOpen() + signal saveFileAs() + signal reply() + signal redact() + + id: root + + MenuItem { + text: "View Source" + + onTriggered: viewSource() + } + + MenuItem { + text: "Open Externally" + + onTriggered: downloadAndOpen() + } + + MenuItem { + text: "Save As" + + onTriggered: saveFileAs() + } + + MenuItem { + text: "Reply" + + onTriggered: reply() + } + + MenuItem { + text: "Redact" + + onTriggered: redact() + } + + onClosed: destroy() +} diff --git a/imports/Spectral/Menu/Timeline/MessageDelegateContextMenu.qml b/imports/Spectral/Menu/Timeline/MessageDelegateContextMenu.qml new file mode 100644 index 0000000..62ca02a --- /dev/null +++ b/imports/Spectral/Menu/Timeline/MessageDelegateContextMenu.qml @@ -0,0 +1,34 @@ +import QtQuick 2.12 +import QtQuick.Controls 2.12 + +import Spectral.Dialog 2.0 + +Menu { + readonly property string selectedText: contentLabel.selectedText + + signal viewSource() + signal reply() + signal redact() + + id: root + + MenuItem { + text: "View Source" + + onTriggered: viewSource() + } + + MenuItem { + text: "Reply" + + onTriggered: reply() + } + + MenuItem { + text: "Redact" + + onTriggered: redact() + } + + onClosed: destroy() +} diff --git a/imports/Spectral/Menu/Timeline/qmldir b/imports/Spectral/Menu/Timeline/qmldir new file mode 100644 index 0000000..20b869c --- /dev/null +++ b/imports/Spectral/Menu/Timeline/qmldir @@ -0,0 +1,3 @@ +module Spectral.Menu.Timeline +MessageDelegateContextMenu 2.0 MessageDelegateContextMenu.qml +FileDelegateContextMenu 2.0 FileDelegateContextMenu.qml diff --git a/imports/Spectral/Menu/qmldir b/imports/Spectral/Menu/qmldir new file mode 100644 index 0000000..c52d0fd --- /dev/null +++ b/imports/Spectral/Menu/qmldir @@ -0,0 +1,2 @@ +module Spectral.Menu +RoomListContextMenu 2.0 RoomListContextMenu.qml diff --git a/imports/Spectral/Page/qmldir b/imports/Spectral/Page/qmldir deleted file mode 100644 index af81264..0000000 --- a/imports/Spectral/Page/qmldir +++ /dev/null @@ -1,4 +0,0 @@ -module Spectral.Page -Login 2.0 Login.qml -Room 2.0 Room.qml -Setting 2.0 Setting.qml diff --git a/imports/Spectral/Panel/RoomDrawer.qml b/imports/Spectral/Panel/RoomDrawer.qml index 475c5f2..3fc90fb 100644 --- a/imports/Spectral/Panel/RoomDrawer.qml +++ b/imports/Spectral/Panel/RoomDrawer.qml @@ -4,6 +4,9 @@ import QtQuick.Controls.Material 2.12 import QtQuick.Layouts 1.12 import Spectral.Component 2.0 +import Spectral.Dialog 2.0 +import Spectral.Effect 2.0 +import Spectral.Setting 0.1 import Spectral 0.1 @@ -16,79 +19,142 @@ Drawer { ColumnLayout { anchors.fill: parent - anchors.margins: 32 + anchors.margins: 24 - Avatar { - Layout.preferredWidth: 96 - Layout.preferredHeight: 96 - Layout.alignment: Qt.AlignHCenter - - hint: room ? room.displayName : "No name" - source: room ? room.avatarMediaId : null - } - - Label { + RowLayout { Layout.fillWidth: true - wrapMode: Label.Wrap - horizontalAlignment: Text.AlignHCenter - text: room && room.id ? room.id : "" + spacing: 16 + + Avatar { + Layout.preferredWidth: 72 + Layout.preferredHeight: 72 + + hint: room ? room.displayName : "No name" + source: room ? room.avatarMediaId : null + } + + ColumnLayout { + Layout.fillWidth: true + + Label { + Layout.fillWidth: true + + font.pixelSize: 18 + font.bold: true + wrapMode: Label.Wrap + text: room ? room.displayName : "No Name" + color: MPalette.foreground + } + + Label { + Layout.fillWidth: true + + wrapMode: Label.Wrap + text: room ? room.totalMemberCount + " Members" : "No Member Count" + color: MPalette.lighter + } + } } - Label { + MenuSeparator { Layout.fillWidth: true - - wrapMode: Label.Wrap - horizontalAlignment: Text.AlignHCenter - text: room && room.canonicalAlias ? room.canonicalAlias : "No Canonical Alias" } - Label { + Control { Layout.fillWidth: true - wrapMode: Label.Wrap - horizontalAlignment: Text.AlignHCenter - text: room ? room.totalMemberCount + " Members" : "No Member Count" + padding: 0 + + contentItem: RowLayout { + spacing: 8 + + MaterialIcon { + Layout.preferredWidth: 32 + Layout.preferredHeight: 32 + Layout.alignment: Qt.AlignTop + + icon: "\ue88f" + color: MPalette.lighter + } + + ColumnLayout { + Layout.fillWidth: true + + Label { + Layout.fillWidth: true + + wrapMode: Label.Wrap + text: room && room.canonicalAlias ? room.canonicalAlias : "No Canonical Alias" + color: MPalette.accent + } + + Label { + Layout.fillWidth: true + + wrapMode: Label.Wrap + text: "Main Alias" + color: MPalette.lighter + } + + Label { + Layout.fillWidth: true + + wrapMode: Label.Wrap + text: room && room.topic ? room.topic : "No Topic" + color: MPalette.foreground + } + + Label { + Layout.fillWidth: true + + wrapMode: Label.Wrap + text: "Topic" + color: MPalette.lighter + } + } + } + + background: RippleEffect { + onPrimaryClicked: roomSettingDialog.createObject(ApplicationWindow.overlay, {"room": room}).open() + } + } + + MenuSeparator { + Layout.fillWidth: true } RowLayout { Layout.fillWidth: true - AutoTextField { + spacing: 8 + + MaterialIcon { + Layout.preferredWidth: 32 + Layout.preferredHeight: 32 + + icon: "\ue7ff" + color: MPalette.lighter + } + + Label { Layout.fillWidth: true - id: roomNameField - text: room && room.name ? room.name : "" + wrapMode: Label.Wrap + text: room ? room.totalMemberCount + " Members" : "No Member Count" + color: MPalette.lighter } - ItemDelegate { - Layout.preferredWidth: height - Layout.preferredHeight: parent.height + ToolButton { + Layout.preferredWidth: 32 + Layout.preferredHeight: 32 - contentItem: MaterialIcon { icon: "\ue5ca" } + contentItem: MaterialIcon { + icon: "\ue145" + color: MPalette.lighter + } - onClicked: room.setName(roomNameField.text) - } - } - - RowLayout { - Layout.fillWidth: true - - AutoTextField { - Layout.fillWidth: true - - id: roomTopicField - - text: room && room.topic ? room.topic : "" - } - - ItemDelegate { - Layout.preferredWidth: height - Layout.preferredHeight: parent.height - - contentItem: MaterialIcon { icon: "\ue5ca" } - - onClicked: room.setTopic(roomTopicField.text) + onClicked: inviteUserDialog.createObject(ApplicationWindow.overlay, {"room": room}).open() } } @@ -106,7 +172,7 @@ Drawer { room: roomDrawer.room } - delegate: SwipeDelegate { + delegate: Item { width: userListView.width height: 48 @@ -127,62 +193,36 @@ Drawer { Layout.fillWidth: true text: name + color: MPalette.foreground } } - swipe.right: Rectangle { - width: height - height: parent.height - anchors.right: parent.right - color: Material.accent + RippleEffect { + anchors.fill: parent - MaterialIcon { - anchors.fill: parent - icon: "\ue879" - color: "white" - } - - SwipeDelegate.onClicked: { - room.kickMember(userId) - swipe.close() - } + onPrimaryClicked: userDetailDialog.createObject(ApplicationWindow.overlay, {"room": room, "user": user}).open() } - - onClicked: swipe.open(SwipeDelegate.Right) } ScrollBar.vertical: ScrollBar {} } + } - Button { - Layout.fillWidth: true + Component { + id: roomSettingDialog - text: "Invite User" - flat: true - highlighted: true + RoomSettingsDialog {} + } - onClicked: inviteUserDialog.open() + Component { + id: userDetailDialog - Dialog { - x: (window.width - width) / 2 - y: (window.height - height) / 2 - width: 360 + UserDetailDialog {} + } - id: inviteUserDialog + Component { + id: inviteUserDialog - parent: ApplicationWindow.overlay - - title: "Input User ID" - modal: true - standardButtons: Dialog.Ok | Dialog.Cancel - - contentItem: AutoTextField { - id: inviteUserDialogTextField - placeholderText: "@bot:matrix.org" - } - - onAccepted: room.inviteToRoom(inviteUserDialogTextField.text) - } - } + InviteUserDialog {} } } diff --git a/imports/Spectral/Panel/RoomListPanel.qml b/imports/Spectral/Panel/RoomListPanel.qml index 2cf0025..1a5079b 100644 --- a/imports/Spectral/Panel/RoomListPanel.qml +++ b/imports/Spectral/Panel/RoomListPanel.qml @@ -5,6 +5,8 @@ import QtQuick.Layouts 1.12 import QtQuick.Controls.Material 2.12 import Spectral.Component 2.0 +import Spectral.Dialog 2.0 +import Spectral.Menu 2.0 import Spectral.Effect 2.0 import Spectral 0.1 @@ -13,12 +15,11 @@ import Spectral.Setting 0.1 import SortFilterProxyModel 0.2 Item { - property var controller: null - readonly property var user: controller.connection ? controller.connection.localUser : null + property var connection: null + readonly property var user: connection ? connection.localUser : null property int filter: 0 property var enteredRoom: null - property alias errorControl: errorControl signal enterRoom(var room) signal leaveRoom(var room) @@ -28,7 +29,7 @@ Item { RoomListModel { id: roomListModel - connection: controller.connection + connection: root.connection onNewMessage: if (!window.active && MSettings.showNotification) spectralController.postNotification(roomId, eventId, roomName, senderName, text, icon) } @@ -44,8 +45,8 @@ Item { switch (category) { case 1: return "Invited" case 2: return "Favorites" - case 3: return "Rooms" - case 4: return "People" + case 3: return "People" + case 4: return "Rooms" case 5: return "Low Priority" } } @@ -60,6 +61,9 @@ Item { ] filters: [ + ExpressionFilter { + expression: joinState != "upgraded" + }, RegExpFilter { roleName: "name" pattern: searchField.text @@ -71,483 +75,15 @@ Item { }, ExpressionFilter { enabled: filter === 2 - expression: category === 1 || category === 2 || category === 4 + expression: category === 1 || category === 2 || category === 3 }, ExpressionFilter { enabled: filter === 3 - expression: category === 3 || category === 5 + expression: category === 4 || category === 5 } ] } - Drawer { - width: Math.max(root.width, 400) - height: root.height - - id: drawer - - edge: Qt.LeftEdge - - Component { - id: mainPage - - ColumnLayout { - readonly property string title: "Main" - - id: mainColumn - - spacing: 0 - - Control { - Layout.fillWidth: true - Layout.preferredHeight: 330 - - padding: 24 - - contentItem: ColumnLayout { - spacing: 4 - - Avatar { - Layout.preferredWidth: 200 - Layout.preferredHeight: 200 - Layout.margins: 12 - Layout.alignment: Qt.AlignHCenter - - source: root.user ? root.user.avatarMediaId : null - hint: root.user ? root.user.displayName : "?" - } - - Label { - Layout.alignment: Qt.AlignHCenter - - text: root.user ? root.user.displayName : "No Name" - color: "white" - font.pixelSize: 22 - } - - Label { - Layout.alignment: Qt.AlignHCenter - - text: root.user ? root.user.id : "@example:matrix.org" - color: "white" - opacity: 0.7 - font.pixelSize: 13 - } - } - - background: Rectangle { color: Material.primary } - - RippleEffect { - anchors.fill: parent - - onClicked: stackView.push(userPage) - } - } - - ScrollView { - Layout.fillWidth: true - Layout.fillHeight: true - - clip: true - - ScrollBar.horizontal.policy: ScrollBar.AlwaysOff - - ColumnLayout { - width: mainColumn.width - spacing: 0 - - Repeater { - model: AccountListModel { - controller: spectralController - } - - delegate: ItemDelegate { - Layout.fillWidth: true - - text: user.displayName - - onClicked: { - controller.connection = connection - drawer.close() - } - } - } - - ItemDelegate { - Layout.fillWidth: true - - text: "Add Account" - - onClicked: loginDialog.open() - } - - Rectangle { - Layout.fillWidth: true - Layout.preferredHeight: 1 - - color: MSettings.darkTheme ? "#424242" : "#e7ebeb" - } - - ItemDelegate { - Layout.fillWidth: true - - text: "Settings" - - onClicked: stackView.push(settingsPage) - } - - ItemDelegate { - Layout.fillWidth: true - - text: "Logout" - - onClicked: controller.logout(controller.connection) - } - - ItemDelegate { - Layout.fillWidth: true - - text: "Exit" - - onClicked: Qt.quit() - } - } - } - } - } - - Component { - id: userPage - - ScrollView { - readonly property string title: "User Info" - - id: main - - ScrollBar.horizontal.policy: ScrollBar.AlwaysOff - - ColumnLayout { - width: main.width - spacing: 0 - - ItemDelegate { - Layout.fillWidth: true - - padding: 24 - - contentItem: ColumnLayout { - spacing: 0 - - Label { - Layout.fillWidth: true - Layout.fillHeight: true - - text: "Matrix ID" - font.pixelSize: 16 - elide: Text.ElideRight - wrapMode: Text.NoWrap - } - - Label { - Layout.fillWidth: true - Layout.fillHeight: true - - text: root.user.id - color: "#5B7480" - font.pixelSize: 13 - elide: Text.ElideRight - wrapMode: Text.NoWrap - } - } - } - - ItemDelegate { - Layout.fillWidth: true - - padding: 24 - - contentItem: ColumnLayout { - spacing: 0 - - Label { - Layout.fillWidth: true - Layout.fillHeight: true - - text: "Name" - font.pixelSize: 16 - elide: Text.ElideRight - wrapMode: Text.NoWrap - } - - Label { - Layout.fillWidth: true - Layout.fillHeight: true - - text: root.user.name - color: "#5B7480" - font.pixelSize: 13 - elide: Text.ElideRight - wrapMode: Text.NoWrap - } - } - } - - ItemDelegate { - Layout.fillWidth: true - - padding: 24 - - contentItem: ColumnLayout { - spacing: 0 - - Label { - Layout.fillWidth: true - Layout.fillHeight: true - - text: "Avatar" - font.pixelSize: 16 - elide: Text.ElideRight - wrapMode: Text.NoWrap - } - - Label { - Layout.fillWidth: true - Layout.fillHeight: true - - text: root.user.avatarMediaId - color: "#5B7480" - font.pixelSize: 13 - elide: Text.ElideRight - wrapMode: Text.NoWrap - } - } - } - - ItemDelegate { - Layout.fillWidth: true - - padding: 24 - - contentItem: ColumnLayout { - spacing: 0 - - Label { - Layout.fillWidth: true - Layout.fillHeight: true - - text: "Server" - font.pixelSize: 16 - elide: Text.ElideRight - wrapMode: Text.NoWrap - } - - Label { - Layout.fillWidth: true - Layout.fillHeight: true - - text: root.controller.connection.accessToken - color: "#5B7480" - font.pixelSize: 13 - elide: Text.ElideRight - wrapMode: Text.NoWrap - } - } - } - - ItemDelegate { - Layout.fillWidth: true - - padding: 24 - - contentItem: ColumnLayout { - spacing: 0 - - Label { - Layout.fillWidth: true - Layout.fillHeight: true - - text: "Device" - font.pixelSize: 16 - elide: Text.ElideRight - wrapMode: Text.NoWrap - } - - Label { - Layout.fillWidth: true - Layout.fillHeight: true - - text: root.controller.connection.deviceId - color: "#5B7480" - font.pixelSize: 13 - elide: Text.ElideRight - wrapMode: Text.NoWrap - } - } - } - - ItemDelegate { - Layout.fillWidth: true - - padding: 24 - - contentItem: ColumnLayout { - spacing: 0 - - Label { - Layout.fillWidth: true - Layout.fillHeight: true - - text: "Token" - font.pixelSize: 16 - elide: Text.ElideRight - wrapMode: Text.NoWrap - } - - Label { - Layout.fillWidth: true - Layout.fillHeight: true - - text: root.controller.connection.accessToken - color: "#5B7480" - font.pixelSize: 13 - elide: Text.ElideRight - wrapMode: Text.NoWrap - } - } - } - } - } - } - - Component { - id: settingsPage - - ScrollView { - readonly property string title: "Settings" - - id: main - - ScrollBar.horizontal.policy: ScrollBar.AlwaysOff - - padding: 32 - - ColumnLayout { - width: main.width - 64 - spacing: 0 - - Switch { - text: "Dark theme" - checked: MSettings.darkTheme - - onCheckedChanged: MSettings.darkTheme = checked - } - - Switch { - text: "Show notifications" - checked: MSettings.showNotification - - onCheckedChanged: MSettings.showNotification = checked - } - - Switch { - text: "Use press and hold instead of right click" - checked: MSettings.pressAndHold - - onCheckedChanged: MSettings.pressAndHold = checked - } - - Switch { - text: "Show tray icon" - checked: MSettings.showTray - - onCheckedChanged: MSettings.showTray = checked - } - - Switch { - text: "Enable timeline background" - checked: MSettings.enableTimelineBackground - - onCheckedChanged: MSettings.enableTimelineBackground = checked - } - - RowLayout { - Layout.fillWidth: true - - Label { - text: "DPI" - } - - Slider { - Layout.fillWidth: true - - value: controller.dpi() - from: 100 - to: 300 - stepSize: 25 - snapMode: Slider.SnapAlways - - ToolTip.visible: pressed - ToolTip.text: value - - onMoved: controller.setDpi(value) - } - } - } - } - } - - ColumnLayout { - anchors.fill: parent - - spacing: 0 - - Rectangle { - Layout.fillWidth: true - Layout.preferredHeight: 64 - - visible: stackView.depth > 1 - - color: Material.primary - - RowLayout { - anchors.fill: parent - anchors.margins: 4 - - ToolButton { - Layout.preferredWidth: height - Layout.fillHeight: true - - contentItem: MaterialIcon { - icon: "\ue5c4" - color: "white" - } - - onClicked: stackView.pop() - } - Label { - Layout.fillWidth: true - - text: stackView.currentItem.title - color: "white" - font.pixelSize: 18 - elide: Label.ElideRight - } - } - } - - StackView { - Layout.fillWidth: true - Layout.fillHeight: true - - id: stackView - - clip: true - - initialItem: mainPage - } - } - } - ColumnLayout { anchors.fill: parent spacing: 0 @@ -564,7 +100,7 @@ Item { rightPadding: 18 contentItem: RowLayout { - ItemDelegate { + ToolButton { Layout.preferredWidth: height Layout.fillHeight: true @@ -614,7 +150,7 @@ Item { onClicked: filterMenu.popup() } - ItemDelegate { + ToolButton { Layout.preferredWidth: height Layout.fillHeight: true @@ -627,40 +163,14 @@ Item { AutoTextField { readonly property bool active: text - readonly property bool isRoom: text.match(/#.*:.*\..*/g) || text.match(/!.*:.*\..*/g) - readonly property bool isUser: text.match(/@.*:.*\..*/g) Layout.fillWidth: true - Layout.fillHeight: true + Layout.alignment: Qt.AlignVCenter id: searchField - topPadding: 0 - bottomPadding: 0 placeholderText: "Search..." color: MPalette.lighter - - background: Item {} - } - - ItemDelegate { - Layout.preferredWidth: height - Layout.fillHeight: true - - visible: searchField.isRoom || searchField.isUser - - contentItem: MaterialIcon { icon: "\ue145" } - - onClicked: { - if (searchField.isRoom) { - controller.joinRoom(controller.connection, searchField.text) - return - } - if (searchField.isUser) { - controller.createDirectChat(controller.connection, searchField.text) - return - } - } } Avatar { @@ -673,9 +183,12 @@ Item { source: root.user ? root.user.avatarMediaId : null hint: root.user ? root.user.displayName : "?" - MouseArea { + RippleEffect { anchors.fill: parent - onClicked: drawer.open() + + circular: true + + onClicked: accountDetailDialog.createObject(ApplicationWindow.overlay).open() } } } @@ -692,52 +205,6 @@ Item { } } - Control { - property string error: "" - property string detail: "" - - Layout.fillWidth: true - - id: errorControl - - visible: false - - topPadding: 16 - bottomPadding: 16 - leftPadding: 24 - rightPadding: 24 - - contentItem: ColumnLayout { - Label { - Layout.fillWidth: true - - text: errorControl.error - font.pixelSize: 16 - color: "white" - wrapMode: Text.Wrap - } - Label { - Layout.fillWidth: true - - text: errorControl.detail - font.pixelSize: 14 - color: "white" - opacity: 0.6 - wrapMode: Text.Wrap - } - } - - background: Rectangle { - color: "#273338" - } - - RippleEffect { - anchors.fill: parent - - onClicked: errorControl.visible = false - } - } - AutoListView { Layout.fillWidth: true Layout.fillHeight: true @@ -766,7 +233,7 @@ Item { } Rectangle { - width: unreadCount > 0 ? 4 : 0 + width: unreadCount >= 0 ? 4 : 0 height: parent.height color: Material.accent @@ -854,11 +321,9 @@ Item { RippleEffect { anchors.fill: parent - onSecondaryClicked: roomContextMenu.popup() onPrimaryClicked: { if (category === RoomType.Invited) { - inviteDialog.currentRoom = currentRoom - inviteDialog.open() + acceptInvitationDialog.createObject(ApplicationWindow.overlay, {"room": currentRoom}).open() } else { if (enteredRoom) { enteredRoom.displayed = false @@ -869,36 +334,13 @@ Item { enteredRoom = currentRoom } } + onSecondaryClicked: roomListContextMenu.createObject(ApplicationWindow.overlay, {"room": currentRoom}).popup() } - Menu { - id: roomContextMenu + Component { + id: roomListContextMenu - MenuItem { - text: "Favourite" - checkable: true - checked: category === RoomType.Favorite - - onTriggered: category === RoomType.Favorite ? currentRoom.removeTag("m.favourite") : currentRoom.addTag("m.favourite", 1.0) - } - MenuItem { - text: "Deprioritize" - checkable: true - checked: category === RoomType.Deprioritized - - onTriggered: category === RoomType.Deprioritized ? currentRoom.removeTag("m.lowpriority") : currentRoom.addTag("m.lowpriority", 1.0) - } - MenuSeparator {} - MenuItem { - text: "Mark as Read" - - onTriggered: currentRoom.markAllMessagesAsRead() - } - MenuItem { - text: "Leave Room" - - onTriggered: currentRoom.forget() - } + RoomListContextMenu {} } } @@ -917,42 +359,9 @@ Item { } } - Dialog { - property var currentRoom + Component { + id: acceptInvitationDialog - id: inviteDialog - parent: ApplicationWindow.overlay - - x: (window.width - width) / 2 - y: (window.height - height) / 2 - width: 360 - - title: "Action Required" - modal: true - - contentItem: Label { text: "Accept this invitation?" } - - footer: DialogButtonBox { - Button { - text: "Accept" - flat: true - - onClicked: currentRoom.acceptInvitation() - } - - Button { - text: "Reject" - flat: true - - onClicked: currentRoom.forget() - } - - Button { - text: "Cancel" - flat: true - - onClicked: inviteDialog.close() - } - } + AcceptInvitationDialog {} } } diff --git a/imports/Spectral/Panel/RoomPanel.qml b/imports/Spectral/Panel/RoomPanel.qml index 62e09b3..b44dfeb 100644 --- a/imports/Spectral/Panel/RoomPanel.qml +++ b/imports/Spectral/Panel/RoomPanel.qml @@ -23,27 +23,42 @@ Item { room: currentRoom } - RoomDrawer { - width: Math.min(root.width * 0.7, 480) - height: root.height - - id: roomDrawer - - room: currentRoom - } - - Label { + Column { anchors.centerIn: parent + + spacing: 16 + visible: !currentRoom - text: "Please choose a room." + + Image { + anchors.horizontalCenter: parent.horizontalCenter + + width: 240 + + fillMode: Image.PreserveAspectFit + + source: "qrc:/assets/img/matrix.svg" + } + + Label { + anchors.horizontalCenter: parent.horizontalCenter + + text: "Welcome to Matrix, a new era of instant messaging." + } + + Label { + anchors.horizontalCenter: parent.horizontalCenter + + text: "To start chatting, select a room from the room list." + } } Image { anchors.fill: parent - visible: currentRoom && MSettings.enableTimelineBackground + visible: currentRoom && MSettings.timelineBackground - source: MSettings.timelineBackground || MSettings.darkTheme ? "qrc:/assets/img/roompanel-dark.svg" : "qrc:/assets/img/roompanel.svg" + source: MSettings.timelineBackground fillMode: Image.PreserveAspectCrop } @@ -64,7 +79,7 @@ Item { topic: currentRoom ? (currentRoom.topic).replace(/(\r\n\t|\n|\r\t)/gm,"") : "" atTop: messageListView.atYBeginning - onClicked: roomDrawer.open() + onClicked: roomDrawer.visible ? roomDrawer.close() : roomDrawer.open() } ColumnLayout { @@ -92,15 +107,16 @@ Item { highlightMoveDuration: 500 boundsBehavior: Flickable.DragOverBounds - model: SortFilterProxyModel { id: sortedMessageEventModel sourceModel: messageEventModel - filters: ExpressionFilter { - expression: marks !== 0x10 && eventType !== "other" - } + filters: [ + ExpressionFilter { + expression: marks !== 0x10 && eventType !== "other" + } + ] onModelReset: { if (currentRoom) { @@ -182,8 +198,7 @@ Item { visible: section !== aboveSection || Math.abs(time - aboveTime) > 600000 } - MessageDelegate { - } + MessageDelegate {} } } @@ -200,8 +215,7 @@ Item { visible: section !== aboveSection || Math.abs(time - aboveTime) > 600000 } - MessageDelegate { - } + MessageDelegate {} } } @@ -244,24 +258,21 @@ Item { } } - RoundButton { - width: 64 - height: 64 - anchors.right: parent.right + Button { anchors.top: parent.top - - id: goBottomFab + anchors.horizontalCenter: parent.horizontalCenter visible: currentRoom && currentRoom.hasUnreadMessages - contentItem: MaterialIcon { - anchors.fill: parent + topPadding: 8 + bottomPadding: 8 + leftPadding: 24 + rightPadding: 24 - icon: "\ue316" - color: "white" - } + Material.foreground: MPalette.foreground + Material.background: MPalette.banner - Material.background: Material.accent + text: "Go to read marker" onClicked: goToEvent(currentRoom.readMarkerEventId) } @@ -287,85 +298,6 @@ Item { onClicked: messageListView.positionViewAtBeginning() } - - Popup { - property string sourceText - - anchors.centerIn: parent - width: 480 - - id: sourceDialog - - parent: ApplicationWindow.overlay - - padding: 16 - - closePolicy: Dialog.CloseOnEscape | Dialog.CloseOnPressOutside - - contentItem: ScrollView { - clip: true - TextArea { - readOnly: true - selectByMouse: true - - text: sourceDialog.sourceText - } - } - } - - Popup { - property alias listModel: readMarkerListView.model - - x: (window.width - width) / 2 - y: (window.height - height) / 2 - width: 320 - - id: readMarkerDialog - - parent: ApplicationWindow.overlay - - modal: true - padding: 16 - - closePolicy: Dialog.CloseOnEscape | Dialog.CloseOnPressOutside - - contentItem: AutoListView { - implicitHeight: Math.min(window.height - 64, - readMarkerListView.contentHeight) - - id: readMarkerListView - - clip: true - boundsBehavior: Flickable.DragOverBounds - - delegate: ItemDelegate { - width: parent.width - height: 48 - - RowLayout { - anchors.fill: parent - anchors.margins: 8 - spacing: 12 - - Avatar { - Layout.preferredWidth: height - Layout.fillHeight: true - - source: modelData.avatar - hint: modelData.displayName - } - - Label { - Layout.fillWidth: true - - text: modelData.displayName - } - } - } - - ScrollBar.vertical: ScrollBar {} - } - } } Control { diff --git a/imports/Spectral/Panel/qmldir b/imports/Spectral/Panel/qmldir index 3e9cc65..a36677f 100644 --- a/imports/Spectral/Panel/qmldir +++ b/imports/Spectral/Panel/qmldir @@ -1,3 +1,4 @@ module Spectral.Panel RoomPanel 2.0 RoomPanel.qml RoomListPanel 2.0 RoomListPanel.qml +RoomDrawer 2.0 RoomDrawer.qml diff --git a/imports/Spectral/Setting/Setting.qml b/imports/Spectral/Setting/Setting.qml index fb7a966..f5c0e75 100644 --- a/imports/Spectral/Setting/Setting.qml +++ b/imports/Spectral/Setting/Setting.qml @@ -5,11 +5,11 @@ import Qt.labs.settings 1.0 Settings { property bool showNotification: true - property bool pressAndHold property bool showTray: true property bool darkTheme - property bool enableTimelineBackground: true property string timelineBackground + + property string fontFamily: "Roboto,Noto Sans,Noto Color Emoji" } diff --git a/include/libqmatrixclient b/include/libqmatrixclient index b467b08..52a81df 160000 --- a/include/libqmatrixclient +++ b/include/libqmatrixclient @@ -1 +1 @@ -Subproject commit b467b0816f5f6816778f90b55a9d0b5437310fd5 +Subproject commit 52a81dfa8a5415be369d819837f445479b833cde diff --git a/qml/main.qml b/qml/main.qml index 29aa113..44bf774 100644 --- a/qml/main.qml +++ b/qml/main.qml @@ -7,19 +7,21 @@ import Qt.labs.platform 1.0 as Platform import Spectral.Panel 2.0 import Spectral.Component 2.0 -import Spectral.Page 2.0 +import Spectral.Dialog 2.0 import Spectral.Effect 2.0 import Spectral 0.1 import Spectral.Setting 0.1 ApplicationWindow { + readonly property bool inPortrait: window.width < window.height + Material.theme: MPalette.theme Material.background: MPalette.background width: 960 height: 640 - minimumWidth: 720 + minimumWidth: 480 minimumHeight: 360 id: window @@ -27,6 +29,8 @@ ApplicationWindow { visible: true title: qsTr("Spectral") + font.family: MSettings.fontFamily + background: Rectangle { color: MSettings.darkTheme ? "#303030" : "#FFFFFF" } @@ -37,16 +41,14 @@ ApplicationWindow { menu: Platform.Menu { Platform.MenuItem { - text: qsTr("Hide Window") - onTriggered: hideWindow() + text: qsTr("Toggle Window") + onTriggered: window.visible ? hideWindow() : showWindow() } Platform.MenuItem { text: qsTr("Quit") onTriggered: Qt.quit() } } - - onActivated: showWindow() } Controller { @@ -59,137 +61,105 @@ ApplicationWindow { roomForm.goToEvent(eventId) showWindow() } - onErrorOccured: { - roomListForm.errorControl.error = error - roomListForm.errorControl.detail = detail - roomListForm.errorControl.visible = true - } - onSyncDone: roomListForm.errorControl.visible = false + onErrorOccured: errorControl.show(error + ": " + detail, 3000) } Shortcut { - sequence: StandardKey.Quit + sequence: "Ctrl+Q" + context: Qt.ApplicationShortcut onActivated: Qt.quit() } - Dialog { - property bool busy: false - - width: 360 - x: (window.width - width) / 2 - y: (window.height - height) / 2 - - id: loginDialog + ToolTip { + id: errorControl parent: ApplicationWindow.overlay - title: "Login" - - contentItem: Column { - AutoTextField { - width: parent.width - - id: serverField - - placeholderText: "Server Address" - text: "https://matrix.org" - } - - AutoTextField { - width: parent.width - - id: usernameField - - placeholderText: "Username" - } - - AutoTextField { - width: parent.width - - id: passwordField - - placeholderText: "Password" - echoMode: TextInput.Password - } - } - - footer: DialogButtonBox { - Button { - text: "OK" - flat: true - enabled: !loginDialog.busy - - onClicked: loginDialog.doLogin() - } - - Button { - text: "Cancel" - flat: true - enabled: !loginDialog.busy - - onClicked: loginDialog.close() - } - - ToolTip { - id: loginButtonTooltip - - } - } - - onVisibleChanged: { - if (visible) spectralController.onErrorOccured.connect(showError) - else spectralController.onErrorOccured.disconnect(showError) - } - - function showError(error, detail) { - loginDialog.busy = false - loginButtonTooltip.text = error + ": " + detail - loginButtonTooltip.open() - } - - function doLogin() { - if (!(serverField.text.startsWith("http") && serverField.text.includes("://"))) { - loginButtonTooltip.text = "Server address should start with http(s)://" - loginButtonTooltip.open() - return - } - - loginDialog.busy = true - spectralController.loginWithCredentials(serverField.text, usernameField.text, passwordField.text) - - spectralController.connectionAdded.connect(function(conn) { - busy = false - loginDialog.close() - }) - } + font.pixelSize: 14 } - SplitView { - anchors.fill: parent + Component { + id: accountDetailDialog + + AccountDetailDialog {} + } + + Component { + id: loginDialog + + LoginDialog {} + } + + Component { + id: joinRoomDialog + + JoinRoomDialog {} + } + + Component { + id: createRoomDialog + + CreateRoomDialog {} + } + + Component { + id: fontFamilyDialog + + FontFamilyDialog {} + } + + Component { + id: chatBackgroundDialog + + OpenFileDialog {} + } + + Drawer { + width: Math.min((inPortrait ? 0.67 : 0.3) * window.width, 360) + height: window.height + modal: inPortrait + interactive: inPortrait + position: inPortrait ? 0 : 1 + visible: !inPortrait + + id: roomListDrawer RoomListPanel { - width: window.width * 0.35 - Layout.minimumWidth: 180 + anchors.fill: parent id: roomListForm clip: true - controller: spectralController + connection: spectralController.connection onLeaveRoom: roomForm.saveReadMarker(room) } + } - RoomPanel { - Layout.fillWidth: true - Layout.minimumWidth: 480 + RoomPanel { + anchors.fill: parent + anchors.leftMargin: !inPortrait ? roomListDrawer.width : undefined + anchors.rightMargin: !inPortrait && roomDrawer.visible ? roomDrawer.width : undefined - id: roomForm + id: roomForm - clip: true + clip: true - currentRoom: roomListForm.enteredRoom - } + currentRoom: roomListForm.enteredRoom + } + + RoomDrawer { + width: Math.min((inPortrait ? 0.67 : 0.3) * window.width, 360) + height: window.height + modal: inPortrait + interactive: inPortrait + + edge: Qt.RightEdge + + id: roomDrawer + + room: roomListForm.enteredRoom } Binding { @@ -208,9 +178,9 @@ ApplicationWindow { window.hide() } - Component.onCompleted: { - spectralController.initiated.connect(function() { - if (spectralController.accountCount == 0) loginDialog.open() - }) - } + Component.onCompleted: { + spectralController.initiated.connect(function() { + if (spectralController.accountCount == 0) loginDialog.createObject(window).open() + }) + } } diff --git a/qtquickcontrols2.conf b/qtquickcontrols2.conf index 6db2aaf..ecaa6b9 100644 --- a/qtquickcontrols2.conf +++ b/qtquickcontrols2.conf @@ -10,4 +10,3 @@ Theme=Light Variant=Dense Primary=#344955 Accent=#673AB7 -Font/Family="Roboto,Noto Sans,Noto Color Emoji" diff --git a/res.qrc b/res.qrc index d5d29dd..bec5bb8 100644 --- a/res.qrc +++ b/res.qrc @@ -13,7 +13,6 @@ imports/Spectral/Component/qmldir imports/Spectral/Effect/ElevationEffect.qml imports/Spectral/Effect/qmldir - imports/Spectral/Page/qmldir assets/font/material.ttf assets/img/icon.icns assets/img/icon.ico @@ -31,17 +30,31 @@ imports/Spectral/Component/AutoListView.qml imports/Spectral/Component/AutoTextField.qml imports/Spectral/Panel/RoomPanelInput.qml - imports/Spectral/Component/SplitView.qml - imports/Spectral/Font/CommonFont.qml imports/Spectral/Component/Timeline/SectionDelegate.qml - assets/img/roompanel.svg assets/img/matrix.svg imports/Spectral/Effect/RippleEffect.qml imports/Spectral/Effect/CircleMask.qml - assets/img/roompanel-dark.svg imports/Spectral/Component/Timeline/ImageDelegate.qml imports/Spectral/Component/Avatar.qml imports/Spectral/Setting/Palette.qml imports/Spectral/Component/Timeline/FileDelegate.qml + imports/Spectral/Component/FullScreenImage.qml + imports/Spectral/Dialog/qmldir + imports/Spectral/Dialog/RoomSettingsDialog.qml + imports/Spectral/Dialog/UserDetailDialog.qml + imports/Spectral/Dialog/MessageSourceDialog.qml + imports/Spectral/Dialog/LoginDialog.qml + imports/Spectral/Dialog/CreateRoomDialog.qml + imports/Spectral/Dialog/JoinRoomDialog.qml + imports/Spectral/Dialog/InviteUserDialog.qml + imports/Spectral/Dialog/AcceptInvitationDialog.qml + imports/Spectral/Menu/qmldir + imports/Spectral/Menu/RoomListContextMenu.qml + imports/Spectral/Menu/Timeline/qmldir + imports/Spectral/Menu/Timeline/MessageDelegateContextMenu.qml + imports/Spectral/Menu/Timeline/FileDelegateContextMenu.qml + imports/Spectral/Dialog/FontFamilyDialog.qml + imports/Spectral/Dialog/AccountDetailDialog.qml + imports/Spectral/Dialog/OpenFileDialog.qml diff --git a/screenshots/1.png b/screenshots/1.png index 76bda81..0561585 100644 Binary files a/screenshots/1.png and b/screenshots/1.png differ diff --git a/screenshots/2.png b/screenshots/2.png index 87ea523..de2514d 100644 Binary files a/screenshots/2.png and b/screenshots/2.png differ diff --git a/screenshots/3.png b/screenshots/3.png index 005629a..a8ec25a 100644 Binary files a/screenshots/3.png and b/screenshots/3.png differ diff --git a/screenshots/4.png b/screenshots/4.png index 13c48c5..b255e70 100644 Binary files a/screenshots/4.png and b/screenshots/4.png differ diff --git a/spectral.pro b/spectral.pro index bb01f3c..ec9cffb 100644 --- a/spectral.pro +++ b/spectral.pro @@ -20,9 +20,6 @@ isEmpty(USE_SYSTEM_SORTFILTERPROXYMODEL) { isEmpty(USE_SYSTEM_QMATRIXCLIENT) { USE_SYSTEM_QMATRIXCLIENT = false } -isEmpty(BUNDLE_FONT) { - BUNDLE_FONT = false -} $$USE_SYSTEM_QMATRIXCLIENT { PKGCONFIG += QMatrixClient @@ -70,13 +67,6 @@ DEFINES += QT_DEPRECATED_WARNINGS #DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000 # disables all the APIs deprecated before Qt 6.0.0 RESOURCES += res.qrc -$$BUNDLE_FONT { - message("Bundling fonts.") - DEFINES += BUNDLE_FONT - RESOURCES += font.qrc -} else { - message("Using fonts from operating system.") -} # Additional import path used to resolve QML modules in Qt Creator's code model QML_IMPORT_PATH += imports/ diff --git a/src/controller.cpp b/src/controller.cpp index c600a4a..d305f38 100644 --- a/src/controller.cpp +++ b/src/controller.cpp @@ -7,6 +7,7 @@ #include "events/eventcontent.h" #include "events/roommessageevent.h" +#include "csapi/account-data.h" #include "csapi/joining.h" #include "csapi/logout.h" @@ -15,6 +16,7 @@ #include #include #include +#include #include #include #include @@ -57,19 +59,25 @@ inline QString accessTokenFileName(const AccountSettings& account) { '/' + fileName; } -void Controller::loginWithCredentials(QString serverAddr, QString user, +void Controller::loginWithCredentials(QString serverAddr, + QString user, QString pass) { if (!user.isEmpty() && !pass.isEmpty()) { + QString deviceName = "Spectral " + QSysInfo::machineHostName() + " " + + QSysInfo::productType() + " " + + QSysInfo::productVersion() + " " + + QSysInfo::currentCpuArchitecture(); + Connection* conn = new Connection(this); conn->setHomeserver(QUrl(serverAddr)); - conn->connectToServer(user, pass, ""); + conn->connectToServer(user, pass, deviceName, ""); connect(conn, &Connection::connected, [=] { AccountSettings account(conn->userId()); account.setKeepLoggedIn(true); account.clearAccessToken(); // Drop the legacy - just in case account.setHomeserver(conn->homeserver()); account.setDeviceId(conn->deviceId()); - account.setDeviceName("Spectral"); + account.setDeviceName(deviceName); if (!saveAccessToken(account, conn->accessToken())) qWarning() << "Couldn't save access token"; account.sync(); @@ -100,7 +108,8 @@ void Controller::logout(Connection* conn) { conn->stopSync(); emit conn->stateChanged(); emit conn->loggedOut(); - if (!m_connections.isEmpty()) setConnection(m_connections[0]); + if (!m_connections.isEmpty()) + setConnection(m_connections[0]); }); connect(job, &LogoutJob::failure, this, [=] { emit errorOccured("Server-side Logout Failed", job->errorString()); @@ -160,14 +169,16 @@ void Controller::invokeLogin() { c->connectWithToken(account.userId(), accessToken, account.deviceId()); } } - if (!m_connections.isEmpty()) setConnection(m_connections[0]); + if (!m_connections.isEmpty()) + setConnection(m_connections[0]); emit initiated(); } QByteArray Controller::loadAccessToken(const AccountSettings& account) { QFile accountTokenFile{accessTokenFileName(account)}; if (accountTokenFile.open(QFile::ReadOnly)) { - if (accountTokenFile.size() < 1024) return accountTokenFile.readAll(); + if (accountTokenFile.size() < 1024) + return accountTokenFile.readAll(); qWarning() << "File" << accountTokenFile.fileName() << "is" << accountTokenFile.size() @@ -203,7 +214,8 @@ void Controller::joinRoom(Connection* c, const QString& alias) { }); } -void Controller::createRoom(Connection* c, const QString& name, +void Controller::createRoom(Connection* c, + const QString& name, const QString& topic) { CreateRoomJob* createRoomJob = c->createRoom(Connection::PublishRoom, "", name, topic, QStringList()); @@ -231,10 +243,12 @@ void Controller::playAudio(QUrl localFile) { connect(player, &QMediaPlayer::stateChanged, [=] { player->deleteLater(); }); } -void Controller::postNotification(const QString& roomId, const QString& eventId, +void Controller::postNotification(const QString& roomId, + const QString& eventId, const QString& roomName, const QString& senderName, - const QString& text, const QImage& icon) { + const QString& text, + const QImage& icon) { notificationsManager.postNotification(roomId, eventId, roomName, senderName, text, icon); } diff --git a/src/controller.h b/src/controller.h index 32fc031..2e73c21 100644 --- a/src/controller.h +++ b/src/controller.h @@ -3,6 +3,7 @@ #include "connection.h" #include "notifications/manager.h" +#include "room.h" #include "settings.h" #include "user.h" @@ -53,13 +54,14 @@ class Controller : public QObject { } Connection* connection() { - if (m_connection.isNull()) return nullptr; + if (m_connection.isNull()) + return nullptr; return m_connection; } void setConnection(Connection* conn) { - if (!conn) return; - if (conn == m_connection) return; + if (conn == m_connection) + return; m_connection = conn; emit connectionChanged(); } @@ -99,9 +101,12 @@ class Controller : public QObject { void createDirectChat(Connection* c, const QString& userID); void copyToClipboard(const QString& text); void playAudio(QUrl localFile); - void postNotification(const QString& roomId, const QString& eventId, - const QString& roomName, const QString& senderName, - const QString& text, const QImage& icon); + void postNotification(const QString& roomId, + const QString& eventId, + const QString& roomName, + const QString& senderName, + const QString& text, + const QImage& icon); }; #endif // CONTROLLER_H diff --git a/src/imageprovider.cpp b/src/imageprovider.cpp index c092b3e..be98de2 100644 --- a/src/imageprovider.cpp +++ b/src/imageprovider.cpp @@ -1,16 +1,26 @@ #include "imageprovider.h" +#include +#include +#include #include #include using QMatrixClient::BaseJob; ThumbnailResponse::ThumbnailResponse(QMatrixClient::Connection* c, - QString id, const QSize& size) - : c(c), - mediaId(std::move(id)), - requestedSize(size), - errorStr("Image request hasn't started") { + QString id, + const QSize& size) + : c(c), + mediaId(std::move(id)), + requestedSize(size), + localFile(QStringLiteral("%1/image_provider/%2-%3x%4.png") + .arg(QStandardPaths::writableLocation( + QStandardPaths::CacheLocation), + mediaId, + QString::number(requestedSize.width()), + QString::number(requestedSize.height()))), + errorStr("Image request hasn't started") { if (requestedSize.isEmpty()) { errorStr.clear(); emit finished(); @@ -18,11 +28,19 @@ ThumbnailResponse::ThumbnailResponse(QMatrixClient::Connection* c, } if (mediaId.count('/') != 1) { errorStr = - tr("Media id '%1' doesn't follow server/mediaId pattern") - .arg(mediaId); + tr("Media id '%1' doesn't follow server/mediaId pattern").arg(mediaId); emit finished(); return; } + + QImage cachedImage; + if (cachedImage.load(localFile)) { + image = cachedImage; + errorStr.clear(); + emit finished(); + return; + } + // Execute a request on the main thread asynchronously moveToThread(c->thread()); QMetaObject::invokeMethod(this, &ThumbnailResponse::startRequest, @@ -45,6 +63,14 @@ void ThumbnailResponse::prepareResult() { QWriteLocker _(&lock); if (job->error() == BaseJob::Success) { image = job->thumbnail(); + + QString localPath = QFileInfo(localFile).absolutePath(); + QDir dir; + if (!dir.exists(localPath)) + dir.mkpath(localPath); + + image.save(localFile); + errorStr.clear(); } else if (job->error() == BaseJob::Abandoned) { errorStr = tr("Image request has been cancelled"); @@ -83,7 +109,7 @@ void ThumbnailResponse::cancel() { } QQuickImageResponse* ImageProvider::requestImageResponse( - const QString& id, const QSize& requestedSize) { - qDebug() << "ImageProvider: requesting " << id << "of size" << requestedSize; + const QString& id, + const QSize& requestedSize) { return new ThumbnailResponse(m_connection.load(), id, requestedSize); } diff --git a/src/imageprovider.h b/src/imageprovider.h index 4d15b04..ed31e94 100644 --- a/src/imageprovider.h +++ b/src/imageprovider.h @@ -7,8 +7,8 @@ #include #include -#include #include +#include namespace QMatrixClient { class Connection; @@ -17,11 +17,12 @@ class Connection; class ThumbnailResponse : public QQuickImageResponse { Q_OBJECT public: - ThumbnailResponse(QMatrixClient::Connection* c, QString mediaId, + ThumbnailResponse(QMatrixClient::Connection* c, + QString mediaId, const QSize& requestedSize); ~ThumbnailResponse() override = default; -private slots: + private slots: void startRequest(); void prepareResult(); void doCancel(); @@ -30,11 +31,12 @@ private slots: QMatrixClient::Connection* c; const QString mediaId; const QSize requestedSize; + const QString localFile; QMatrixClient::MediaThumbnailJob* job = nullptr; QImage image; QString errorStr; - mutable QReadWriteLock lock; // Guards ONLY these two members above + mutable QReadWriteLock lock; // Guards ONLY these two members above QQuickTextureFactory* textureFactory() const override; QString errorString() const override; @@ -49,7 +51,8 @@ class ImageProvider : public QObject, public QQuickAsyncImageProvider { explicit ImageProvider() = default; QQuickImageResponse* requestImageResponse( - const QString& id, const QSize& requestedSize) override; + const QString& id, + const QSize& requestedSize) override; QMatrixClient::Connection* connection() { return m_connection; } void setConnection(QMatrixClient::Connection* connection) { diff --git a/src/main.cpp b/src/main.cpp index ca17cf3..0f57107 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -23,7 +23,7 @@ using namespace QMatrixClient; -int main(int argc, char *argv[]) { +int main(int argc, char* argv[]) { #if defined(Q_OS_LINUX) || defined(Q_OS_WIN) || defined(Q_OS_FREEBSD) if (qgetenv("QT_SCALE_FACTOR").size() == 0) { QSettings settings("ENCOM", "Spectral"); @@ -36,6 +36,8 @@ int main(int argc, char *argv[]) { } #endif + QCoreApplication::setAttribute(Qt::AA_UseOpenGLES); + QNetworkProxyFactory::setUseSystemConfiguration(true); QApplication app(argc, argv); @@ -57,26 +59,24 @@ int main(int argc, char *argv[]) { "RoomMessageEvent", "ENUM"); qmlRegisterUncreatableType("Spectral", 0, 1, "RoomType", "ENUM"); - qRegisterMetaType("User*"); - qRegisterMetaType("Room*"); + qRegisterMetaType("User*"); + qRegisterMetaType("const User*"); + qRegisterMetaType("Room*"); + qRegisterMetaType("Connection*"); qRegisterMetaType("MessageEventType"); - qRegisterMetaType("SpectralRoom*"); - qRegisterMetaType("SpectralUser*"); - -#if defined(BUNDLE_FONT) - QFontDatabase::addApplicationFont(":/assets/font/roboto.ttf"); - QFontDatabase::addApplicationFont(":/assets/font/twemoji.ttf"); -#endif + qRegisterMetaType("SpectralRoom*"); + qRegisterMetaType("SpectralUser*"); QQmlApplicationEngine engine; engine.addImportPath("qrc:/imports"); - ImageProvider *m_provider = new ImageProvider(); + ImageProvider* m_provider = new ImageProvider(); engine.rootContext()->setContextProperty("imageProvider", m_provider); engine.addImageProvider(QLatin1String("mxc"), m_provider); engine.load(QUrl(QStringLiteral("qrc:/qml/main.qml"))); - if (engine.rootObjects().isEmpty()) return -1; + if (engine.rootObjects().isEmpty()) + return -1; return app.exec(); } diff --git a/src/messageeventmodel.cpp b/src/messageeventmodel.cpp index d5e9799..40c15d4 100644 --- a/src/messageeventmodel.cpp +++ b/src/messageeventmodel.cpp @@ -41,7 +41,7 @@ QHash MessageEventModel::roleNames() const { return roles; } -MessageEventModel::MessageEventModel(QObject *parent) +MessageEventModel::MessageEventModel(QObject* parent) : QAbstractListModel(parent), m_currentRoom(nullptr) { using namespace QMatrixClient; qmlRegisterType(); @@ -52,8 +52,9 @@ MessageEventModel::MessageEventModel(QObject *parent) MessageEventModel::~MessageEventModel() {} -void MessageEventModel::setRoom(SpectralRoom *room) { - if (room == m_currentRoom) return; +void MessageEventModel::setRoom(SpectralRoom* room) { + if (room == m_currentRoom) + return; beginResetModel(); if (m_currentRoom) { @@ -96,8 +97,9 @@ void MessageEventModel::setRoom(SpectralRoom *room) { connect(m_currentRoom, &Room::pendingEventAdded, this, &MessageEventModel::endInsertRows); connect(m_currentRoom, &Room::pendingEventAboutToMerge, this, - [this](RoomEvent *, int i) { - if (i == 0) return; // No need to move anything, just refresh + [this](RoomEvent*, int i) { + if (i == 0) + return; // No need to move anything, just refresh movingEvent = true; // Reverse i because row 0 is bottommost in the model @@ -131,7 +133,7 @@ void MessageEventModel::setRoom(SpectralRoom *room) { refreshEventRoles(lastReadEventId, {ReadMarkerRole}); }); connect(m_currentRoom, &Room::replacedEvent, this, - [this](const RoomEvent *newEvent) { + [this](const RoomEvent* newEvent) { refreshLastUserEvents(refreshEvent(newEvent->id()) - timelineBaseIndex()); }); @@ -144,10 +146,15 @@ void MessageEventModel::setRoom(SpectralRoom *room) { connect(m_currentRoom, &Room::fileTransferCancelled, this, &MessageEventModel::refreshEvent); connect(m_currentRoom, &Room::readMarkerForUserMoved, this, - [=](User *, QString fromEventId, QString toEventId) { + [=](User*, QString fromEventId, QString toEventId) { refreshEventRoles(fromEventId, {UserMarkerRole}); refreshEventRoles(toEventId, {UserMarkerRole}); }); + connect(m_currentRoom->connection(), &Connection::ignoredUsersListChanged, + this, [=] { + beginResetModel(); + endResetModel(); + }); qDebug() << "Connected to room" << room->id() << "as" << room->localUser()->id(); } else @@ -155,23 +162,25 @@ void MessageEventModel::setRoom(SpectralRoom *room) { endResetModel(); } -int MessageEventModel::refreshEvent(const QString &eventId) { +int MessageEventModel::refreshEvent(const QString& eventId) { return refreshEventRoles(eventId); } -void MessageEventModel::refreshRow(int row) { refreshEventRoles(row); } +void MessageEventModel::refreshRow(int row) { + refreshEventRoles(row); +} int MessageEventModel::timelineBaseIndex() const { return m_currentRoom ? int(m_currentRoom->pendingEvents().size()) : 0; } -void MessageEventModel::refreshEventRoles(int row, const QVector &roles) { +void MessageEventModel::refreshEventRoles(int row, const QVector& roles) { const auto idx = index(row); emit dataChanged(idx, idx, roles); } -int MessageEventModel::refreshEventRoles(const QString &eventId, - const QVector &roles) { +int MessageEventModel::refreshEventRoles(const QString& eventId, + const QVector& roles) { const auto it = m_currentRoom->findInTimeline(eventId); if (it == m_currentRoom->timelineEdge()) { qWarning() << "Trying to refresh inexistent event:" << eventId; @@ -183,15 +192,16 @@ int MessageEventModel::refreshEventRoles(const QString &eventId, return row; } -inline bool hasValidTimestamp(const QMatrixClient::TimelineItem &ti) { +inline bool hasValidTimestamp(const QMatrixClient::TimelineItem& ti) { return ti->timestamp().isValid(); } QDateTime MessageEventModel::makeMessageTimestamp( - const QMatrixClient::Room::rev_iter_t &baseIt) const { - const auto &timeline = m_currentRoom->messageEvents(); + const QMatrixClient::Room::rev_iter_t& baseIt) const { + const auto& timeline = m_currentRoom->messageEvents(); auto ts = baseIt->event()->timestamp(); - if (ts.isValid()) return ts; + if (ts.isValid()) + return ts; // The event is most likely redacted or just invalid. // Look for the nearest date around and slap zero time to it. @@ -210,11 +220,14 @@ QDateTime MessageEventModel::makeMessageTimestamp( QString MessageEventModel::renderDate(QDateTime timestamp) const { auto date = timestamp.toLocalTime().date(); - if (date == QDate::currentDate()) return tr("Today"); - if (date == QDate::currentDate().addDays(-1)) return tr("Yesterday"); + if (date == QDate::currentDate()) + return tr("Today"); + if (date == QDate::currentDate().addDays(-1)) + return tr("Yesterday"); if (date == QDate::currentDate().addDays(-2)) return tr("The day before yesterday"); - if (date > QDate::currentDate().addDays(-7)) return date.toString("dddd"); + if (date > QDate::currentDate().addDays(-7)) + return date.toString("dddd"); return date.toString(Qt::DefaultLocaleShortDate); } @@ -222,8 +235,8 @@ void MessageEventModel::refreshLastUserEvents(int baseTimelineRow) { if (!m_currentRoom || m_currentRoom->timelineSize() <= baseTimelineRow) return; - const auto &timelineBottom = m_currentRoom->messageEvents().rbegin(); - const auto &lastSender = (*(timelineBottom + baseTimelineRow))->senderId(); + const auto& timelineBottom = m_currentRoom->messageEvents().rbegin(); + const auto& lastSender = (*(timelineBottom + baseTimelineRow))->senderId(); const auto limit = timelineBottom + std::min(baseTimelineRow + 10, m_currentRoom->timelineSize()); for (auto it = timelineBottom + std::max(baseTimelineRow - 10, 0); @@ -235,12 +248,13 @@ void MessageEventModel::refreshLastUserEvents(int baseTimelineRow) { } } -int MessageEventModel::rowCount(const QModelIndex &parent) const { - if (!m_currentRoom || parent.isValid()) return 0; +int MessageEventModel::rowCount(const QModelIndex& parent) const { + if (!m_currentRoom || parent.isValid()) + return 0; return m_currentRoom->timelineSize(); } -QVariant MessageEventModel::data(const QModelIndex &idx, int role) const { +QVariant MessageEventModel::data(const QModelIndex& idx, int role) const { const auto row = idx.row(); if (!m_currentRoom || row < 0 || @@ -253,14 +267,14 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const { std::max(0, row - timelineBaseIndex()); const auto pendingIt = m_currentRoom->pendingEvents().crbegin() + std::min(row, timelineBaseIndex()); - const auto &evt = isPending ? **pendingIt : **timelineIt; + const auto& evt = isPending ? **pendingIt : **timelineIt; if (role == Qt::DisplayRole) { - return utils::removeReply(utils::eventToString(evt, m_currentRoom, Qt::RichText)); + return utils::removeReply(m_currentRoom->eventToString(evt, Qt::RichText)); } if (role == MessageRole) { - return utils::removeReply(utils::eventToString(evt, m_currentRoom)); + return utils::removeReply(m_currentRoom->eventToString(evt)); } if (role == Qt::ToolTipRole) { @@ -282,7 +296,8 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const { return e->hasFileContent() ? "file" : "message"; } } - if (evt.isStateEvent()) return "state"; + if (evt.isStateEvent()) + return "state"; return "other"; } @@ -298,7 +313,7 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const { if (role == ContentTypeRole) { if (auto e = eventCast(&evt)) { - const auto &contentType = e->mimeType().name(); + const auto& contentType = e->mimeType().name(); return contentType == "text/plain" ? QStringLiteral("text/html") : contentType; } @@ -323,19 +338,27 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const { }; } - if (role == HighlightRole) return m_currentRoom->isEventHighlighted(&evt); + if (role == HighlightRole) + return m_currentRoom->isEventHighlighted(&evt); if (role == ReadMarkerRole) return evt.id() == lastReadEventId && row > timelineBaseIndex(); if (role == SpecialMarksRole) { - if (isPending) return pendingIt->deliveryStatus(); + if (isPending) + return pendingIt->deliveryStatus(); - if (is(evt)) return EventStatus::Hidden; - if (evt.isRedacted()) return EventStatus::Hidden; + if (is(evt)) + return EventStatus::Hidden; + if (evt.isRedacted()) + return EventStatus::Hidden; if (evt.isStateEvent() && - static_cast(evt).repeatsState()) + static_cast(evt).repeatsState()) + return EventStatus::Hidden; + + if (m_currentRoom->connection()->isIgnored( + m_currentRoom->user(evt.senderId()))) return EventStatus::Hidden; return EventStatus::Normal; @@ -351,7 +374,8 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const { } if (role == AnnotationRole) - if (isPending) return pendingIt->annotation(); + if (isPending) + return pendingIt->annotation(); if (role == TimeRole || role == SectionRole) { auto ts = @@ -361,8 +385,9 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const { if (role == UserMarkerRole) { QVariantList variantList; - for (User *user : m_currentRoom->usersAtEventId(evt.id())) { - if (user == m_currentRoom->localUser()) continue; + for (User* user : m_currentRoom->usersAtEventId(evt.id())) { + if (user == m_currentRoom->localUser()) + continue; variantList.append(QVariant::fromValue(user)); } return variantList; @@ -370,22 +395,24 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const { if (role == ReplyEventIdRole || role == ReplyDisplayRole || role == ReplyAuthorRole) { - const QString &replyEventId = evt.contentJson()["m.relates_to"] + const QString& replyEventId = evt.contentJson()["m.relates_to"] .toObject()["m.in_reply_to"] .toObject()["event_id"] .toString(); - if (replyEventId.isEmpty()) return {}; + if (replyEventId.isEmpty()) + return {}; const auto replyIt = m_currentRoom->findInTimeline(replyEventId); - if (replyIt == m_currentRoom->timelineEdge()) return {}; + if (replyIt == m_currentRoom->timelineEdge()) + return {}; const auto& replyEvt = **replyIt; switch (role) { case ReplyEventIdRole: return replyEventId; case ReplyDisplayRole: - return utils::removeReply(utils::eventToString(replyEvt, m_currentRoom, Qt::RichText)); + return utils::removeReply( + m_currentRoom->eventToString(replyEvt, Qt::RichText)); case ReplyAuthorRole: - return QVariant::fromValue( - m_currentRoom->user(replyEvt.senderId())); + return QVariant::fromValue(m_currentRoom->user(replyEvt.senderId())); } return {}; } @@ -394,7 +421,8 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const { role == AboveAuthorRole || role == AboveTimeRole) for (auto r = row + 1; r < rowCount(); ++r) { auto i = index(r); - if (data(i, SpecialMarksRole) != EventStatus::Hidden) switch (role) { + if (data(i, SpecialMarksRole) != EventStatus::Hidden) + switch (role) { case AboveEventTypeRole: return data(i, EventTypeRole); case AboveSectionRole: @@ -409,7 +437,7 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const { return {}; } -int MessageEventModel::eventIDToIndex(const QString &eventID) { +int MessageEventModel::eventIDToIndex(const QString& eventID) { const auto it = m_currentRoom->findInTimeline(eventID); if (it == m_currentRoom->timelineEdge()) { qWarning() << "Trying to find inexistent event:" << eventID; diff --git a/src/roomlistmodel.cpp b/src/roomlistmodel.cpp index c1e5114..425aefd 100644 --- a/src/roomlistmodel.cpp +++ b/src/roomlistmodel.cpp @@ -16,8 +16,10 @@ RoomListModel::RoomListModel(QObject* parent) : QAbstractListModel(parent) {} RoomListModel::~RoomListModel() {} void RoomListModel::setConnection(Connection* connection) { - if (connection == m_connection) return; - if (m_connection) m_connection->disconnect(this); + if (connection == m_connection) + return; + if (m_connection) + m_connection->disconnect(this); if (!connection) { qDebug() << "Removing current connection..."; m_connection = nullptr; @@ -29,7 +31,8 @@ void RoomListModel::setConnection(Connection* connection) { m_connection = connection; - for (SpectralRoom* room : m_rooms) room->disconnect(this); + for (SpectralRoom* room : m_rooms) + room->disconnect(this); connect(connection, &Connection::connected, this, &RoomListModel::doResetModel); @@ -40,6 +43,12 @@ void RoomListModel::setConnection(Connection* connection) { connect(connection, &Connection::leftRoom, this, &RoomListModel::updateRoom); connect(connection, &Connection::aboutToDeleteRoom, this, &RoomListModel::deleteRoom); + connect(connection, &Connection::directChatsListChanged, this, + [=](Connection::DirectChatsMap additions, + Connection::DirectChatsMap removals) { + for (QString roomID : additions.values() + removals.values()) + refresh(static_cast(connection->room(roomID))); + }); doResetModel(); } @@ -47,11 +56,14 @@ void RoomListModel::setConnection(Connection* connection) { void RoomListModel::doResetModel() { beginResetModel(); m_rooms.clear(); - for (auto r : m_connection->roomMap()) doAddRoom(r); + for (auto r : m_connection->roomMap()) + doAddRoom(r); endResetModel(); } -SpectralRoom* RoomListModel::roomAt(int row) { return m_rooms.at(row); } +SpectralRoom* RoomListModel::roomAt(int row) { + return m_rooms.at(row); +} void RoomListModel::doAddRoom(Room* r) { if (auto* room = static_cast(r)) { @@ -77,16 +89,19 @@ void RoomListModel::connectRoomSignals(SpectralRoom* room) { connect(room, &Room::addedMessages, this, [=] { refresh(room, {LastEventRole}); }); connect(room, &Room::notificationCountChanged, this, [=] { - if (room->notificationCount() == 0) return; - if (room->timelineSize() == 0) return; - const RoomEvent* lastEvent = room->messageEvents().rbegin()->get(); - if (lastEvent->isStateEvent()) return; - User* sender = room->user(lastEvent->senderId()); - if (sender == room->localUser()) return; - emit newMessage( - room->id(), lastEvent->id(), room->displayName(), - sender->displayname(), utils::eventToString(*lastEvent), - room->avatar(128)); + if (room->notificationCount() == 0) + return; + if (room->timelineSize() == 0) + return; + const RoomEvent* lastEvent = room->messageEvents().rbegin()->get(); + if (lastEvent->isStateEvent()) + return; + User* sender = room->user(lastEvent->senderId()); + if (sender == room->localUser()) + return; + emit newMessage(room->id(), lastEvent->id(), room->displayName(), + sender->displayname(), room->eventToString(*lastEvent), + room->avatar(128)); }); } @@ -129,7 +144,8 @@ void RoomListModel::updateRoom(Room* room, Room* prev) { void RoomListModel::deleteRoom(Room* room) { qDebug() << "Deleting room" << room->id(); const auto it = std::find(m_rooms.begin(), m_rooms.end(), room); - if (it == m_rooms.end()) return; // Already deleted, nothing to do + if (it == m_rooms.end()) + return; // Already deleted, nothing to do qDebug() << "Erasing room" << room->id(); const int row = it - m_rooms.begin(); beginRemoveRows(QModelIndex(), row, row); @@ -138,34 +154,54 @@ void RoomListModel::deleteRoom(Room* room) { } int RoomListModel::rowCount(const QModelIndex& parent) const { - if (parent.isValid()) return 0; + if (parent.isValid()) + return 0; return m_rooms.count(); } QVariant RoomListModel::data(const QModelIndex& index, int role) const { - if (!index.isValid()) return QVariant(); + if (!index.isValid()) + return QVariant(); if (index.row() >= m_rooms.count()) { qDebug() << "UserListModel: something wrong here..."; return QVariant(); } SpectralRoom* room = m_rooms.at(index.row()); - if (role == NameRole) return room->displayName(); - if (role == AvatarRole) return room->avatarMediaId(); - if (role == TopicRole) return room->topic(); + if (role == NameRole) + return room->displayName(); + if (role == AvatarRole) + return room->avatarMediaId(); + if (role == TopicRole) + return room->topic(); if (role == CategoryRole) { - if (room->joinState() == JoinState::Invite) return RoomType::Invited; - if (room->isFavourite()) return RoomType::Favorite; - if (room->isDirectChat()) return RoomType::Direct; - if (room->isLowPriority()) return RoomType::Deprioritized; + if (room->joinState() == JoinState::Invite) + return RoomType::Invited; + if (room->isFavourite()) + return RoomType::Favorite; + if (room->isDirectChat()) + return RoomType::Direct; + if (room->isLowPriority()) + return RoomType::Deprioritized; return RoomType::Normal; } - if (role == UnreadCountRole) return room->unreadCount(); - if (role == NotificationCountRole) return room->notificationCount(); - if (role == HighlightCountRole) return room->highlightCount(); - if (role == LastEventRole) return room->lastEvent(); - if (role == LastActiveTimeRole) return room->lastActiveTime(); - if (role == CurrentRoomRole) return QVariant::fromValue(room); + if (role == UnreadCountRole) + return room->unreadCount(); + if (role == NotificationCountRole) + return room->notificationCount(); + if (role == HighlightCountRole) + return room->highlightCount(); + if (role == LastEventRole) + return room->lastEvent(); + if (role == LastActiveTimeRole) + return room->lastActiveTime(); + if (role == JoinStateRole) { + if (!room->successorId().isEmpty()) + return QStringLiteral("upgraded"); + return toCString(room->joinState()); + } + if (role == CurrentRoomRole) + return QVariant::fromValue(room); return QVariant(); } @@ -200,6 +236,7 @@ QHash RoomListModel::roleNames() const { roles[HighlightCountRole] = "highlightCount"; roles[LastEventRole] = "lastEvent"; roles[LastActiveTimeRole] = "lastActiveTime"; + roles[JoinStateRole] = "joinState"; roles[CurrentRoomRole] = "currentRoom"; return roles; } diff --git a/src/roomlistmodel.h b/src/roomlistmodel.h index d8c56d6..238b372 100644 --- a/src/roomlistmodel.h +++ b/src/roomlistmodel.h @@ -17,8 +17,8 @@ class RoomType : public QObject { enum Types { Invited = 1, Favorite, - Normal, Direct, + Normal, Deprioritized, }; REGISTER_ENUM(Types) @@ -39,6 +39,7 @@ class RoomListModel : public QAbstractListModel { HighlightCountRole, LastEventRole, LastActiveTimeRole, + JoinStateRole, CurrentRoomRole, }; @@ -75,9 +76,12 @@ class RoomListModel : public QAbstractListModel { signals: void connectionChanged(); void roomAdded(SpectralRoom* room); - void newMessage(const QString& roomId, const QString& eventId, - const QString& roomName, const QString& senderName, - const QString& text, const QImage& icon); + void newMessage(const QString& roomId, + const QString& eventId, + const QString& roomName, + const QString& senderName, + const QString& text, + const QImage& icon); }; #endif // ROOMLISTMODEL_H diff --git a/src/spectralroom.cpp b/src/spectralroom.cpp index 2dd1060..259afaa 100644 --- a/src/spectralroom.cpp +++ b/src/spectralroom.cpp @@ -6,6 +6,7 @@ #include "csapi/content-repo.h" #include "csapi/leaving.h" #include "csapi/typing.h" +#include "events/accountdataevents.h" #include "events/typingevent.h" #include @@ -18,7 +19,8 @@ #include "utils.h" -SpectralRoom::SpectralRoom(Connection* connection, QString roomId, +SpectralRoom::SpectralRoom(Connection* connection, + QString roomId, JoinState joinState) : Room(connection, std::move(roomId), joinState) { connect(this, &SpectralRoom::notificationCountChanged, this, @@ -70,21 +72,21 @@ void SpectralRoom::chooseAndUploadFile() { } } -void SpectralRoom::saveFileAs(QString eventId) { - auto fileName = QFileDialog::getSaveFileName(Q_NULLPTR, tr("Save File as"), - fileNameToDownload(eventId)); - if (!fileName.isEmpty()) downloadFile(eventId, QUrl::fromLocalFile(fileName)); +void SpectralRoom::acceptInvitation() { + connection()->joinRoom(id()); } -void SpectralRoom::acceptInvitation() { connection()->joinRoom(id()); } - -void SpectralRoom::forget() { connection()->forgetRoom(id()); } +void SpectralRoom::forget() { + connection()->forgetRoom(id()); +} bool SpectralRoom::hasUsersTyping() { QList users = usersTyping(); - if (users.isEmpty()) return false; + if (users.isEmpty()) + return false; int count = users.length(); - if (users.contains(localUser())) count--; + if (users.contains(localUser())) + count--; return count != 0; } @@ -104,10 +106,11 @@ void SpectralRoom::sendTypingNotification(bool isTyping) { } QString SpectralRoom::lastEvent() { - if (timelineSize() == 0) return ""; + if (timelineSize() == 0) + return ""; const RoomEvent* lastEvent = messageEvents().rbegin()->get(); return user(lastEvent->senderId())->displayname() + ": " + - utils::removeReply(utils::eventToString(*lastEvent, this)); + utils::removeReply(eventToString(*lastEvent)); } bool SpectralRoom::isEventHighlighted(const RoomEvent* e) const { @@ -116,7 +119,8 @@ bool SpectralRoom::isEventHighlighted(const RoomEvent* e) const { void SpectralRoom::checkForHighlights(const QMatrixClient::TimelineItem& ti) { auto localUserId = localUser()->id(); - if (ti->senderId() == localUserId) return; + if (ti->senderId() == localUserId) + return; if (auto* e = ti.viewAs()) { const auto& text = e->plainBody(); if (text.contains(localUserId) || @@ -142,8 +146,10 @@ void SpectralRoom::countChanged() { } } -void SpectralRoom::sendReply(QString userId, QString eventId, - QString replyContent, QString sendContent) { +void SpectralRoom::sendReply(QString userId, + QString eventId, + QString replyContent, + QString sendContent) { QJsonObject json{ {"msgtype", "m.text"}, {"body", "> <" + userId + "> " + replyContent + "\n\n" + sendContent}, @@ -159,7 +165,8 @@ void SpectralRoom::sendReply(QString userId, QString eventId, } QDateTime SpectralRoom::lastActiveTime() { - if (timelineSize() == 0) return QDateTime(); + if (timelineSize() == 0) + return QDateTime(); return messageEvents().rbegin()->get()->timestamp(); } @@ -205,15 +212,19 @@ QVariantList SpectralRoom::getUsers(const QString& prefix) { } QString SpectralRoom::postMarkdownText(const QString& markdown) { - unsigned char *sequence = (unsigned char *) qstrdup(markdown.toUtf8().constData()); - qint64 length = strlen((char *) sequence); + unsigned char* sequence = + (unsigned char*)qstrdup(markdown.toUtf8().constData()); + qint64 length = strlen((char*)sequence); - hoedown_renderer* renderer = hoedown_html_renderer_new(HOEDOWN_HTML_USE_XHTML, 32); - hoedown_extensions extensions = (hoedown_extensions) ((HOEDOWN_EXT_BLOCK | HOEDOWN_EXT_SPAN | HOEDOWN_EXT_MATH_EXPLICIT) & ~HOEDOWN_EXT_QUOTE); + hoedown_renderer* renderer = + hoedown_html_renderer_new(HOEDOWN_HTML_USE_XHTML, 32); + hoedown_extensions extensions = (hoedown_extensions)( + (HOEDOWN_EXT_BLOCK | HOEDOWN_EXT_SPAN | HOEDOWN_EXT_MATH_EXPLICIT) & + ~HOEDOWN_EXT_QUOTE); hoedown_document* document = hoedown_document_new(renderer, extensions, 32); hoedown_buffer* html = hoedown_buffer_new(length); hoedown_document_render(document, html, sequence, length); - QString result = QString::fromUtf8((char *) html->data, html->size); + QString result = QString::fromUtf8((char*)html->data, html->size); free(sequence); hoedown_buffer_free(html); diff --git a/src/spectralroom.h b/src/spectralroom.h index 68722dc..29cea77 100644 --- a/src/spectralroom.h +++ b/src/spectralroom.h @@ -8,6 +8,13 @@ #include #include +#include +#include +#include +#include +#include +#include + using namespace QMatrixClient; class SpectralRoom : public Room { @@ -23,7 +30,8 @@ class SpectralRoom : public Room { Q_PROPERTY(bool busy READ busy NOTIFY busyChanged) public: - explicit SpectralRoom(Connection* connection, QString roomId, + explicit SpectralRoom(Connection* connection, + QString roomId, JoinState joinState = {}); const QString& cachedInput() const { return m_cachedInput; } @@ -76,6 +84,148 @@ class SpectralRoom : public Room { Q_INVOKABLE QString postMarkdownText(const QString& markdown); + template + QString eventToString(const BaseEventT& evt, + Qt::TextFormat format = Qt::PlainText) { + bool prettyPrint = (format == Qt::RichText); + + using namespace QMatrixClient; + return visit( + evt, + [this, prettyPrint](const RoomMessageEvent& e) { + using namespace MessageEventContent; + + if (prettyPrint && e.hasTextContent() && + e.mimeType().name() != "text/plain") + return static_cast(e.content())->body; + if (e.hasFileContent()) { + auto fileCaption = + e.content()->fileInfo()->originalName.toHtmlEscaped(); + if (fileCaption.isEmpty()) { + if (prettyPrint) + fileCaption = this->prettyPrint(e.plainBody()); + else + fileCaption = e.plainBody(); + } + return !fileCaption.isEmpty() ? fileCaption : tr("a file"); + } + return prettyPrint ? this->prettyPrint(e.plainBody()) : e.plainBody(); + }, + [this](const RoomMemberEvent& e) { + // FIXME: Rewind to the name that was at the time of this event + auto subjectName = this->user(e.userId())->displayname(); + // The below code assumes senderName output in AuthorRole + switch (e.membership()) { + case MembershipType::Invite: + if (e.repeatsState()) + return tr("reinvited %1 to the room").arg(subjectName); + FALLTHROUGH; + case MembershipType::Join: { + if (e.repeatsState()) + return tr("joined the room (repeated)"); + if (!e.prevContent() || + e.membership() != e.prevContent()->membership) { + return e.membership() == MembershipType::Invite + ? tr("invited %1 to the room").arg(subjectName) + : tr("joined the room"); + } + QString text{}; + if (e.isRename()) { + if (e.displayName().isEmpty()) + text = tr("cleared the display name"); + else + text = tr("changed the display name to %1") + .arg(e.displayName().toHtmlEscaped()); + } + if (e.isAvatarUpdate()) { + if (!text.isEmpty()) + text += " and "; + if (e.avatarUrl().isEmpty()) + text += tr("cleared the avatar"); + else + text += tr("updated the avatar"); + } + return text; + } + case MembershipType::Leave: + if (e.prevContent() && + e.prevContent()->membership == MembershipType::Invite) { + return (e.senderId() != e.userId()) + ? tr("withdrew %1's invitation").arg(subjectName) + : tr("rejected the invitation"); + } + + if (e.prevContent() && + e.prevContent()->membership == MembershipType::Ban) { + return (e.senderId() != e.userId()) + ? tr("unbanned %1").arg(subjectName) + : tr("self-unbanned"); + } + return (e.senderId() != e.userId()) + ? tr("has put %1 out of the room: %2") + .arg(subjectName, e.contentJson()["reason"_ls] + .toString() + .toHtmlEscaped()) + : tr("left the room"); + case MembershipType::Ban: + return (e.senderId() != e.userId()) + ? tr("banned %1 from the room: %2") + .arg(subjectName, e.contentJson()["reason"_ls] + .toString() + .toHtmlEscaped()) + : tr("self-banned from the room"); + case MembershipType::Knock: + return tr("knocked"); + default:; + } + return tr("made something unknown"); + }, + [](const RoomAliasesEvent& e) { + return tr("has set room aliases on server %1 to: %2") + .arg(e.stateKey(), QLocale().createSeparatedList(e.aliases())); + }, + [](const RoomCanonicalAliasEvent& e) { + return (e.alias().isEmpty()) + ? tr("cleared the room main alias") + : tr("set the room main alias to: %1").arg(e.alias()); + }, + [](const RoomNameEvent& e) { + return (e.name().isEmpty()) ? tr("cleared the room name") + : tr("set the room name to: %1") + .arg(e.name().toHtmlEscaped()); + }, + [this, prettyPrint](const RoomTopicEvent& e) { + return (e.topic().isEmpty()) + ? tr("cleared the topic") + : tr("set the topic to: %1") + .arg(prettyPrint ? this->prettyPrint(e.topic()) + : e.topic()); + }, + [](const RoomAvatarEvent&) { return tr("changed the room avatar"); }, + [](const EncryptionEvent&) { + return tr("activated End-to-End Encryption"); + }, + [](const RoomCreateEvent& e) { + return (e.isUpgrade() ? tr("upgraded the room to version %1") + : tr("created the room, version %1")) + .arg(e.version().isEmpty() ? "1" : e.version().toHtmlEscaped()); + }, + [](const StateEventBase& e) { + // A small hack for state events from TWIM bot + return e.stateKey() == "twim" + ? tr("updated the database", + "TWIM bot updated the database") + : e.stateKey().isEmpty() + ? tr("updated %1 state", "%1 - Matrix event type") + .arg(e.matrixType()) + : tr("updated %1 state for %2", + "%1 - Matrix event type, %2 - state key") + .arg(e.matrixType(), + e.stateKey().toHtmlEscaped()); + }, + tr("Unknown event")); + } + private: QString m_cachedInput; QSet highlights; @@ -101,11 +251,12 @@ class SpectralRoom : public Room { public slots: void chooseAndUploadFile(); - void saveFileAs(QString eventId); void acceptInvitation(); void forget(); void sendTypingNotification(bool isTyping); - void sendReply(QString userId, QString eventId, QString replyContent, + void sendReply(QString userId, + QString eventId, + QString replyContent, QString sendContent); }; diff --git a/src/userlistmodel.cpp b/src/userlistmodel.cpp index 15cff77..ff7e697 100644 --- a/src/userlistmodel.cpp +++ b/src/userlistmodel.cpp @@ -14,14 +14,16 @@ UserListModel::UserListModel(QObject* parent) : QAbstractListModel(parent), m_currentRoom(nullptr) {} void UserListModel::setRoom(QMatrixClient::Room* room) { - if (m_currentRoom == room) return; + if (m_currentRoom == room) + return; using namespace QMatrixClient; beginResetModel(); if (m_currentRoom) { m_currentRoom->disconnect(this); -// m_currentRoom->connection()->disconnect(this); - for (User* user : m_users) user->disconnect(this); + // m_currentRoom->connection()->disconnect(this); + for (User* user : m_users) + user->disconnect(this); m_users.clear(); } m_currentRoom = room; @@ -49,12 +51,14 @@ void UserListModel::setRoom(QMatrixClient::Room* room) { } QMatrixClient::User* UserListModel::userAt(QModelIndex index) { - if (index.row() < 0 || index.row() >= m_users.size()) return nullptr; + if (index.row() < 0 || index.row() >= m_users.size()) + return nullptr; return m_users.at(index.row()); } QVariant UserListModel::data(const QModelIndex& index, int role) const { - if (!index.isValid()) return QVariant(); + if (!index.isValid()) + return QVariant(); if (index.row() >= m_users.count()) { qDebug() @@ -71,12 +75,16 @@ QVariant UserListModel::data(const QModelIndex& index, int role) const { if (role == AvatarRole) { return user->avatarMediaId(); } + if (role == ObjectRole) { + return QVariant::fromValue(user); + } return QVariant(); } int UserListModel::rowCount(const QModelIndex& parent) const { - if (parent.isValid()) return 0; + if (parent.isValid()) + return 0; return m_users.count(); } @@ -111,7 +119,8 @@ void UserListModel::refresh(QMatrixClient::User* user, QVector roles) { void UserListModel::avatarChanged(QMatrixClient::User* user, const QMatrixClient::Room* context) { - if (context == m_currentRoom) refresh(user, {AvatarRole}); + if (context == m_currentRoom) + refresh(user, {AvatarRole}); } int UserListModel::findUserPos(User* user) const { @@ -127,5 +136,6 @@ QHash UserListModel::roleNames() const { roles[NameRole] = "name"; roles[UserIDRole] = "userId"; roles[AvatarRole] = "avatar"; + roles[ObjectRole] = "user"; return roles; } diff --git a/src/userlistmodel.h b/src/userlistmodel.h index fd01a5e..25db08a 100644 --- a/src/userlistmodel.h +++ b/src/userlistmodel.h @@ -17,7 +17,12 @@ class UserListModel : public QAbstractListModel { Q_PROPERTY( QMatrixClient::Room* room READ room WRITE setRoom NOTIFY roomChanged) public: - enum EventRoles { NameRole = Qt::UserRole + 1, UserIDRole, AvatarRole }; + enum EventRoles { + NameRole = Qt::UserRole + 1, + UserIDRole, + AvatarRole, + ObjectRole + }; using User = QMatrixClient::User; diff --git a/src/utils.h b/src/utils.h index cce426d..dc36648 100644 --- a/src/utils.h +++ b/src/utils.h @@ -26,120 +26,6 @@ static const QRegularExpression userPillRegExp{ QString removeReply(const QString& text); QString cleanHTML(const QString& text, QMatrixClient::Room* room); - -template -QString eventToString(const BaseEventT& evt, - QMatrixClient::Room* room = nullptr, - Qt::TextFormat format = Qt::PlainText) { - bool prettyPrint = (format == Qt::RichText); - - using namespace QMatrixClient; - return visit( - evt, - [room, prettyPrint](const RoomMessageEvent& e) { - using namespace MessageEventContent; - - if (prettyPrint && e.hasTextContent() && - e.mimeType().name() != "text/plain") { - return cleanHTML(static_cast(e.content())->body, - room); - } - if (e.hasFileContent()) { - auto fileCaption = e.content()->fileInfo()->originalName; - if (fileCaption.isEmpty()) - fileCaption = prettyPrint && room ? room->prettyPrint(e.plainBody()) - : e.plainBody(); - if (fileCaption.isEmpty()) return QObject::tr("a file"); - } - return prettyPrint && room ? room->prettyPrint(e.plainBody()) - : e.plainBody(); - }, - [room](const RoomMemberEvent& e) { - // FIXME: Rewind to the name that was at the time of this event - QString subjectName = - room ? room->roomMembername(e.userId()) : e.userId(); - // The below code assumes senderName output in AuthorRole - switch (e.membership()) { - case MembershipType::Invite: - if (e.repeatsState()) - return QObject::tr("reinvited %1 to the room").arg(subjectName); - FALLTHROUGH; - case MembershipType::Join: { - if (e.repeatsState()) - return QObject::tr("joined the room (repeated)"); - if (!e.prevContent() || - e.membership() != e.prevContent()->membership) { - return e.membership() == MembershipType::Invite - ? QObject::tr("invited %1 to the room") - .arg(subjectName) - : QObject::tr("joined the room"); - } - QString text{}; - if (e.isRename()) { - if (e.displayName().isEmpty()) - text = QObject::tr("cleared their display name"); - else - text = QObject::tr("changed their display name to %1") - .arg(e.displayName()); - } - if (e.isAvatarUpdate()) { - if (!text.isEmpty()) text += " and "; - if (e.avatarUrl().isEmpty()) - text += QObject::tr("cleared the avatar"); - else - text += QObject::tr("updated the avatar"); - } - return text; - } - case MembershipType::Leave: - if (e.prevContent() && - e.prevContent()->membership == MembershipType::Ban) { - return (e.senderId() != e.userId()) - ? QObject::tr("unbanned %1").arg(subjectName) - : QObject::tr("self-unbanned"); - } - return (e.senderId() != e.userId()) - ? QObject::tr("has kicked %1 from the room") - .arg(subjectName) - : QObject::tr("left the room"); - case MembershipType::Ban: - return (e.senderId() != e.userId()) - ? QObject::tr("banned %1 from the room ") - .arg(subjectName) - : QObject::tr(" self-banned from the room "); - case MembershipType::Knock: - return QObject::tr("knocked"); - default:; - } - return QObject::tr("made something unknown"); - }, - [](const RoomAliasesEvent& e) { - return QObject::tr("set aliases to: %1").arg(e.aliases().join(",")); - }, - [](const RoomCanonicalAliasEvent& e) { - return (e.alias().isEmpty()) - ? QObject::tr("cleared the room main alias") - : QObject::tr("set the room main alias to: %1") - .arg(e.alias()); - }, - [](const RoomNameEvent& e) { - return (e.name().isEmpty()) - ? QObject::tr("cleared the room name") - : QObject::tr("set the room name to: %1").arg(e.name()); - }, - [](const RoomTopicEvent& e) { - return (e.topic().isEmpty()) - ? QObject::tr("cleared the topic") - : QObject::tr("set the topic to: %1").arg(e.topic()); - }, - [](const RoomAvatarEvent&) { - return QObject::tr("changed the room avatar"); - }, - [](const EncryptionEvent&) { - return QObject::tr("activated End-to-End Encryption"); - }, - QObject::tr("Unknown Event")); -}; } // namespace utils #endif