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