├── .clang-format ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .gitmodules ├── .lgtm.yml ├── BUILDING.md ├── CMakeLists.txt ├── COPYING ├── ISSUE_TEMPLATE.md ├── LICENSE.spdx ├── LICENSES ├── BSD-3-Clause.txt ├── GPL-3.0-or-later.txt ├── LGPL-2.1-only.txt └── LGPL-2.1-or-later.txt ├── Quaternion.project ├── README.md ├── SECURITY.md ├── Screenshot.png ├── client ├── accountselector.cpp ├── accountselector.h ├── activitydetector.cpp ├── activitydetector.h ├── chatedit.cpp ├── chatedit.h ├── chatroomwidget.cpp ├── chatroomwidget.h ├── desktop_integration.h ├── dialog.cpp ├── dialog.h ├── dockmodemenu.cpp ├── dockmodemenu.h ├── htmlfilter.cpp ├── htmlfilter.h ├── kchatedit.cpp ├── kchatedit.h ├── logging_categories.h ├── logindialog.cpp ├── logindialog.h ├── main.cpp ├── mainwindow.cpp ├── mainwindow.h ├── models │ ├── abstractroomordering.cpp │ ├── abstractroomordering.h │ ├── messageeventmodel.cpp │ ├── messageeventmodel.h │ ├── orderbytag.cpp │ ├── orderbytag.h │ ├── roomlistmodel.cpp │ ├── roomlistmodel.h │ ├── userlistmodel.cpp │ └── userlistmodel.h ├── networkconfigdialog.cpp ├── networkconfigdialog.h ├── profiledialog.cpp ├── profiledialog.h ├── qml │ ├── AnimatedTransition.qml │ ├── AnimationBehavior.qml │ ├── Attachment.qml │ ├── Avatar.qml │ ├── FastNumberAnimation.qml │ ├── FileContent.qml │ ├── ImageContent.qml │ ├── Logger.qml │ ├── NormalNumberAnimation.qml │ ├── RoomHeader.qml │ ├── ScrollFinisher.qml │ ├── Timeline.qml │ ├── TimelineItem.qml │ ├── TimelineItemToolButton.qml │ ├── TimelineMouseArea.qml │ ├── TimelineSettings.qml │ └── TimelineTextEditSelector.qml ├── quaternionroom.cpp ├── quaternionroom.h ├── resources.qrc ├── roomdialogs.cpp ├── roomdialogs.h ├── roomlistdock.cpp ├── roomlistdock.h ├── systemtrayicon.cpp ├── systemtrayicon.h ├── timelinewidget.cpp ├── timelinewidget.h ├── translations │ ├── quaternion_de.ts │ ├── quaternion_en.ts │ ├── quaternion_en_GB.ts │ ├── quaternion_en_US.ts │ ├── quaternion_es.ts │ ├── quaternion_pl.ts │ └── quaternion_ru.ts ├── userlistdock.cpp ├── userlistdock.h ├── verificationdialog.cpp └── verificationdialog.h ├── cmake ├── ECMInstallIcons.cmake └── MacOSXBundleInfo.plist.in ├── flatpak ├── build.sh ├── io.github.quotient_im.Quaternion.yaml └── setup_runtime.sh ├── icons ├── breeze │ ├── COPYING.breeze │ ├── README.breeze │ ├── irc-channel-joined.svg │ └── irc-channel-parted.svg ├── busy_16x16.gif ├── irc-channel-invited.svg ├── quaternion.icns ├── quaternion.ico ├── quaternion │ ├── 128-apps-quaternion.png │ ├── 16-apps-quaternion.png │ ├── 22-apps-quaternion.png │ ├── 32-apps-quaternion.png │ ├── 48-apps-quaternion.png │ ├── 64-apps-quaternion.png │ ├── sc-apps-quaternion.svgz │ └── sources │ │ ├── quaternion-green.svg │ │ ├── quaternion-red.svg │ │ └── sc-apps-quaternion.svg ├── scrolldown.svg └── scrollup.svg ├── linux ├── io.github.quotient_im.Quaternion.appdata.xml └── io.github.quotient_im.Quaternion.desktop ├── quaternion.kdev4 ├── quaternion.supp └── quaternion_win32.rc /.clang-format: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2019 Project Quotient 2 | # SPDX-License-Identifier: LGPL-2.1-only 3 | # 4 | # You may use this file under the terms of the LGPL-2.1 license 5 | # See the file LICENSE from this package for details. 6 | 7 | # This is the clang-format configuration style to be used by libQuotient. 8 | # Inspired by: 9 | # https://code.qt.io/cgit/qt/qt5.git/plain/_clang-format 10 | # https://wiki.qt.io/Qt_Coding_Style 11 | # https://wiki.qt.io/Coding_Conventions 12 | # Further information: https://clang.llvm.org/docs/ClangFormatStyleOptions.html 13 | 14 | # For convenience, the file includes commented out settings that we assume 15 | # to borrow from the WebKit style. The values for such settings try to but 16 | # are not guaranteed to coincide with the latest version of the WebKit style. 17 | 18 | # This file assumes ClangFormat 18 or newer 19 | 20 | --- 21 | Language: Cpp 22 | BasedOnStyle: WebKit 23 | #AccessModifierOffset: -4 24 | AlignAfterOpenBracket: Align 25 | #AlignArrayOfStructures: None # As of ClangFormat 14, Left doesn't work well 26 | #AlignConsecutiveAssignments: None 27 | #AlignConsecutiveDeclarations: None 28 | #AlignConsecutiveMacros: None 29 | AlignConsecutiveShortCaseStatements: 30 | Enabled: true 31 | AlignEscapedNewlines: Left 32 | AlignOperands: Align 33 | #AlignTrailingComments: false 34 | #AllowAllArgumentsOnNextLine: true 35 | #AllowAllParametersOfDeclarationOnNextLine: true 36 | AllowBreakBeforeNoexceptSpecifier: Always 37 | AllowShortBlocksOnASingleLine: Empty 38 | AllowShortCaseLabelsOnASingleLine: true 39 | #AllowShortCompoundRequirementOnASingleLine: true 40 | #AllowShortEnumsOnASingleLine: true 41 | #AllowShortFunctionsOnASingleLine: All 42 | #AllowShortIfStatementsOnASingleLine: Never 43 | #AllowShortLambdasOnASingleLine: All 44 | #AllowShortLoopsOnASingleLine: false 45 | #AlwaysBreakAfterReturnType: None 46 | #AlwaysBreakBeforeMultilineStrings: false 47 | AlwaysBreakTemplateDeclarations: Yes # To be replaced with BreakTemplateDeclarations 48 | AttributeMacros: 49 | - Q_IMPLICIT 50 | #BinPackArguments: true 51 | #BinPackParameters: true 52 | #BitFieldColonSpacing: Both 53 | BraceWrapping: 54 | # AfterCaseLabel: false 55 | # AfterClass: false 56 | # AfterControlStatement: Never 57 | # AfterEnum: false 58 | # AfterExternBlock: false 59 | AfterFunction: true 60 | # AfterNamespace: false 61 | # AfterStruct: false 62 | # AfterUnion: false 63 | # BeforeCatch: false 64 | # BeforeElse: false 65 | BeforeLambdaBody: true 66 | # BeforeWhile: false 67 | # IndentBraces: false 68 | SplitEmptyFunction: false 69 | SplitEmptyRecord: false 70 | SplitEmptyNamespace: false 71 | BracedInitializerIndentWidth: 2 # Initializer padding inhibits this, making indentation inconsistent 72 | #BreakAdjacentStringLiterals: true 73 | #BreakAfterAttributes: Leave 74 | #BreakAfterReturnType: Automatic # ClangFormat 19; default value anyway 75 | BreakBeforeBinaryOperators: NonAssignment 76 | #BreakBeforeConceptDeclarations: Always 77 | BreakBeforeBraces: Custom 78 | #BreakBeforeTernaryOperators: true 79 | #BreakConstructorInitializers: BeforeComma 80 | #BreakFunctionDefinitionParameters: false # ClangFormat 19; default value anyway 81 | #BreakInheritanceList: BeforeColon 82 | #BreakStringLiterals: true 83 | #BreakTemplateDeclarations: Multiline # As of ClangFormat 19, has unintended formatting side-effects 84 | ColumnLimit: 100 85 | #QualifierAlignment: Leave # ClangFormat 14 - except 'Leave', updates whole files 86 | #CompactNamespaces: false 87 | #ConstructorInitializerIndentWidth: 4 88 | ContinuationIndentWidth: 2 89 | Cpp11BracedListStyle: true 90 | #DerivePointerAlignment: false 91 | #EmptyLineAfterAccessModifier: Never 92 | EmptyLineBeforeAccessModifier: LogicalBlock 93 | FixNamespaceComments: true 94 | IncludeBlocks: Regroup 95 | IncludeCategories: 96 | - Regex: '["]' 101 | Priority: 16 102 | - Regex: '^ marks will be invisible in the report. 11 | 12 | --> 13 | 14 | ### Description 15 | 16 | Describe here the problem that you are experiencing, or the feature you are requesting. 17 | 18 | ### Steps to reproduce 19 | 20 | - For bugs, list the steps 21 | - that reproduce the bug 22 | - using hyphens as bullet points 23 | 24 | Describe how what happens differs from what you expected. 25 | 26 | Quaternion dumps logs to the standard output. If you can find the logs and 27 | identify any log snippets relevant to your issue, please include 28 | those here (please be careful to remove any personal or private data): 29 | 30 | ### Version information 31 | 32 | 33 | 34 | - **Quaternion version**: 35 | - **Qt version**: 38 | - **Install method**: 39 | - **Platform**: 40 | -------------------------------------------------------------------------------- /LICENSE.spdx: -------------------------------------------------------------------------------- 1 | SPDXVersion: SPDX-2.3 2 | DataLicense: CC0-1.0 3 | 4 | PackageName: Quaternion 5 | PackageSupplier: Organization: The Quotient project 6 | PackageDownloadLocation: git+https://github.com/quotient-im/Quaternion.git 7 | FilesAnalyzed: false 8 | PackageHomePage: https://github.com/quotient-im/Quaternion 9 | PackageLicenseInfoFromFiles: GPL-3.0-or-later 10 | PackageLicenseInfoFromFiles: LGPL-2.1-only 11 | PackageLicenseInfoFromFiles: LGPL-2.1-or-later 12 | PackageLicenseInfoFromFiles: BSD-3-Clause 13 | PackageLicenseDeclared: GPL-3.0-or-later 14 | PackageCopyrightText: Copyright The Quotient project contributors 15 | -------------------------------------------------------------------------------- /LICENSES/BSD-3-Clause.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) . All rights reserved. 2 | 3 | Redistribution and use in source and binary forms, with or without modification, 4 | are permitted provided that the following conditions are met: 5 | 6 | 1. Redistributions of source code must retain the above copyright notice, 7 | this list of conditions and the following disclaimer. 8 | 9 | 2. Redistributions in binary form must reproduce the above copyright notice, 10 | this list of conditions and the following disclaimer in the documentation 11 | and/or other materials provided with the distribution. 12 | 13 | 3. Neither the name of the copyright holder nor the names of its contributors 14 | may be used to endorse or promote products derived from this software without 15 | specific prior written permission. 16 | 17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 18 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 19 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 20 | ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE 21 | LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 22 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 23 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 24 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 25 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE 26 | USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 27 | -------------------------------------------------------------------------------- /Quaternion.project: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | . 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | cmake $(ProjectPath) -G "MinGW Makefiles" -DCMAKE_BUILD_TYPE=Debug -DCMAKE_EXPORT_COMPILE_COMMANDS=1 129 | mingw32-make clean && mingw32-make -j4 130 | mingw32-make clean 131 | mingw32-make -j4 132 | 133 | 134 | 135 | None 136 | $(IntermediateDirectory) 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | cmake $(ProjectPath) -G "MinGW Makefiles" -DCMAKE_BUILD_TYPE=Release -DCMAKE_EXPORT_COMPILE_COMMANDS=1 166 | mingw32-make clean && mingw32-make -j4 167 | mingw32-make clean 168 | mingw32-make -j4 169 | 170 | 171 | 172 | None 173 | $(IntermediateDirectory) 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | Only the latest stable (i.e. not beta) version of Quaternion is supported with security updates. 6 | An effort is put into supporting the version on most recent stable releases of each major Linux 7 | distribution (Debian, Ubuntu, Fedora, OpenSuse). Users of older Quaternion versions are strongly 8 | advised to upgrade to the latest release - support of those versions is very limited, if provided 9 | at all. If you can't do it because your Linux distribution is too old, you likely have other 10 | security problems as well; upgrade your Linux distribution! 11 | 12 | ## Reporting a Vulnerability 13 | 14 | If you find a significant vulnerability, or evidence of one, use either of the following contacts: 15 | - send an email to [Kitsune Ral](mailto:Kitsune-Ral@users.sf.net); or 16 | - reach out in Matrix to [@kitsune:matrix.org](https://matrix.to/#/@kitsune:matrix.org) (if you can, switch encryption on). 17 | 18 | In any of these two options, first indicate that you have such information 19 | (do not disclose it yet) and wait for further instructions. 20 | 21 | By default, we will give credit to anyone who reports a vulnerability in a responsible way so that 22 | we can fix it before public disclosure. If you want to remain anonymous or pseudonymous instead, 23 | please let us know; we will gladly respect your wishes. 24 | 25 | If you provide a security fix as a PR, you have no way to remain anonymous; you also thereby 26 | lay out the vulnerability itself so this is NOT the right way for undisclosed vulnerabilities, 27 | whether or not you want to stay incognito. 28 | 29 | ## Timeline and commitments 30 | 31 | Initial reaction to the message about a vulnerability (see above) will be no more than 5 days. 32 | From the moment of the private report or public disclosure (if it hasn't been reported earlier 33 | in private) of each vulnerability, we take effort to fix it on priority before any other issues. 34 | In case of vulnerabilities with [CVSS v2](https://nvd.nist.gov/cvss.cfm) score of 4.0 and higher 35 | the commitment is to provide a workaround within 30 days and a full fix within 60 days after 36 | the specific information on the vulnerability has been reported to the project by any means 37 | (in private or in public). For vulnerabilities with lower score there is no commitment on 38 | the timeline, only prioritisation. The full fix doesn't imply that all software functionality 39 | remains accessible (in the worst case the vulnerable functionality may be disabled or removed 40 | to prevent the attack). 41 | -------------------------------------------------------------------------------- /Screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quotient-im/Quaternion/c85d16312ee42a341c10c55e5bfb9c4c3b97c20b/Screenshot.png -------------------------------------------------------------------------------- /client/accountselector.cpp: -------------------------------------------------------------------------------- 1 | #include "accountselector.h" 2 | 3 | #include "logging_categories.h" 4 | 5 | #include 6 | #include 7 | 8 | using namespace Quotient; 9 | 10 | AccountSelector::AccountSelector(AccountRegistry* registry, QWidget* parent) 11 | : QComboBox(parent) 12 | { 13 | Q_ASSERT(registry != nullptr); 14 | connect(this, QOverload::of(&QComboBox::currentIndexChanged), this, 15 | [this] { emit currentAccountChanged(currentAccount()); }); 16 | 17 | for (auto* acc: registry->accounts()) 18 | addItem(acc->userId(), QVariant::fromValue(acc)); 19 | 20 | connect(registry, &AccountRegistry::rowsInserted, this, 21 | [this, registry](const QModelIndex&, int first, int last) { 22 | const auto& accounts = registry->accounts(); 23 | for (int i = first; i < last; i++) { 24 | auto acc = accounts[i]; 25 | if (const auto idx = indexOfAccount(acc); idx == -1) 26 | addItem(acc->userId(), QVariant::fromValue(acc)); 27 | else 28 | qCWarning(ACCOUNTSELECTOR) 29 | << "Refusing to add the same account twice"; 30 | } 31 | }); 32 | connect(registry, &AccountRegistry::rowsAboutToBeRemoved, this, 33 | [this, registry](const QModelIndex&, int first, int last) { 34 | const auto& accounts = registry->accounts(); 35 | for (int i = first; i < last; i++) { 36 | auto acc = accounts[i]; 37 | if (const auto idx = indexOfAccount(acc); idx != -1) 38 | removeItem(idx); 39 | else 40 | qCWarning(ACCOUNTSELECTOR) 41 | << "Account to drop not found, ignoring"; 42 | } 43 | }); 44 | } 45 | 46 | void AccountSelector::setAccount(Connection *newAccount) 47 | { 48 | if (!newAccount) { 49 | setCurrentIndex(-1); 50 | return; 51 | } 52 | if (auto i = indexOfAccount(newAccount); i != -1) { 53 | setCurrentIndex(i); 54 | return; 55 | } 56 | Q_ASSERT(false); 57 | qCWarning(ACCOUNTSELECTOR) 58 | << "Account for" << newAccount->userId() + '/' + newAccount->deviceId() 59 | << "wasn't found in the full list of accounts"; 60 | } 61 | 62 | Connection* AccountSelector::currentAccount() const 63 | { 64 | return currentData().value(); 65 | } 66 | 67 | int AccountSelector::indexOfAccount(Connection* a) const 68 | { 69 | for (int i = 0; i < count(); ++i) 70 | if (itemData(i).value() == a) 71 | return i; 72 | 73 | return -1; 74 | } 75 | -------------------------------------------------------------------------------- /client/accountselector.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | namespace Quotient { 6 | class AccountRegistry; 7 | class Connection; 8 | } 9 | 10 | class AccountSelector : public QComboBox 11 | { 12 | Q_OBJECT 13 | public: 14 | AccountSelector(Quotient::AccountRegistry *registry, 15 | QWidget* parent = nullptr); 16 | 17 | void setAccount(Quotient::Connection* newAccount); 18 | Quotient::Connection* currentAccount() const; 19 | int indexOfAccount(Quotient::Connection* a) const; 20 | 21 | signals: 22 | void currentAccountChanged(Quotient::Connection* newAccount); 23 | }; 24 | 25 | -------------------------------------------------------------------------------- /client/activitydetector.cpp: -------------------------------------------------------------------------------- 1 | /************************************************************************** 2 | * * 3 | * SPDX-FileCopyrightText: 2016 Malte Brandy * 4 | * * 5 | * SPDX-License-Identifier: GPL-3.0-or-later 6 | * * 7 | **************************************************************************/ 8 | 9 | #include "activitydetector.h" 10 | 11 | #include "logging_categories.h" 12 | 13 | #include 14 | #include 15 | #include 16 | 17 | void ActivityDetector::setEnabled(bool enabled) 18 | { 19 | if (enabled == m_enabled) 20 | return; 21 | 22 | m_enabled = enabled; 23 | const auto& topLevels = qApp->topLevelWidgets(); 24 | for (auto* w: topLevels) 25 | if (!w->isHidden()) 26 | w->setMouseTracking(enabled); 27 | if (enabled) 28 | qApp->installEventFilter(this); 29 | else 30 | qApp->removeEventFilter(this); 31 | qCDebug(MAIN) << "Activity Detector enabled:" << enabled; 32 | } 33 | 34 | bool ActivityDetector::eventFilter(QObject* obj, QEvent* ev) 35 | { 36 | switch (ev->type()) 37 | { 38 | case QEvent::KeyPress: 39 | case QEvent::FocusIn: 40 | case QEvent::MouseMove: 41 | case QEvent::MouseButtonPress: 42 | emit triggered(); 43 | break; 44 | default:; 45 | } 46 | return QObject::eventFilter(obj, ev); 47 | } 48 | -------------------------------------------------------------------------------- /client/activitydetector.h: -------------------------------------------------------------------------------- 1 | /************************************************************************** 2 | * * 3 | * SPDX-FileCopyrightText: 2016 Malte Brandy * 4 | * * 5 | * SPDX-License-Identifier: GPL-3.0-or-later 6 | * * 7 | **************************************************************************/ 8 | 9 | #pragma once 10 | 11 | #include 12 | 13 | class ActivityDetector : public QObject 14 | { 15 | Q_OBJECT 16 | public slots: 17 | void setEnabled(bool enabled); 18 | 19 | signals: 20 | void triggered(); 21 | 22 | private: 23 | bool m_enabled = false; 24 | 25 | bool eventFilter(QObject* obj, QEvent* ev) override; 26 | }; 27 | -------------------------------------------------------------------------------- /client/chatedit.h: -------------------------------------------------------------------------------- 1 | /************************************************************************** 2 | * * 3 | * SPDX-FileCopyrightText: 2017 Kitsune Ral 4 | * * 5 | * SPDX-License-Identifier: GPL-3.0-or-later 6 | * * 7 | **************************************************************************/ 8 | 9 | #pragma once 10 | 11 | #include "kchatedit.h" 12 | 13 | #include 14 | 15 | class ChatRoomWidget; 16 | 17 | class ChatEdit : public KChatEdit 18 | { 19 | Q_OBJECT 20 | public: 21 | using completions_t = QVector>; 22 | 23 | ChatEdit(ChatRoomWidget* c); 24 | 25 | void triggerCompletion(); 26 | void cancelCompletion(); 27 | bool isCompletionActive(); 28 | 29 | void insertMention(QString author, QUrl url); 30 | bool acceptMimeData(const QMimeData* source); 31 | 32 | // NB: the following virtual functions are protected in QTextEdit but 33 | // ChatRoomWidget delegates to them 34 | 35 | bool canInsertFromMimeData(const QMimeData* source) const override; 36 | 37 | public slots: 38 | void switchContext(QObject* contextKey) override; 39 | void alternatePaste(); 40 | 41 | signals: 42 | void cancelledCompletion(); 43 | 44 | private: 45 | ChatRoomWidget* chatRoomWidget; 46 | 47 | QTextCursor completionCursor; 48 | /// Text/href pairs for completion 49 | completions_t completionMatches; 50 | int matchesListPosition; 51 | 52 | bool pickingMentions = false; 53 | bool m_pastePlaintext; 54 | 55 | /// \brief Initialise a new completion 56 | /// 57 | /// \return true if completion matches exist for the current entry; 58 | /// false otherwise 59 | bool initCompletion(); 60 | void appendMentionAt(QTextCursor& cursor, QString mention, 61 | QUrl mentionUrl, bool select); 62 | void keyPressEvent(QKeyEvent* event) override; 63 | void contextMenuEvent(QContextMenuEvent* event) override; 64 | void insertFromMimeData(const QMimeData* source) override; 65 | void dragEnterEvent(QDragEnterEvent* event) override; 66 | 67 | static bool pastePlaintextByDefault(); 68 | 69 | }; 70 | -------------------------------------------------------------------------------- /client/chatroomwidget.h: -------------------------------------------------------------------------------- 1 | /************************************************************************** 2 | * * 3 | * SPDX-FileCopyrightText: 2015 Felix Rohrbach * 4 | * * 5 | * SPDX-License-Identifier: GPL-3.0-or-later 6 | * * 7 | **************************************************************************/ 8 | 9 | #pragma once 10 | 11 | #include "chatedit.h" 12 | #include "htmlfilter.h" 13 | 14 | #include 15 | 16 | #include 17 | #include 18 | 19 | namespace Quotient { 20 | class Connection; 21 | } 22 | 23 | class TimelineWidget; 24 | class QuaternionRoom; 25 | class MainWindow; 26 | 27 | class QLabel; 28 | class QAction; 29 | 30 | class ChatRoomWidget : public QWidget 31 | { 32 | Q_OBJECT 33 | public: 34 | explicit ChatRoomWidget(MainWindow* parent = nullptr); 35 | TimelineWidget* timelineWidget() const; 36 | QuaternionRoom* currentRoom() const; 37 | 38 | // Helpers for m_chatEdit 39 | 40 | ChatEdit::completions_t findCompletionMatches(const QString& pattern) const; 41 | QString matrixHtmlFromMime(const QMimeData* data) const; 42 | void checkDndEvent(QDropEvent* event) const; 43 | 44 | public slots: 45 | void setRoom(QuaternionRoom* newRoom); 46 | void insertMention(const QString &userId); 47 | void attachImage(const QImage& img, const QList& sources); 48 | QString attachFile(const QString& localPath); 49 | void dropFile(const QString& localPath); 50 | QString checkAttachment(); 51 | void cancelAttaching(); 52 | void focusInput(); 53 | 54 | //! Set a line above the message input, with optional list of member displaynames 55 | void setHudHtml(const QString& htmlCaption, 56 | const QStringList& plainTextNames = {}); 57 | 58 | void showStatusMessage(const QString& message, int timeout = 0) const; 59 | void showCompletions(QStringList matches, int pos); 60 | 61 | void typingChanged(); 62 | void quote(const QString& htmlText); 63 | 64 | private slots: 65 | void sendInput(); 66 | void encryptionChanged(); 67 | 68 | private: 69 | TimelineWidget* m_timelineWidget; 70 | QLabel* m_hudCaption; //!< For typing and completion notifications 71 | QAction* m_attachAction; 72 | ChatEdit* m_chatEdit; 73 | 74 | std::unique_ptr m_fileToAttach; 75 | Quotient::SettingsGroup m_uiSettings; 76 | 77 | MainWindow* mainWindow() const; 78 | Quotient::Connection* currentConnection() const; 79 | 80 | QString sendFile(); 81 | void sendMessage(); 82 | void sendSelection(int fromPosition, HtmlFilter::Options htmlFilterOptions); 83 | [[nodiscard]] QString sendCommand(QStringView command, 84 | const QString& argString); 85 | 86 | void resizeEvent(QResizeEvent*) override; 87 | void keyPressEvent(QKeyEvent* event) override; 88 | void dragEnterEvent(QDragEnterEvent* event) override; 89 | void dropEvent(QDropEvent* event) override; 90 | 91 | int maximumChatEditHeight() const; 92 | }; 93 | -------------------------------------------------------------------------------- /client/desktop_integration.h: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2017 Elvis Angelaccio 3 | * SPDX-FileCopyrightText: 2020 The Quotient project 4 | * 5 | * SPDX-License-Identifier: GPL-3.0-or-later 6 | */ 7 | 8 | #pragma once 9 | 10 | #include 11 | #include 12 | 13 | inline const auto AppName = QStringLiteral("quaternion"); 14 | inline const auto AppId = QStringLiteral("io.github.quotient_im.Quaternion"); 15 | 16 | inline bool inFlatpak() { return QFileInfo::exists("/.flatpak-info"); } 17 | 18 | inline QIcon appIcon() 19 | { 20 | using Qt::operator""_s; 21 | return QIcon::fromTheme(inFlatpak() ? AppId : AppName, QIcon(u":/icon.png"_s)); 22 | } 23 | -------------------------------------------------------------------------------- /client/dialog.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2017 Kitsune Ral 3 | * 4 | * SPDX-License-Identifier: LGPL-2.1-or-later 5 | */ 6 | 7 | #include "dialog.h" 8 | 9 | #include 10 | #include 11 | 12 | #if QT_VERSION < QT_VERSION_CHECK(6, 6, 0) 13 | #include // For std::views::adjacent 14 | #endif 15 | 16 | Dialog::Dialog(const QString& title, QWidget *parent, 17 | UseStatusLine useStatusLine, const QString& applyTitle, 18 | QDialogButtonBox::StandardButtons addButtons) 19 | : Dialog(title 20 | , QDialogButtonBox::Ok | /*QDialogButtonBox::Cancel |*/ addButtons 21 | , parent, useStatusLine) 22 | { 23 | if (!applyTitle.isEmpty()) 24 | buttons->button(QDialogButtonBox::Ok)->setText(applyTitle); 25 | } 26 | 27 | 28 | Dialog::Dialog(const QString& title, QDialogButtonBox::StandardButtons setButtons, 29 | QWidget *parent, UseStatusLine useStatusLine) 30 | : QDialog(parent) 31 | , pendingApplyMessage(tr("Applying changes, please wait")) 32 | , statusLabel(useStatusLine == NoStatusLine ? nullptr : new QLabel) 33 | , buttons(new QDialogButtonBox(setButtons)) 34 | , outerLayout(this) 35 | { 36 | setWindowTitle(title); 37 | 38 | connect(buttons, &QDialogButtonBox::clicked, this, &Dialog::buttonClicked); 39 | 40 | outerLayout.addWidget(buttons); 41 | if (statusLabel) 42 | outerLayout.addWidget(statusLabel); 43 | } 44 | 45 | void Dialog::addLayout(QLayout* l, int stretch) 46 | { 47 | int offset = 1 + (statusLabel != nullptr); 48 | outerLayout.insertLayout(outerLayout.count() - offset, l, stretch); 49 | } 50 | 51 | void Dialog::addWidget(QWidget* w, int stretch, Qt::Alignment alignment) 52 | { 53 | int offset = 1 + (statusLabel != nullptr); 54 | outerLayout.insertWidget(outerLayout.count() - offset, w, stretch, alignment); 55 | } 56 | 57 | QLabel* Dialog::makeBuddyLabel(QString labelText, QWidget* field) 58 | { 59 | auto label = new QLabel(labelText); 60 | label->setBuddy(field); 61 | return label; 62 | } 63 | 64 | QPushButton* Dialog::button(QDialogButtonBox::StandardButton which) 65 | { 66 | return buttonBox()->button(which); 67 | } 68 | 69 | #if QT_VERSION < QT_VERSION_CHECK(6, 6, 0) 70 | void Dialog::setTabOrder(std::initializer_list widgets) 71 | { 72 | for (auto [w1, w2] : std::views::adjacent<2>(widgets)) 73 | setTabOrder(w1, w2); 74 | } 75 | #endif 76 | 77 | void Dialog::reactivate() 78 | { 79 | if (!isVisible()) 80 | { 81 | load(); 82 | show(); 83 | } 84 | raise(); 85 | activateWindow(); 86 | } 87 | 88 | void Dialog::setStatusMessage(const QString& msg) 89 | { 90 | Q_ASSERT(statusLabel); 91 | statusLabel->setText(msg); 92 | } 93 | 94 | void Dialog::applyFailed(const QString& errorMessage) 95 | { 96 | setStatusMessage(errorMessage); 97 | setDisabled(false); 98 | } 99 | 100 | void Dialog::buttonClicked(QAbstractButton* button) 101 | { 102 | switch (buttons->buttonRole(button)) 103 | { 104 | case QDialogButtonBox::AcceptRole: 105 | case QDialogButtonBox::YesRole: 106 | if (validate()) 107 | { 108 | if (statusLabel) 109 | statusLabel->setText(pendingApplyMessage); 110 | setDisabled(true); 111 | apply(); 112 | } 113 | break; 114 | case QDialogButtonBox::ResetRole: 115 | load(); 116 | break; 117 | case QDialogButtonBox::RejectRole: 118 | case QDialogButtonBox::NoRole: 119 | reject(); 120 | break; 121 | default: 122 | ; // Derived classes may completely replace or reuse this method 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /client/dialog.h: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2017 Kitsune Ral 3 | * 4 | * SPDX-License-Identifier: LGPL-2.1-or-later 5 | */ 6 | 7 | #pragma once 8 | 9 | #include 10 | #include 11 | #include 12 | #include 13 | 14 | class QAbstractButton; 15 | class QLabel; 16 | 17 | class Dialog : public QDialog 18 | { 19 | Q_OBJECT 20 | public: 21 | enum UseStatusLine { NoStatusLine, StatusLine }; 22 | static constexpr auto NoExtraButtons = QDialogButtonBox::NoButton; 23 | 24 | explicit Dialog(const QString& title, QWidget* parent = nullptr, 25 | UseStatusLine useStatusLine = NoStatusLine, const QString& applyTitle = {}, 26 | QDialogButtonBox::StandardButtons addButtons = QDialogButtonBox::Reset); 27 | 28 | explicit Dialog(const QString& title, QDialogButtonBox::StandardButtons setButtons, 29 | QWidget* parent = nullptr, UseStatusLine useStatusLine = NoStatusLine); 30 | 31 | /// Create and add a layout of the given type 32 | /*! This creates a new layout object and adds it to the bottom of 33 | * the dialog client area (i.e., above the button box). */ 34 | template 35 | LayoutT* addLayout(int stretch = 0) 36 | { 37 | auto l = new LayoutT; 38 | addLayout(l, stretch); 39 | return l; 40 | } 41 | /// Add a layout to the bottom of the dialog's client area 42 | void addLayout(QLayout* l, int stretch = 0); 43 | /// Add a widget to the bottom of the dialog's client area 44 | void addWidget(QWidget* w, int stretch = 0, Qt::Alignment alignment = {}); 45 | 46 | static QLabel* makeBuddyLabel(QString labelText, QWidget* field); 47 | 48 | QPushButton* button(QDialogButtonBox::StandardButton which); 49 | 50 | using QWidget::setTabOrder; 51 | #if QT_VERSION < QT_VERSION_CHECK(6, 6, 0) 52 | static void setTabOrder(std::initializer_list widgets); 53 | #endif 54 | 55 | public slots: 56 | /// Show or raise the dialog 57 | void reactivate(); 58 | /// Set the status line of the dialog window 59 | void setStatusMessage(const QString& msg); 60 | /// Return to the dialog after a failed apply 61 | void applyFailed(const QString& errorMessage); 62 | 63 | protected: 64 | /// (Re-)Load data in the dialog 65 | /*! \sa buttonClicked */ 66 | virtual void load() {} 67 | /// Check data in the dialog before accepting 68 | /*! \sa apply, buttonClicked */ 69 | virtual bool validate() { return true; } 70 | /// Apply changes and close the dialog 71 | /*! 72 | * This method is invoked upon clicking the "apply" button (by default 73 | * it's the one with `AcceptRole`), if validate() returned true. 74 | * \sa buttonClicked, validate 75 | */ 76 | virtual void apply() { accept(); } 77 | /// React to a click of a button in the dialog box 78 | /*! 79 | * This virtual function is invoked every time one of push buttons 80 | * in the dialog button box is clicked; it defines how the dialog reacts 81 | * to each button. By default, it calls validate() and, if it succeeds, 82 | * apply() on buttons with `AcceptRole`; cancels the dialog on 83 | * `RejectRole`; and reloads the fields on `ResetRole`. Override this 84 | * method to change this behaviour. 85 | * \sa validate, apply, reject, load 86 | */ 87 | virtual void buttonClicked(QAbstractButton* button); 88 | 89 | QDialogButtonBox* buttonBox() const { return buttons; } 90 | QLabel* statusLine() const { return statusLabel; } 91 | 92 | void setPendingApplyMessage(const QString& msg) { pendingApplyMessage = msg; } 93 | 94 | private: 95 | QString pendingApplyMessage; 96 | 97 | QLabel* statusLabel; 98 | QDialogButtonBox* buttons; 99 | 100 | QVBoxLayout outerLayout; 101 | }; 102 | -------------------------------------------------------------------------------- /client/dockmodemenu.cpp: -------------------------------------------------------------------------------- 1 | #include "dockmodemenu.h" 2 | 3 | #include 4 | #if QT_VERSION_MAJOR >= 6 5 | # include 6 | #endif 7 | 8 | DockModeMenu::DockModeMenu(QString name, QDockWidget* w) 9 | : QMenu(name) 10 | , dockWidget(w) 11 | , offAction(addAction(tr("&Off", "The dock panel is hidden"), 12 | [this] { dockWidget->setVisible(false); })) 13 | , dockedAction(addAction(tr("&Docked"), 14 | [this] { 15 | dockWidget->setVisible(true); 16 | dockWidget->setFloating(false); 17 | })) 18 | , floatingAction(addAction( 19 | tr("&Floating", "The dock panel is floating, aka undocked"), [this] { 20 | dockWidget->setVisible(true); 21 | dockWidget->setFloating(true); 22 | })) 23 | { 24 | offAction->setStatusTip(tr("Completely hide this list")); 25 | offAction->setCheckable(true); 26 | dockedAction->setStatusTip(tr("The list is shown within the main window")); 27 | dockedAction->setCheckable(true); 28 | floatingAction->setStatusTip( 29 | tr("The list is shown separately from the main window")); 30 | floatingAction->setCheckable(true); 31 | auto* radioGroup = new QActionGroup(this); 32 | for (auto* a : { offAction, dockedAction, floatingAction }) 33 | radioGroup->addAction(a); 34 | connect(dockWidget, &QDockWidget::visibilityChanged, this, 35 | &DockModeMenu::updateMode); 36 | connect(dockWidget, &QDockWidget::topLevelChanged, this, 37 | &DockModeMenu::updateMode); 38 | updateMode(); 39 | } 40 | 41 | void DockModeMenu::updateMode() 42 | { 43 | if (dockWidget->isHidden()) 44 | offAction->setChecked(true); 45 | else if (dockWidget->isFloating()) 46 | floatingAction->setChecked(true); 47 | else 48 | dockedAction->setChecked(true); 49 | } 50 | -------------------------------------------------------------------------------- /client/dockmodemenu.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | class QDockWidget; 6 | 7 | class DockModeMenu : public QMenu { 8 | Q_OBJECT 9 | public: 10 | DockModeMenu(QString name, QDockWidget* w); 11 | 12 | private slots: 13 | void updateMode(); 14 | 15 | private: 16 | QDockWidget* dockWidget; 17 | QAction* offAction; 18 | QAction* dockedAction; 19 | QAction* floatingAction; 20 | }; 21 | -------------------------------------------------------------------------------- /client/htmlfilter.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | // #include // For Q_NAMESPACE and Q_DECLARE_METATYPE 6 | 7 | namespace Quotient { 8 | class Room; 9 | } 10 | 11 | namespace HtmlFilter { 12 | Q_NAMESPACE 13 | 14 | enum Option : unsigned char { 15 | Default = 0x0, 16 | //! Treat `` contents as Markdown (toMatrixHtml() only) 17 | ConvertMarkdown = 0x1, 18 | //! Treat `` contents as a fragment in a bigger HTML payload 19 | //! (suppresses markup processing inside HTML elements and `` 20 | //! conversion - toMatrixHtml() only) 21 | Fragment = 0x2, 22 | //! Stop at tags not allowed in Matrix, instead of ignoring them 23 | //! (from*Html() functions only) 24 | Validate = 0x4, 25 | //! Remove elements previously used for reply fallbacks 26 | StripMxReply = 0x8 27 | }; 28 | Q_ENUM_NS(Option) 29 | Q_DECLARE_FLAGS(Options, Option) 30 | 31 | struct Context { 32 | Quotient::Room* room; 33 | Quotient::EventId eventId{}; 34 | }; 35 | 36 | /*! \brief Result structure for HTML parsing 37 | * 38 | * This is the return type of from*Html() functions, which, unlike 39 | * toMatrixHtml(), can't assume that HTML it receives is valid since it either 40 | * comes from the wire or a user input and therefore need a means to report 41 | * an error when the parser cannot cope (most often because of incorrectly 42 | * closed tags but also if plain incorrect HTML is passed). 43 | * 44 | * \sa fromMatrixHtml(), fromLocalHtml() 45 | */ 46 | struct Result { 47 | Q_GADGET 48 | Q_PROPERTY(QString filteredHtml MEMBER filteredHtml CONSTANT) 49 | Q_PROPERTY(QString::size_type errorPos MEMBER errorPos CONSTANT) 50 | Q_PROPERTY(QString errorString MEMBER errorString CONSTANT) 51 | 52 | public: 53 | /// HTML that the filter managed to produce (incomplete in case of error) 54 | QString filteredHtml {}; 55 | /// The position at which the first error was encountered; -1 if no error 56 | QString::size_type errorPos = -1; 57 | /// The human-readable error message; empty if no error 58 | QString errorString {}; 59 | }; 60 | 61 | /*! \brief Convert user input to Matrix-flavoured HTML 62 | * 63 | * This function takes user input in \p markup and converts it to the Matrix 64 | * flavour of HTML. The text in \p markup is treated as-if taken from 65 | * QTextDocument[Fragment]::toHtml(); however, the body of this HTML is itself 66 | * treated as (HTML-encoded) markup as well, in assumption that rich text 67 | * (in QTextDocument sense) is exported as the outer level of HTML while 68 | * the user adds their own HTML inside that rich text. The function decodes 69 | * and merges the two levels of markup before converting the resulting HTML 70 | * to its Matrix flavour. 71 | * 72 | * When compiling with Qt 5.14 or newer, it is possible to pass ConvertMarkdown 73 | * in \p options in order to handle the user's markup as a mix of Markdown and 74 | * HTML. In that case the function will first turn the Markdown parts to HTML 75 | * and then merge the resulting HTML snippets with the outer markup. 76 | * 77 | * The function removes HTML tags disallowed in Matrix; on top of that, 78 | * it cleans away extra parts (DTD, `head`, top-level `p`, extra `span` 79 | * inside hyperlinks etc.) added by Qt when exporting QTextDocument 80 | * to HTML, and converts some formatting that can be represented in Matrix 81 | * to tags and attributes allowed by the CS API spec. 82 | * 83 | * \note This function assumes well-formed XHTML produced by Qt classes; while 84 | * it corrects unescaped ampersands (`&`) it does not try to turn HTML 85 | * to XHTML, as from*Html() functions do. In case of an error, debug 86 | * builds will fail on assertion, release builds will silently stop 87 | * processing and return what could be processed so far. 88 | * 89 | * \sa 90 | * https://matrix.org/docs/spec/client_server/latest#m-room-message-msgtypes 91 | */ 92 | QString toMatrixHtml(const QString& markup, const Context& context, Options options = Default); 93 | 94 | /*! \brief Make the received HTML with Matrix attributes compatible with Qt 95 | * 96 | * Similar to toMatrixHtml(), this function removes HTML tags disallowed in 97 | * Matrix and cleans away extraneous HTML parts but it does the reverse 98 | * conversion of Matrix-specific attributes to HTML subset that Qt supports. 99 | * It can deal with a few more irregularities compared to toMatrixHtml(), but 100 | * still doesn't recover from, e.g., missing closing tags except those usually 101 | * not closed in HTML (`br` etc.). In case of an irrecoverable error 102 | * the returned structure will contain the error details (position and brief 103 | * description), along with whatever HTML the function managed to produce before 104 | * the failure. 105 | * 106 | * \param matrixHtml text in Matrix HTML that should be converted to Qt HTML 107 | * \param context optional room context 108 | * \param options whether the algorithm should stop at disallowed HTML tags 109 | * rather than ignore them and try to continue 110 | * \sa Result 111 | * \sa 112 | * https://matrix.org/docs/spec/client_server/latest#m-room-message-msgtypes 113 | */ 114 | Result fromMatrixHtml(const QString& matrixHtml, const Context& context, Options options = Default); 115 | 116 | /*! \brief Make the received generic HTML compatible with Qt and convertible 117 | * to Matrix 118 | * 119 | * This function is similar to fromMatrixHtml() in that it produces HTML that 120 | * can be fed to Qt components - QTextDocument[Fragment]::fromHtml(), 121 | * in particular; it also uses the same way to tackle irregularities and errors 122 | * in HTML and removes tags and attributes that cannot be converted to Matrix. 123 | * Unlike fromMatrixHtml() that accepts Matrix-flavoured HTML, this function 124 | * accepts generic HTML and allows a few exceptions compared to the Matrix spec 125 | * recommendations for HTML; specifically, it preserves the `head` element; 126 | * and `id`, `class`, and `style` attributes throughout HTML are not restricted, 127 | * allowing generic CSS stuff to do its job inasmuch as Qt supports that. 128 | * 129 | * The case for this function is loading a piece of external HTML into a Qt 130 | * component in anticipation that this piece will later be translated to Matrix 131 | * HTML - e.g. drag-n-drop/clipboard paste into the message input control. 132 | * 133 | * \sa fromMatrixHtml 134 | */ 135 | Result fromLocalHtml(const QString& html, const Context& context, Options options = Fragment); 136 | } 137 | Q_DECLARE_METATYPE(HtmlFilter::Result) 138 | -------------------------------------------------------------------------------- /client/kchatedit.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2017 Elvis Angelaccio 3 | * 4 | * SPDX-License-Identifier: GPL-3.0-or-later 5 | */ 6 | 7 | #include "kchatedit.h" 8 | 9 | #include 10 | #include 11 | #include 12 | 13 | class KChatEdit::KChatEditPrivate 14 | { 15 | public: 16 | QString getDocumentText(QTextDocument* doc) const; 17 | void updateAndMoveInHistory(int increment); 18 | void saveInput(); 19 | 20 | QTextDocument* makeDocument() 21 | { 22 | Q_ASSERT(contextKey); 23 | return new QTextDocument(contextKey); 24 | } 25 | 26 | void setContext(QObject* newContextKey) 27 | { 28 | contextKey = newContextKey; 29 | auto& context = contexts[contextKey]; // Create if needed 30 | auto& history = context.history; 31 | // History always ends with a placeholder that is initially empty 32 | // but may be filled with tentative input when the user entered 33 | // something and then went out for history. 34 | if (history.isEmpty() || !history.last()->isEmpty()) 35 | history.push_back(makeDocument()); 36 | 37 | while (history.size() > maxHistorySize) 38 | delete history.takeFirst(); 39 | index = history.size() - 1; 40 | 41 | // QTextDocuments are parented to the context object, so are destroyed 42 | // automatically along with it; but the hashmap should be cleaned up 43 | if (newContextKey != q) 44 | QObject::connect(newContextKey, &QObject::destroyed, q, 45 | [this, newContextKey] { 46 | contexts.remove(newContextKey); 47 | }); 48 | Q_ASSERT(contexts.contains(newContextKey) && !history.empty()); 49 | } 50 | 51 | KChatEdit* q = nullptr; 52 | QObject* contextKey = nullptr; 53 | 54 | struct Context { 55 | QVector history; 56 | QTextDocument* cachedInput = nullptr; 57 | }; 58 | QHash contexts; 59 | 60 | int index = 0; 61 | int maxHistorySize = 100; 62 | QTextBlockFormat defaultBlockFmt; 63 | }; 64 | 65 | QString KChatEdit::KChatEditPrivate::getDocumentText(QTextDocument* doc) const 66 | { 67 | Q_ASSERT(doc); 68 | return q->acceptRichText() ? doc->toHtml() : doc->toPlainText(); 69 | } 70 | 71 | void KChatEdit::KChatEditPrivate::updateAndMoveInHistory(int increment) 72 | { 73 | Q_ASSERT(contexts.contains(contextKey)); 74 | auto& history = contexts.find(contextKey)->history; 75 | Q_ASSERT(index >= 0 && index < history.size()); 76 | if (index + increment < 0 || index + increment >= history.size()) 77 | return; // Prevent stepping out of bounds 78 | auto& historyItem = history[index]; 79 | 80 | // Only save input if different from the latest one. 81 | if (q->document() != historyItem /* shortcut expensive getDocumentText() */ 82 | && getDocumentText(q->document()) != getDocumentText(historyItem)) 83 | historyItem = q->document(); 84 | 85 | // Fill the input with a copy of the history entry at a new index 86 | q->setDocument(history.at(index += increment)->clone(contextKey)); 87 | q->moveCursor(QTextCursor::End); 88 | } 89 | 90 | void KChatEdit::KChatEditPrivate::saveInput() 91 | { 92 | if (q->document()->isEmpty()) 93 | return; 94 | 95 | Q_ASSERT(contexts.contains(contextKey)); 96 | auto& history = contexts.find(contextKey)->history; 97 | // Only save input if different from the latest one or from the history. 98 | const auto input = getDocumentText(q->document()); 99 | if (index < history.size() - 1 100 | && input == getDocumentText(history[index])) { 101 | // Take the history entry and move it to the most recent position (but 102 | // before the placeholder). 103 | history.move(index, history.size() - 2); 104 | emit q->savedInputChanged(); 105 | } else if (input != getDocumentText(q->savedInput())) { 106 | // Insert a copy of the edited text just before the placeholder 107 | history.insert(history.end() - 1, q->document()); 108 | q->setDocument(makeDocument()); 109 | 110 | if (history.size() >= maxHistorySize) { 111 | delete history.takeFirst(); 112 | } 113 | emit q->savedInputChanged(); 114 | } 115 | 116 | index = history.size() - 1; 117 | q->clear(); 118 | q->resetCurrentFormat(); 119 | } 120 | 121 | KChatEdit::KChatEdit(QWidget *parent) 122 | : QTextEdit(parent), d(new KChatEditPrivate) 123 | { 124 | setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Maximum); 125 | connect(this, &QTextEdit::textChanged, this, &QWidget::updateGeometry); 126 | d->q = this; // KChatEdit initialization complete, pimpl can use it 127 | 128 | d->setContext(this); // A special context that always exists 129 | setDocument(d->makeDocument()); 130 | d->defaultBlockFmt = textCursor().blockFormat(); 131 | } 132 | 133 | KChatEdit::~KChatEdit() = default; 134 | 135 | QTextDocument* KChatEdit::savedInput() const 136 | { 137 | Q_ASSERT(d->contexts.contains(d->contextKey)); 138 | auto& history = d->contexts.find(d->contextKey)->history; 139 | if (history.size() >= 2) 140 | return history.at(history.size() - 2); 141 | 142 | Q_ASSERT(history.size() == 1); 143 | return history.front(); 144 | } 145 | 146 | void KChatEdit::saveInput() 147 | { 148 | d->saveInput(); 149 | } 150 | 151 | QVector KChatEdit::history() const 152 | { 153 | Q_ASSERT(d->contexts.contains(d->contextKey)); 154 | return d->contexts.value(d->contextKey).history; 155 | } 156 | 157 | int KChatEdit::maxHistorySize() const 158 | { 159 | return d->maxHistorySize; 160 | } 161 | 162 | void KChatEdit::setMaxHistorySize(int newMaxSize) 163 | { 164 | if (d->maxHistorySize != newMaxSize) { 165 | d->maxHistorySize = newMaxSize; 166 | emit maxHistorySizeChanged(); 167 | } 168 | } 169 | 170 | void KChatEdit::switchContext(QObject* contextKey) 171 | { 172 | if (!contextKey) 173 | contextKey = this; 174 | if (d->contextKey == contextKey) 175 | return; 176 | 177 | Q_ASSERT(d->contexts.contains(d->contextKey)); 178 | d->contexts.find(d->contextKey)->cachedInput = 179 | document()->isEmpty() ? nullptr : document(); 180 | d->setContext(contextKey); 181 | auto& cachedInput = d->contexts.find(d->contextKey)->cachedInput; 182 | setDocument(cachedInput ? cachedInput : d->makeDocument()); 183 | moveCursor(QTextCursor::End); 184 | emit contextSwitched(); 185 | } 186 | 187 | void KChatEdit::resetCurrentFormat() 188 | { 189 | auto c = textCursor(); 190 | c.setCharFormat({}); 191 | c.setBlockFormat(d->defaultBlockFmt); 192 | setTextCursor(c); 193 | } 194 | 195 | QSize KChatEdit::minimumSizeHint() const 196 | { 197 | QSize minimumSizeHint = QTextEdit::minimumSizeHint(); 198 | QMargins margins; 199 | margins += static_cast(document()->documentMargin()); 200 | margins += contentsMargins(); 201 | 202 | if (!placeholderText().isEmpty()) { 203 | minimumSizeHint.setWidth(int( 204 | fontMetrics().boundingRect(placeholderText()).width() 205 | + margins.left()*2.5)); 206 | } 207 | if (document()->isEmpty()) { 208 | minimumSizeHint.setHeight(fontMetrics().lineSpacing() + margins.top() + margins.bottom()); 209 | } else { 210 | minimumSizeHint.setHeight(int(document()->size().height())); 211 | } 212 | 213 | return minimumSizeHint; 214 | } 215 | 216 | QSize KChatEdit::sizeHint() const 217 | { 218 | ensurePolished(); 219 | 220 | if (document()->isEmpty()) { 221 | return minimumSizeHint(); 222 | } 223 | 224 | QMargins margins; 225 | margins += static_cast(document()->documentMargin()); 226 | margins += contentsMargins(); 227 | 228 | QSize size = document()->size().toSize(); 229 | size.rwidth() += margins.left() + margins.right(); 230 | size.rheight() += margins.top() + margins.bottom(); 231 | 232 | // Be consistent with minimumSizeHint(). 233 | if (document()->lineCount() == 1 && !toPlainText().contains('\n')) { 234 | size.setHeight(fontMetrics().lineSpacing() + margins.top() + margins.bottom()); 235 | } 236 | 237 | return size; 238 | } 239 | 240 | void KChatEdit::keyPressEvent(QKeyEvent *event) 241 | { 242 | if (event->matches(QKeySequence::Copy)) { 243 | emit copyRequested(); 244 | return; 245 | } 246 | 247 | switch (event->key()) { 248 | case Qt::Key_Enter: 249 | case Qt::Key_Return: 250 | if (!(QGuiApplication::keyboardModifiers() & Qt::ShiftModifier)) { 251 | emit returnPressed(); 252 | return; 253 | } 254 | break; 255 | case Qt::Key_Up: 256 | if (!textCursor().movePosition(QTextCursor::Up)) { 257 | d->updateAndMoveInHistory(-1); 258 | } 259 | break; 260 | case Qt::Key_Down: 261 | if (!textCursor().movePosition(QTextCursor::Down)) { 262 | d->updateAndMoveInHistory(+1); 263 | } 264 | break; 265 | default: 266 | break; 267 | } 268 | 269 | QTextEdit::keyPressEvent(event); 270 | } 271 | 272 | -------------------------------------------------------------------------------- /client/kchatedit.h: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2017 Elvis Angelaccio 3 | * SPDX-FileCopyrightText: 2020 The Quotient project 4 | * 5 | * SPDX-License-Identifier: GPL-3.0-or-later 6 | */ 7 | 8 | #pragma once 9 | 10 | #include 11 | 12 | /** 13 | * @class KChatEdit kchatedit.h KChatEdit 14 | * 15 | * @brief An input widget with history for chat applications. 16 | * 17 | * This widget can be used to get input for chat windows, which typically 18 | * corresponds to chat messages or protocol-specific commands (for example the 19 | * "/whois" IRC command). 20 | * 21 | * By default the widget takes as little space as possible, which is the same 22 | * space as used by a QLineEdit. It is possible to expand the widget and enter 23 | * "multi-line" messages, by pressing Shift + Return. 24 | * 25 | * Chat applications usually maintain a history of what the user typed, which 26 | * can be browsed with the Up and Down keys (exactly like in command-line 27 | * shells). This feature is fully supported by this widget. The widget emits the 28 | * inputRequested() signal upon pressing the Return key. You can then call 29 | * saveInput() to make the input text disappear, as typical in chat 30 | * applications. The input goes in the history and can be retrieved with the 31 | * savedInput() method. 32 | * 33 | * @author Elvis Angelaccio 34 | * @author Kitsune Ral 35 | */ 36 | class KChatEdit : public QTextEdit 37 | { 38 | Q_OBJECT 39 | Q_PROPERTY(QTextDocument* savedInput READ savedInput NOTIFY savedInputChanged) 40 | Q_PROPERTY(int maxHistorySize READ maxHistorySize WRITE setMaxHistorySize NOTIFY maxHistorySizeChanged) 41 | 42 | public: 43 | explicit KChatEdit(QWidget *parent = nullptr); 44 | ~KChatEdit() override; 45 | 46 | /** 47 | * The latest input text saved in the history. 48 | * This corresponds to the last element of history(). 49 | * @return Latest available input or an empty document if saveInput() has not been called yet. 50 | * @see inputChanged(), saveInput(), history() 51 | */ 52 | QTextDocument* savedInput() const; 53 | 54 | /** 55 | * Saves in the history the current document(). 56 | * This also clears the QTextEdit area. 57 | * @note If the history is full (see maxHistorySize(), new inputs will take space from the oldest 58 | * items in the history. 59 | * @see savedInput(), history(), maxHistorySize() 60 | */ 61 | void saveInput(); 62 | 63 | /** 64 | * @return The history of the text inputs that the user typed. 65 | * @see savedInput(), saveInput(); 66 | */ 67 | QVector history() const; 68 | 69 | /** 70 | * @return The maximum number of input items that the history can store. 71 | * @see history() 72 | */ 73 | int maxHistorySize() const; 74 | 75 | /** 76 | * Set the maximum number of input items that the history can store. 77 | * @see maxHistorySize() 78 | */ 79 | void setMaxHistorySize(int newMaxSize); 80 | 81 | QSize minimumSizeHint() const Q_DECL_OVERRIDE; 82 | QSize sizeHint() const Q_DECL_OVERRIDE; 83 | 84 | public Q_SLOTS: 85 | /** 86 | * @brief Switch the context (e.g., a chat room) of the widget 87 | * 88 | * This clears the current entry and the history of the chat edit 89 | * and replaces them with the entry and the history for the object 90 | * passed as a parameter, if there are any. 91 | */ 92 | virtual void switchContext(QObject* contextKey); 93 | 94 | /** 95 | * @brief Reset the current character(s) formatting 96 | * 97 | * This is equivalent to calling `setCurrentCharFormat({})`. 98 | */ 99 | void resetCurrentFormat(); 100 | 101 | Q_SIGNALS: 102 | /** 103 | * A new input has been saved in the history. 104 | * @see savedInput(), saveInput(), history() 105 | */ 106 | void savedInputChanged(); 107 | 108 | /** 109 | * Emitted when the user types Key_Return or Key_Enter, which typically means the user 110 | * wants to "send" what was typed. Call saveInput() if you want to actually save the input. 111 | * @see savedInput(), saveInput(), history() 112 | */ 113 | void returnPressed(); 114 | 115 | /** 116 | * Emitted when the user presses Ctrl+C. 117 | */ 118 | void copyRequested(); 119 | 120 | /** A new context has been selected */ 121 | void contextSwitched(); 122 | 123 | void maxHistorySizeChanged(); 124 | 125 | protected: 126 | void keyPressEvent(QKeyEvent *event) override; 127 | 128 | private: 129 | class KChatEditPrivate; 130 | QScopedPointer d; 131 | 132 | Q_DISABLE_COPY(KChatEdit) 133 | }; 134 | -------------------------------------------------------------------------------- /client/logging_categories.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | // NB: Only include this file from .cpp 4 | 5 | #include 6 | 7 | // Reusing the macro defined in Quotient - these must never cross ways 8 | #define QUO_LOGGING_CATEGORY(Name, Id) \ 9 | inline Q_LOGGING_CATEGORY((Name), (Id), QtInfoMsg) 10 | 11 | namespace { 12 | QUO_LOGGING_CATEGORY(MAIN, "quaternion.main") 13 | QUO_LOGGING_CATEGORY(ACCOUNTSELECTOR, "quaternion.accountselector") 14 | QUO_LOGGING_CATEGORY(MODELS, "quaternion.models") 15 | QUO_LOGGING_CATEGORY(EVENTMODEL, "quaternion.models.events") 16 | QUO_LOGGING_CATEGORY(TIMELINE, "quaternion.timeline") 17 | QUO_LOGGING_CATEGORY(HTMLFILTER, "quaternion.htmlfilter") 18 | QUO_LOGGING_CATEGORY(MSGINPUT, "quaternion.messageinput") 19 | QUO_LOGGING_CATEGORY(THUMBNAILS, "quaternion.thumbnails") 20 | 21 | // Only to be used in QML; shows up here for documentation purpose only 22 | [[maybe_unused]] QUO_LOGGING_CATEGORY(TIMELINEQML, "quaternion.timeline.qml") 23 | } 24 | -------------------------------------------------------------------------------- /client/logindialog.h: -------------------------------------------------------------------------------- 1 | /************************************************************************** 2 | * * 3 | * SPDX-FileCopyrightText: 2015 Felix Rohrbach * 4 | * * 5 | * SPDX-License-Identifier: GPL-3.0-or-later 6 | * * 7 | **************************************************************************/ 8 | 9 | #pragma once 10 | 11 | #include "dialog.h" 12 | 13 | #include 14 | 15 | class QLineEdit; 16 | class QCheckBox; 17 | 18 | namespace Quotient { 19 | class AccountSettings; 20 | class AccountRegistry; 21 | } 22 | 23 | static const auto E2eeEnabledSetting = QStringLiteral("enable_e2ee"); 24 | 25 | class LoginDialog : public Dialog { 26 | Q_OBJECT 27 | public: 28 | // FIXME: make loggedInAccounts pointer to const once we get to 29 | // libQuotient 0.8 30 | LoginDialog(const QString& statusMessage, 31 | Quotient::AccountRegistry* loggedInAccounts, QWidget* parent, 32 | const QStringList& knownAccounts = {}); 33 | LoginDialog(const QString& statusMessage, 34 | const Quotient::AccountSettings& reloginAccount, 35 | QWidget* parent); 36 | void setup(const QString& statusMessage); 37 | 38 | Quotient::Connection* releaseConnection(); 39 | QString deviceName() const; 40 | bool keepLoggedIn() const; 41 | 42 | private slots: 43 | void apply() override; 44 | void loginWithBestFlow(); 45 | void loginWithPassword(); 46 | void loginWithSso(); 47 | 48 | private: 49 | QLineEdit* userEdit; 50 | QLineEdit* passwordEdit; 51 | QLineEdit* initialDeviceName; 52 | QLineEdit* deviceId; 53 | QLineEdit* serverEdit; 54 | QCheckBox* saveTokenCheck; 55 | 56 | Quotient::QObjectHolder m_connection; 57 | }; 58 | -------------------------------------------------------------------------------- /client/main.cpp: -------------------------------------------------------------------------------- 1 | /************************************************************************** 2 | * * 3 | * SPDX-FileCopyrightText: 2015 Felix Rohrbach * 4 | * * 5 | * SPDX-License-Identifier: GPL-3.0-or-later 6 | * * 7 | **************************************************************************/ 8 | 9 | #include "desktop_integration.h" 10 | #include "logging_categories.h" 11 | #include "mainwindow.h" 12 | 13 | #include 14 | #include 15 | 16 | #include 17 | #include 18 | #include 19 | 20 | #include 21 | #include 22 | #include 23 | #include 24 | 25 | using namespace Qt::StringLiterals; 26 | 27 | namespace { 28 | inline void loadTranslations() 29 | { 30 | // Extract a number from another macro and turn it to a const char[] 31 | #define ITOA(i) #i 32 | static const auto translationConfigs = std::to_array>( 33 | { { { u"qt"_s, u"qtbase"_s, u"qtnetwork"_s, u"qtdeclarative"_s, u"qtmultimedia"_s, 34 | u"qtquickcontrols"_s, u"qtquickcontrols2"_s, 35 | // QtKeychain tries to install its translations to Qt's path; 36 | // try to look there, just in case (see also below) 37 | u"qtkeychain"_s }, 38 | QLibraryInfo::path(QLibraryInfo::TranslationsPath) }, 39 | { { u"qtkeychain"_s }, 40 | QStandardPaths::locate(QStandardPaths::GenericDataLocation, 41 | u"qt" ITOA(QT_VERSION_MAJOR) "keychain/translations"_s, 42 | QStandardPaths::LocateDirectory) }, 43 | { { u"qt"_s, u"qtkeychain"_s, u"quotient"_s, AppName }, 44 | QStandardPaths::locate(QStandardPaths::AppLocalDataLocation, u"translations"_s, 45 | QStandardPaths::LocateDirectory) } }); 46 | #undef ITOA 47 | 48 | for (const auto& [configNames, configPath] : translationConfigs) 49 | for (const auto& configName : configNames) { 50 | auto translator = std::make_unique(); 51 | // Check the current directory then configPath 52 | if (translator->load(QLocale(), configName, u"_"_s) 53 | || translator->load(QLocale(), configName, u"_"_s, configPath)) { 54 | auto path = translator->filePath(); 55 | if (QApplication::installTranslator(translator.get())) { 56 | qCDebug(MAIN).noquote() << "Loaded translations from" << path; 57 | translator.release()->setParent(qApp); // Change pointer ownership 58 | } else 59 | qCWarning(MAIN).noquote() << "Failed to load translations from" << path; 60 | } else 61 | qCDebug(MAIN) << "No translations for" << configName << "at" << configPath; 62 | } 63 | } 64 | } 65 | 66 | int main( int argc, char* argv[] ) 67 | { 68 | QApplication::setOrganizationName(u"Quotient"_s); 69 | QApplication::setApplicationName(AppName); 70 | QApplication::setApplicationDisplayName(u"Quaternion"_s); 71 | QApplication::setApplicationVersion(u"0.0.97.1 (+git)"_s); 72 | QApplication::setDesktopFileName(AppId); 73 | 74 | using Quotient::Settings; 75 | Settings::setLegacyNames(u"QMatrixClient"_s, u"quaternion"_s); 76 | Settings settings; 77 | 78 | QApplication app(argc, argv); 79 | #if defined Q_OS_UNIX && !defined Q_OS_MAC 80 | // #681: When in Flatpak and unless overridden by configuration, set 81 | // the style to Breeze as it looks much fresher than Fusion that Qt 82 | // applications default to in Flatpak outside KDE. Although Qt docs 83 | // recommend to call setStyle() before constructing a QApplication object 84 | // (to make sure the style's palette is applied?) that doesn't work with 85 | // Breeze because it seems to make use of platform theme hints, which 86 | // in turn need a created QApplication object (see #700). 87 | const auto useBreezeStyle = settings.get("UI/use_breeze_style", inFlatpak()); 88 | if (useBreezeStyle) { 89 | QApplication::setStyle("Breeze"); 90 | QIcon::setThemeName("breeze"); 91 | QIcon::setFallbackThemeName("breeze"); 92 | } else 93 | #endif 94 | { 95 | QQuickStyle::setFallbackStyle(u"Fusion"_s); // Looks better on desktops 96 | // QQuickStyle::setStyle("Material"); 97 | } 98 | 99 | { 100 | auto font = QApplication::font(); 101 | if (const auto fontFamily = settings.get("UI/Fonts/family"); 102 | !fontFamily.isEmpty()) 103 | font.setFamily(fontFamily); 104 | 105 | if (const auto fontPointSize = 106 | settings.value("UI/Fonts/pointSize").toReal(); 107 | fontPointSize > 0) 108 | font.setPointSizeF(fontPointSize); 109 | 110 | qCInfo(MAIN) << "Using application font:" << font.toString(); 111 | QApplication::setFont(font); 112 | } 113 | 114 | QCommandLineParser parser; 115 | parser.setApplicationDescription(QApplication::translate("main", 116 | "Quaternion - an IM client for the Matrix protocol")); 117 | parser.addHelpOption(); 118 | parser.addVersionOption(); 119 | 120 | QList options; 121 | QCommandLineOption locale { QStringLiteral("locale"), 122 | QApplication::translate("main", "Override locale"), 123 | QApplication::translate("main", "locale") }; 124 | options.append(locale); 125 | QCommandLineOption hideMainWindow { QStringLiteral("hide-mainwindow"), 126 | QApplication::translate("main", "Hide main window on startup") }; 127 | options.append(hideMainWindow); 128 | // Add more command line options before this line 129 | 130 | if (!parser.addOptions(options)) 131 | Q_ASSERT_X(false, __FUNCTION__, 132 | "Command line options are improperly defined, fix the code"); 133 | parser.process(app); 134 | 135 | const auto overrideLocale = parser.value(locale); 136 | if (!overrideLocale.isEmpty()) 137 | { 138 | QLocale::setDefault(QLocale(overrideLocale)); 139 | qCInfo(MAIN) << "Using locale" << QLocale().name(); 140 | } 141 | 142 | loadTranslations(); 143 | 144 | Quotient::NetworkSettings().setupApplicationProxy(); 145 | Quotient::Connection::setEncryptionDefault(true); 146 | 147 | MainWindow window; 148 | if (parser.isSet(hideMainWindow)) { 149 | qCDebug(MAIN) << "--- Hide time!"; 150 | window.hide(); 151 | } 152 | else { 153 | qCDebug(MAIN) << "--- Show time!"; 154 | window.show(); 155 | } 156 | 157 | return QApplication::exec(); 158 | } 159 | 160 | -------------------------------------------------------------------------------- /client/mainwindow.h: -------------------------------------------------------------------------------- 1 | /************************************************************************** 2 | * * 3 | * SPDX-FileCopyrightText: 2015 Felix Rohrbach * 4 | * * 5 | * SPDX-License-Identifier: GPL-3.0-or-later 6 | * * 7 | **************************************************************************/ 8 | 9 | #pragma once 10 | 11 | // QSslError is used in a signal container parameter and needs to be complete 12 | // for moc to generate stuff since Qt 6 13 | #include 14 | #include 15 | 16 | #include 17 | #include 18 | 19 | namespace Quotient { 20 | class Room; 21 | class Connection; 22 | class AccountSettings; 23 | } 24 | 25 | class RoomListDock; 26 | class UserListDock; 27 | class ChatRoomWidget; 28 | class SystemTrayIcon; 29 | class QuaternionRoom; 30 | class LoginDialog; 31 | 32 | class QAction; 33 | class QMenu; 34 | class QMenuBar; 35 | class QSystemTrayIcon; 36 | class QMovie; 37 | class QLabel; 38 | class QLineEdit; 39 | 40 | class QNetworkReply; 41 | class QNetworkProxy; 42 | class QAuthenticator; 43 | 44 | class MainWindow: public QMainWindow, public Quotient::UriResolverBase { 45 | Q_OBJECT 46 | public: 47 | using Connection = Quotient::Connection; 48 | 49 | MainWindow(); 50 | ~MainWindow() override; 51 | 52 | void addConnection(Connection* c); 53 | void dropConnection(Connection* c); 54 | 55 | Quotient::AccountRegistry* registry() { return accountRegistry; } 56 | 57 | // For openUserInput() 58 | enum : bool { NoRoomJoining = false, ForJoining = true }; 59 | 60 | public slots: 61 | /// Open non-empty id or URI using the specified action hint 62 | /*! Asks the user to choose the connection if necessary */ 63 | void openResource(const QString& idOrUri, const QString& action = {}); 64 | /// Open a dialog to enter the resource id/URI and then navigate to it 65 | void openUserInput(bool forJoining = NoRoomJoining); 66 | /// Open/focus the room settings dialog 67 | /*! If \p r is empty, the currently open room is used */ 68 | void openRoomSettings(QuaternionRoom* r = nullptr); 69 | void selectRoom(Quotient::Room* r); 70 | void showStatusMessage(const QString& message, int timeout = 0); 71 | QFuture logout(Connection* c); 72 | 73 | private slots: 74 | void invokeLogin(); 75 | 76 | void reloginNeeded(Connection* c, const QString& message = {}); 77 | void networkError(Connection* c); 78 | void sslErrors(const QPointer& reply, 79 | const QList& errors); 80 | void proxyAuthenticationRequired(const QNetworkProxy& /* unused */, 81 | QAuthenticator* auth); 82 | 83 | void showLoginWindow(const QString& statusMessage = {}); 84 | void showLoginWindow(const QString& statusMessage, 85 | const QString& userId); 86 | void showAboutWindow(); 87 | 88 | // UriResolverBase overrides 89 | Quotient::UriResolveResult visitUser(Quotient::User* user, 90 | const QString& action) override; 91 | void visitRoom(Quotient::Room* room, const QString& eventId) override; 92 | void joinRoom(Quotient::Connection* account, 93 | const QString& roomAliasOrId, 94 | const QStringList& viaServers = {}) override; 95 | bool visitNonMatrix(const QUrl& url) override; 96 | 97 | private: 98 | Quotient::AccountRegistry* accountRegistry = 99 | new Quotient::AccountRegistry(this); 100 | QVector logoutOnExit; 101 | 102 | RoomListDock* roomListDock = nullptr; 103 | UserListDock* userListDock = nullptr; 104 | ChatRoomWidget* chatRoomWidget = nullptr; 105 | 106 | QMovie* busyIndicator = nullptr; 107 | QLabel* busyLabel = nullptr; 108 | 109 | QMenu* connectionMenu = nullptr; 110 | QMenu* logoutMenu = nullptr; 111 | QAction* openRoomAction = nullptr; 112 | QAction* roomSettingsAction = nullptr; 113 | QAction* createRoomAction = nullptr; 114 | QAction* dcAction = nullptr; 115 | QAction* joinAction = nullptr; 116 | QAction* confirmLinksAction = nullptr; 117 | 118 | SystemTrayIcon* systemTrayIcon = nullptr; 119 | 120 | // FIXME: This will be a problem when we get ability to show 121 | // several rooms at once. 122 | QuaternionRoom* currentRoom = nullptr; 123 | 124 | void createMenu(); 125 | QAction* addUiOptionCheckbox(QMenu* parent, const QString& text, 126 | const QString& statusTip, const QString& settingsKey, 127 | bool defaultValue = false); 128 | void showInitialLoadIndicator(); 129 | void updateLoadingStatus(int accountsStillLoading); 130 | void firstSyncOver(const Connection *c); 131 | void loadSettings(); 132 | void saveSettings() const; 133 | void doOpenLoginDialog(LoginDialog* dialog); 134 | Connection* chooseConnection(Connection* connection, 135 | const QString& prompt); 136 | void showMillisToRecon(Connection* c); 137 | 138 | std::pair loadAccessToken( 139 | const Quotient::AccountSettings& account); 140 | 141 | /// Get the default connection to perform actions 142 | /*! 143 | * \return the connection of the current room; or, if there's only 144 | * one connection, that connection; failing that, nullptr 145 | */ 146 | Connection* getDefaultConnection() const; 147 | 148 | void closeEvent(QCloseEvent* event) override; 149 | }; 150 | -------------------------------------------------------------------------------- /client/models/abstractroomordering.cpp: -------------------------------------------------------------------------------- 1 | /****************************************************************************** 2 | * SPDX-FileCopyrightText: 2018-2019 QMatrixClient Project 3 | * 4 | * SPDX-License-Identifier: GPL-3.0-or-later 5 | */ 6 | 7 | #include "abstractroomordering.h" 8 | 9 | #include "roomlistmodel.h" 10 | 11 | #include 12 | using namespace std::placeholders; 13 | 14 | AbstractRoomOrdering::AbstractRoomOrdering(RoomListModel* m) 15 | : QObject(m) 16 | { } 17 | 18 | AbstractRoomOrdering::groupLessThan_closure_t 19 | AbstractRoomOrdering::groupLessThanFactory() const 20 | { 21 | return std::bind_front(&AbstractRoomOrdering::groupLessThan, this); 22 | } 23 | 24 | AbstractRoomOrdering::roomLessThan_closure_t 25 | AbstractRoomOrdering::roomLessThanFactory(const QVariant& group) const 26 | { 27 | return std::bind_front(&AbstractRoomOrdering::roomLessThan, this, group); 28 | } 29 | 30 | void AbstractRoomOrdering::updateGroups(Room* room) 31 | { 32 | model()->updateGroups(room); 33 | } 34 | 35 | RoomListModel* AbstractRoomOrdering::model() const 36 | { 37 | return static_cast(parent()); 38 | } 39 | -------------------------------------------------------------------------------- /client/models/abstractroomordering.h: -------------------------------------------------------------------------------- 1 | /****************************************************************************** 2 | * SPDX-FileCopyrightText: 2018-2019 QMatrixClient Project 3 | * 4 | * SPDX-License-Identifier: GPL-3.0-or-later 5 | */ 6 | 7 | #pragma once 8 | 9 | #include 10 | #include 11 | 12 | struct RoomGroup 13 | { 14 | QVariant key; 15 | QVector rooms; 16 | 17 | bool operator==(const RoomGroup& other) const 18 | { 19 | return key == other.key; 20 | } 21 | bool operator!=(const RoomGroup& other) const 22 | { 23 | return !(*this == other); 24 | } 25 | bool operator==(const QVariant& otherCaption) const 26 | { 27 | return key == otherCaption; 28 | } 29 | bool operator!=(const QVariant& otherCaption) const 30 | { 31 | return !(*this == otherCaption); 32 | } 33 | friend bool operator==(const QVariant& otherCaption, 34 | const RoomGroup& group) 35 | { 36 | return group == otherCaption; 37 | } 38 | friend bool operator!=(const QVariant& otherCaption, 39 | const RoomGroup& group) 40 | { 41 | return !(group == otherCaption); 42 | } 43 | 44 | static inline const auto SystemPrefix = QStringLiteral("im.quotient."); 45 | static inline const auto LegacyPrefix = QStringLiteral("org.qmatrixclient."); 46 | }; 47 | using RoomGroups = QVector; 48 | 49 | class RoomListModel; 50 | 51 | class AbstractRoomOrdering : public QObject 52 | { 53 | Q_OBJECT 54 | public: 55 | using Room = Quotient::Room; 56 | using Connection = Quotient::Connection; 57 | using groups_t = QVariantList; 58 | 59 | explicit AbstractRoomOrdering(RoomListModel* m); 60 | 61 | public: // Overridables 62 | /// Returns human-readable name of the room ordering 63 | virtual QString orderingName() const = 0; 64 | /// Returns human-readable room group caption 65 | virtual QVariant groupLabel(const RoomGroup& g) const = 0; 66 | /// Orders a group against a key of another group 67 | virtual bool groupLessThan(const QVariant& g1key, 68 | const QVariant& g2key) const = 0; 69 | /// Orders two rooms within one group 70 | virtual bool roomLessThan(const QVariant& group, 71 | const Room* r1, const Room* r2) const = 0; 72 | 73 | /// Returns the full list of room groups 74 | virtual groups_t roomGroups(const Room* room) const = 0; 75 | /// Connects order updates to signals from a new Matrix connection 76 | virtual void connectSignals(Connection* connection) = 0; 77 | /// Connects order updates to signals from a new Matrix room 78 | virtual void connectSignals(Room* room) = 0; 79 | 80 | public: 81 | using groupLessThan_closure_t = std::function; 82 | /// Returns a closure that invokes this->groupLessThan() 83 | groupLessThan_closure_t groupLessThanFactory() const; 84 | 85 | using roomLessThan_closure_t = 86 | std::function; 87 | /// Returns a closure that invokes this->roomLessThan in a given group 88 | roomLessThan_closure_t roomLessThanFactory(const QVariant& group) const; 89 | 90 | protected slots: 91 | /// A facade for derived classes to trigger RoomListModel::updateGroups 92 | virtual void updateGroups(Room* room); 93 | 94 | protected: 95 | RoomListModel* model() const; 96 | }; 97 | -------------------------------------------------------------------------------- /client/models/messageeventmodel.h: -------------------------------------------------------------------------------- 1 | /************************************************************************** 2 | * * 3 | * SPDX-FileCopyrightText: 2015 Felix Rohrbach * 4 | * * 5 | * SPDX-License-Identifier: GPL-3.0-or-later 6 | * * 7 | **************************************************************************/ 8 | 9 | #pragma once 10 | 11 | #include "../quaternionroom.h" 12 | 13 | #include 14 | 15 | class MessageEventModel: public QAbstractListModel 16 | { 17 | Q_OBJECT 18 | Q_PROPERTY(QuaternionRoom* room READ room NOTIFY roomChanged) 19 | Q_PROPERTY(int readMarkerVisualIndex READ readMarkerVisualIndex NOTIFY readMarkerUpdated) 20 | public: 21 | enum EventRoles { 22 | EventTypeRole = Qt::UserRole + 1, 23 | EventIdRole, 24 | DateTimeRole, 25 | DateRole, 26 | EventGroupingRole, 27 | AuthorRole, 28 | AuthorHasAvatarRole, 29 | ContentRole, 30 | ContentTypeRole, 31 | RepliedToRole, 32 | HighlightRole, 33 | SpecialMarksRole, 34 | LongOperationRole, 35 | AnnotationRole, 36 | RefRole, 37 | ReactionsRole, 38 | EventClassNameRole, 39 | VerificationStateRole, 40 | }; 41 | 42 | explicit MessageEventModel(QObject* parent = nullptr); 43 | 44 | QuaternionRoom* room() const; 45 | void changeRoom(QuaternionRoom* room); 46 | 47 | int rowCount(const QModelIndex& parent = QModelIndex()) const override; 48 | QVariant data(const QModelIndex& idx, int role = Qt::DisplayRole) const override; 49 | QHash roleNames() const override; 50 | int findRow(const QString& id, bool includePending = false) const; 51 | 52 | Q_INVOKABLE QColor fadedBackColor(QColor unfadedColor, qreal fadeRatio = 0.5) const; 53 | 54 | signals: 55 | void roomChanged(); 56 | /// This is different from Room::readMarkerMoved() in that it is also 57 | /// emitted when the room or the last read event is first shown 58 | void readMarkerUpdated(); 59 | 60 | private slots: 61 | int refreshEvent(const QString& eventId); 62 | void refreshRow(int row); 63 | void incomingEvents(Quotient::RoomEventsRange events, int atIndex); 64 | 65 | private: 66 | QuaternionRoom* m_currentRoom = nullptr; 67 | int readMarkerVisualIndex() const; 68 | bool movingEvent = false; 69 | 70 | int timelineBaseIndex() const; 71 | QDateTime makeMessageTimestamp(const QuaternionRoom::rev_iter_t& baseIt) const; 72 | static QString renderDate(const QDateTime& timestamp); 73 | bool isUserActivityNotable(const QuaternionRoom::rev_iter_t& baseIt) const; 74 | 75 | void refreshLastUserEvents(int baseTimelineRow); 76 | void refreshEventRoles(int row, const QVector& roles = {}); 77 | QString visualiseEvent(const Quotient::RoomEvent& evt, bool abbreviate = false) const; 78 | }; 79 | 80 | struct EventForQml { 81 | Quotient::EventId eventId; 82 | Quotient::RoomMember sender; 83 | QString content; 84 | 85 | Q_GADGET 86 | Q_PROPERTY(Quotient::EventId eventId MEMBER eventId FINAL) 87 | Q_PROPERTY(Quotient::RoomMember sender MEMBER sender FINAL) 88 | Q_PROPERTY(QString content MEMBER content FINAL) 89 | }; 90 | 91 | namespace EventGrouping { 92 | Q_NAMESPACE 93 | 94 | enum Values { 95 | KeepPreviousGroup = 0, 96 | ShowAuthor = 1, 97 | ShowDateAndAuthor = 2 98 | }; 99 | Q_ENUM_NS(Values) 100 | 101 | } 102 | 103 | namespace VerificationState { 104 | Q_NAMESPACE 105 | 106 | enum Values { 107 | Unverified = 0, 108 | Verified = 1, 109 | NotRelevant = 2, //!< Unencrypted messages 110 | }; 111 | Q_ENUM_NS(Values) 112 | 113 | } 114 | -------------------------------------------------------------------------------- /client/models/orderbytag.h: -------------------------------------------------------------------------------- 1 | /****************************************************************************** 2 | * SPDX-FileCopyrightText: 2018-2019 QMatrixClient Project 3 | * 4 | * SPDX-License-Identifier: GPL-3.0-or-later 5 | */ 6 | 7 | #pragma once 8 | 9 | #include "abstractroomordering.h" 10 | 11 | // TODO: When the library l10n is enabled, these two should go down to it 12 | QString tagToCaption(const QString& tag); 13 | QString captionToTag(const QString& caption); 14 | 15 | class OrderByTag : public AbstractRoomOrdering 16 | { 17 | public: 18 | explicit OrderByTag(RoomListModel* m) 19 | : AbstractRoomOrdering(m), tagsOrder(initTagsOrder()) 20 | { } 21 | 22 | private: 23 | QStringList tagsOrder; 24 | 25 | // Overrides 26 | 27 | QString orderingName() const override { return QStringLiteral("tag"); } 28 | QVariant groupLabel(const RoomGroup& g) const override; 29 | bool groupLessThan(const QVariant& g1key, const QVariant& g2key) const override; 30 | bool roomLessThan(const QVariant& groupKey, 31 | const Room* r1, const Room* r2) const override; 32 | groups_t roomGroups(const Room* room) const override; 33 | void connectSignals(Connection* connection) override; 34 | void connectSignals(Room* room) override; 35 | void updateGroups(Room* room) override; 36 | QStringList getFilteredTags(const Room* room) const; 37 | 38 | static QStringList initTagsOrder(); 39 | }; 40 | -------------------------------------------------------------------------------- /client/models/roomlistmodel.h: -------------------------------------------------------------------------------- 1 | /************************************************************************** 2 | * * 3 | * SPDX-FileCopyrightText: 2016 Felix Rohrbach * 4 | * * 5 | * SPDX-License-Identifier: GPL-3.0-or-later 6 | * * 7 | **************************************************************************/ 8 | 9 | #pragma once 10 | 11 | #include "abstractroomordering.h" 12 | #include "../quaternionroom.h" 13 | 14 | #include 15 | #include 16 | #include 17 | 18 | #include 19 | #include 20 | 21 | class QAbstractItemView; 22 | 23 | class RoomListModel: public QAbstractItemModel 24 | { 25 | Q_OBJECT 26 | template 27 | using ConnectionsGuard = Quotient::ConnectionsGuard; 28 | public: 29 | enum Roles { 30 | HasUnreadRole = Qt::UserRole + 1, 31 | HighlightCountRole, JoinStateRole, ObjectRole 32 | }; 33 | 34 | using Room = Quotient::Room; 35 | 36 | explicit RoomListModel(QAbstractItemView* parent); 37 | ~RoomListModel() override = default; 38 | 39 | QVariant roomGroupAt(QModelIndex idx) const; 40 | QuaternionRoom* roomAt(QModelIndex idx) const; 41 | QModelIndex indexOf(const QVariant& group) const; 42 | QModelIndex indexOf(const QVariant& group, Room* room) const; 43 | 44 | QModelIndex index(int row, int column, 45 | const QModelIndex& parent = {}) const override; 46 | QModelIndex parent(const QModelIndex& index) const override; 47 | using QObject::parent; 48 | QVariant data(const QModelIndex& index, int role) const override; 49 | int columnCount(const QModelIndex&) const override; 50 | int rowCount(const QModelIndex& parent) const override; 51 | int totalRooms() const; 52 | bool isValidGroupIndex(const QModelIndex& i) const; 53 | bool isValidRoomIndex(const QModelIndex& i) const; 54 | 55 | template 56 | void setOrder() { doSetOrder(std::make_unique(this)); } 57 | 58 | signals: 59 | void groupAdded(int row); 60 | void saveCurrentSelection(); 61 | void restoreCurrentSelection(); 62 | 63 | public slots: 64 | void addConnection(Quotient::Connection* connection); 65 | void deleteConnection(Quotient::Connection* connection); 66 | 67 | // FIXME, quotient-im/libQuotient#63: 68 | // This should go to the library's ConnectionManager/RoomManager 69 | void deleteTag(QModelIndex index); 70 | 71 | private slots: 72 | void addRoom(Room* room); 73 | void refresh(Room* room, const QVector& roles = {}); 74 | void deleteRoom(Room* room); 75 | 76 | void updateGroups(Room* room); 77 | 78 | private: 79 | friend class AbstractRoomOrdering; 80 | 81 | std::vector> m_connections; 82 | RoomGroups m_roomGroups; 83 | AbstractRoomOrdering* m_roomOrder = nullptr; 84 | 85 | QMultiHash m_roomIndices; 86 | 87 | RoomGroups::iterator tryInsertGroup(const QVariant& key); 88 | void addRoomToGroups(Room* room, QVariantList groups = {}); 89 | void connectRoomSignals(Room* room); 90 | void doRemoveRoom(const QModelIndex& idx); 91 | 92 | void visitRoom(const Room& room, 93 | const std::function& visitor); 94 | 95 | void doSetOrder(std::unique_ptr&& newOrder); 96 | 97 | std::pair 98 | preparePersistentIndexChange(int fromPos, int shiftValue) const; 99 | 100 | // Beware, the returned iterators are as short-lived as QModelIndex'es 101 | auto lowerBoundGroup(const QVariant& group) 102 | { 103 | return std::ranges::lower_bound(m_roomGroups, group, 104 | m_roomOrder->groupLessThanFactory(), &RoomGroup::key); 105 | } 106 | auto lowerBoundGroup(const QVariant& group) const 107 | { 108 | return std::ranges::lower_bound(m_roomGroups, group, 109 | m_roomOrder->groupLessThanFactory(), &RoomGroup::key); 110 | } 111 | 112 | auto lowerBoundRoom(RoomGroup& group, Room* room) const 113 | { 114 | return std::ranges::lower_bound(group.rooms, room, 115 | m_roomOrder->roomLessThanFactory(group.key)); 116 | } 117 | 118 | auto lowerBoundRoom(const RoomGroup& group, Room* room) const 119 | { 120 | return std::ranges::lower_bound(group.rooms, room, 121 | m_roomOrder->roomLessThanFactory(group.key)); 122 | } 123 | }; 124 | -------------------------------------------------------------------------------- /client/models/userlistmodel.cpp: -------------------------------------------------------------------------------- 1 | /************************************************************************** 2 | * * 3 | * SPDX-FileCopyrightText: 2015 Felix Rohrbach * 4 | * * 5 | * SPDX-License-Identifier: GPL-3.0-or-later 6 | * * 7 | **************************************************************************/ 8 | 9 | #include "userlistmodel.h" 10 | 11 | #include "../logging_categories.h" 12 | 13 | #include 14 | #include 15 | #include 16 | #include 17 | // Injecting the dependency on a view is not so nice; but the way the model 18 | // provides avatar decorations depends on the delegate size 19 | #include 20 | 21 | #include 22 | #include 23 | #include 24 | #include 25 | 26 | using Quotient::RoomMember; 27 | 28 | UserListModel::UserListModel(QAbstractItemView* parent) 29 | : QAbstractListModel(parent), m_currentRoom(nullptr) 30 | { } 31 | 32 | void UserListModel::setRoom(Quotient::Room* room) 33 | { 34 | if (m_currentRoom == room) 35 | return; 36 | 37 | using namespace Quotient; 38 | beginResetModel(); 39 | if (m_currentRoom) { 40 | m_currentRoom->connection()->disconnect(this); 41 | m_currentRoom->disconnect(this); 42 | m_memberIds.clear(); 43 | } 44 | m_currentRoom = room; 45 | if (m_currentRoom) { 46 | connect(m_currentRoom, &Room::memberJoined, this, &UserListModel::userAdded); 47 | connect(m_currentRoom, &Room::memberLeft, this, &UserListModel::userRemoved); 48 | connect(m_currentRoom, &Room::memberNameAboutToUpdate, this, &UserListModel::userRemoved); 49 | connect(m_currentRoom, &Room::memberNameUpdated, this, &UserListModel::userAdded); 50 | connect(m_currentRoom, &Room::memberListChanged, this, &UserListModel::membersChanged); 51 | connect(m_currentRoom, &Room::memberAvatarUpdated, this, &UserListModel::avatarChanged); 52 | connect(m_currentRoom->connection(), &Connection::loggedOut, this, 53 | [this] { setRoom(nullptr); }); 54 | doFilter({}); 55 | qCDebug(MODELS) << m_memberIds.count() << "member(s) in the room"; 56 | } 57 | endResetModel(); 58 | } 59 | 60 | Quotient::RoomMember UserListModel::userAt(QModelIndex index) const 61 | { 62 | if (index.row() < 0 || index.row() >= m_memberIds.size()) 63 | return {}; 64 | return m_currentRoom->member(m_memberIds.at(index.row())); 65 | } 66 | 67 | QVariant UserListModel::data(const QModelIndex& index, int role) const 68 | { 69 | if( !index.isValid() ) 70 | return QVariant(); 71 | 72 | if( index.row() >= m_memberIds.count() ) 73 | { 74 | qCWarning(MODELS) << "UserListModel, something's wrong: index.row() >= " 75 | "m_users.count()"; 76 | return QVariant(); 77 | } 78 | auto m = userAt(index); 79 | if( role == Qt::DisplayRole ) 80 | { 81 | return m.displayName(); 82 | } 83 | const auto* view = static_cast(parent()); 84 | if (role == Qt::DecorationRole) { 85 | // Convert avatar image to QIcon 86 | const auto dpi = view->devicePixelRatioF(); 87 | if (auto av = m.avatar(static_cast(view->iconSize().height() * dpi), [] {}); 88 | !av.isNull()) { 89 | av.setDevicePixelRatio(dpi); 90 | return QIcon(QPixmap::fromImage(av)); 91 | } 92 | // TODO: Show a different fallback icon for invited users 93 | return QIcon::fromTheme("user-available", 94 | QIcon(":/irc-channel-joined")); 95 | } 96 | 97 | if (role == Qt::ToolTipRole) 98 | { 99 | auto tooltip = 100 | QStringLiteral("%1
%2").arg(m.name().toHtmlEscaped(), m.id().toHtmlEscaped()); 101 | // TODO: Find a new way to determine that the user is bridged 102 | // if (!user->bridged().isEmpty()) 103 | // tooltip += "
" + tr("Bridged from: %1").arg(user->bridged()); 104 | return tooltip; 105 | } 106 | 107 | if (role == Qt::ForegroundRole) { 108 | // FIXME: boilerplate with TimelineItem.qml:57 109 | const auto& palette = view->palette(); 110 | return QColor::fromHslF(static_cast(m.hueF()), 111 | 1 - palette.color(QPalette::Window).saturationF(), 112 | 0.9f - 0.7f * palette.color(QPalette::Window).lightnessF(), 113 | palette.color(QPalette::ButtonText).alphaF()); 114 | } 115 | 116 | return QVariant(); 117 | } 118 | 119 | int UserListModel::rowCount(const QModelIndex& parent) const 120 | { 121 | if( parent.isValid() ) 122 | return 0; 123 | 124 | return m_memberIds.count(); 125 | } 126 | 127 | void UserListModel::userAdded(const RoomMember& member) 128 | { 129 | auto pos = findUserPos(member.id()); 130 | if (pos != m_memberIds.size() && m_memberIds[pos] == member.id()) 131 | { 132 | qCWarning(MODELS) << "Trying to add the user" << member.id() 133 | << "but it's already in the user list"; 134 | return; 135 | } 136 | beginInsertRows(QModelIndex(), pos, pos); 137 | m_memberIds.insert(pos, member.id()); 138 | endInsertRows(); 139 | } 140 | 141 | void UserListModel::userRemoved(const RoomMember& member) 142 | { 143 | auto pos = findUserPos(member); 144 | if (pos == m_memberIds.size()) 145 | { 146 | qCWarning(MODELS) 147 | << "Trying to remove a room member not in the user list:" 148 | << member.id(); 149 | return; 150 | } 151 | 152 | beginRemoveRows(QModelIndex(), pos, pos); 153 | m_memberIds.removeAt(pos); 154 | endRemoveRows(); 155 | } 156 | 157 | void UserListModel::filter(const QString& filterString) 158 | { 159 | if (m_currentRoom == nullptr) 160 | return; 161 | 162 | beginResetModel(); 163 | doFilter(filterString); 164 | endResetModel(); 165 | } 166 | 167 | void UserListModel::refresh(const RoomMember& member, QVector roles) 168 | { 169 | auto pos = findUserPos(member); 170 | if ( pos != m_memberIds.size() ) 171 | emit dataChanged(index(pos), index(pos), roles); 172 | else 173 | qCWarning(MODELS) 174 | << "Trying to access a room member not in the user list"; 175 | } 176 | 177 | void UserListModel::avatarChanged(const RoomMember& m) 178 | { 179 | refresh(m, {Qt::DecorationRole}); 180 | } 181 | 182 | int UserListModel::findUserPos(const Quotient::RoomMember& m) const 183 | { 184 | return findUserPos(m.disambiguatedName()); 185 | } 186 | 187 | int UserListModel::findUserPos(const QString& username) const 188 | { 189 | return static_cast(Quotient::lowerBoundMemberIndex(m_memberIds, username, m_currentRoom)); 190 | } 191 | 192 | void UserListModel::doFilter(const QString& filterString) 193 | { 194 | QElapsedTimer et; et.start(); 195 | 196 | auto filteredMembers = Quotient::rangeTo( 197 | std::views::filter(m_currentRoom->joinedMembers(), 198 | Quotient::memberMatcher(filterString, Qt::CaseInsensitive))); 199 | std::ranges::sort(filteredMembers, Quotient::MemberSorter()); 200 | const auto sortedIds = std::views::transform(filteredMembers, &RoomMember::id); 201 | #if QT_VERSION >= QT_VERSION_CHECK(6, 6, 0) 202 | m_memberIds.assign(sortedIds.begin(), sortedIds.end()); 203 | #else 204 | m_memberIds = QList(sortedIds.begin(), sortedIds.end()); 205 | #endif 206 | 207 | qCDebug(MODELS) << "Filtering" << m_memberIds.size() << "user(s) in" 208 | << m_currentRoom->displayName() << "took" << et; 209 | } 210 | -------------------------------------------------------------------------------- /client/models/userlistmodel.h: -------------------------------------------------------------------------------- 1 | /************************************************************************** 2 | * * 3 | * SPDX-FileCopyrightText: 2015 Felix Rohrbach * 4 | * * 5 | * SPDX-License-Identifier: GPL-3.0-or-later 6 | * * 7 | **************************************************************************/ 8 | 9 | #pragma once 10 | 11 | #include 12 | 13 | class QAbstractItemView; 14 | 15 | namespace Quotient 16 | { 17 | class Connection; 18 | class Room; 19 | class RoomMember; 20 | } 21 | 22 | class UserListModel: public QAbstractListModel 23 | { 24 | Q_OBJECT 25 | public: 26 | UserListModel(QAbstractItemView* parent); 27 | 28 | void setRoom(Quotient::Room* room); 29 | Quotient::RoomMember userAt(QModelIndex index) const; 30 | 31 | QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override; 32 | int rowCount(const QModelIndex& parent=QModelIndex()) const override; 33 | 34 | signals: 35 | void membersChanged(); //!< Reflection of Room::memberListChanged 36 | 37 | public slots: 38 | void filter(const QString& filterString); 39 | 40 | private slots: 41 | void userAdded(const Quotient::RoomMember& member); 42 | void userRemoved(const Quotient::RoomMember& member); 43 | void refresh(const Quotient::RoomMember& member, QVector roles = {}); 44 | void avatarChanged(const Quotient::RoomMember& m); 45 | 46 | private: 47 | Quotient::Room* m_currentRoom; 48 | QList m_memberIds; 49 | 50 | int findUserPos(const Quotient::RoomMember &m) const; 51 | int findUserPos(const QString& username) const; 52 | void doFilter(const QString& filterString); 53 | }; 54 | -------------------------------------------------------------------------------- /client/networkconfigdialog.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2017 Kitsune Ral 3 | * 4 | * SPDX-License-Identifier: LGPL-2.1-or-later 5 | */ 6 | 7 | #include "networkconfigdialog.h" 8 | 9 | #include 10 | 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | #include 20 | #include 21 | 22 | NetworkConfigDialog::NetworkConfigDialog(QWidget* parent) 23 | : Dialog(tr("Network proxy settings"), parent) 24 | , useProxyBox(new QGroupBox(tr("&Override system defaults"), this)) 25 | , proxyTypeGroup(new QButtonGroup(this)) 26 | , proxyHostName(new QLineEdit(this)) 27 | , proxyPort(new QSpinBox(this)) 28 | , proxyUserName(new QLineEdit(this)) 29 | { 30 | // Create and configure all the controls 31 | 32 | useProxyBox->setCheckable(true); 33 | useProxyBox->setChecked(false); 34 | connect(useProxyBox, &QGroupBox::toggled, 35 | this, &NetworkConfigDialog::maybeDisableControls); 36 | 37 | auto noProxyButton = new QRadioButton(tr("&No proxy")); 38 | noProxyButton->setChecked(true); 39 | proxyTypeGroup->addButton(noProxyButton, 40 | QNetworkProxy::NoProxy); 41 | proxyTypeGroup->addButton(new QRadioButton(tr("&HTTP(S) proxy")), 42 | QNetworkProxy::HttpProxy); 43 | proxyTypeGroup->addButton(new QRadioButton(tr("&SOCKS5 proxy")), 44 | QNetworkProxy::Socks5Proxy); 45 | connect(proxyTypeGroup, &QButtonGroup::idToggled, this, 46 | &NetworkConfigDialog::maybeDisableControls); 47 | 48 | maybeDisableControls(); 49 | 50 | auto hostLabel = makeBuddyLabel(tr("Host"), proxyHostName); 51 | auto portLabel = makeBuddyLabel(tr("Port"), proxyPort); 52 | auto userLabel = makeBuddyLabel(tr("User name"), proxyUserName); 53 | 54 | proxyPort->setRange(0, 65535); 55 | proxyPort->setSpecialValueText(QStringLiteral(" ")); 56 | 57 | // Now laying all this out 58 | 59 | auto proxyTypeLayout = new QGridLayout; 60 | auto radios = proxyTypeGroup->buttons(); 61 | proxyTypeLayout->addWidget(radios[0], 0, 0); 62 | for (int i = 2; i <= radios.size(); ++i) // Consider i as 1-based index 63 | proxyTypeLayout->addWidget(radios[i - 1], i / 2, i % 2); 64 | 65 | auto hostPortLayout = new QHBoxLayout; 66 | for (auto l: { hostLabel, portLabel }) 67 | { 68 | hostPortLayout->addWidget(l); 69 | hostPortLayout->addWidget(l->buddy()); 70 | } 71 | auto userNameLayout = new QHBoxLayout; 72 | userNameLayout->addWidget(userLabel); 73 | userNameLayout->addWidget(userLabel->buddy()); 74 | 75 | auto proxySettingsLayout = new QVBoxLayout(useProxyBox); 76 | proxySettingsLayout->addLayout(proxyTypeLayout); 77 | proxySettingsLayout->addLayout(hostPortLayout); 78 | proxySettingsLayout->addLayout(userNameLayout); 79 | 80 | addWidget(useProxyBox); 81 | } 82 | 83 | NetworkConfigDialog::~NetworkConfigDialog() = default; 84 | 85 | void NetworkConfigDialog::maybeDisableControls() 86 | { 87 | if (useProxyBox->isChecked()) 88 | { 89 | bool disable = proxyTypeGroup->checkedId() == -1 || 90 | proxyTypeGroup->checkedId() == QNetworkProxy::NoProxy; 91 | proxyHostName->setDisabled(disable); 92 | proxyPort->setDisabled(disable); 93 | proxyUserName->setDisabled(disable); 94 | } 95 | } 96 | 97 | void NetworkConfigDialog::apply() 98 | { 99 | Quotient::NetworkSettings networkSettings; 100 | 101 | auto proxyType = useProxyBox->isChecked() ? 102 | QNetworkProxy::ProxyType(proxyTypeGroup->checkedId()) : 103 | QNetworkProxy::DefaultProxy; 104 | networkSettings.setProxyType(proxyType); 105 | networkSettings.setProxyHostName(proxyHostName->text()); 106 | networkSettings.setProxyPort(quint16(proxyPort->value())); 107 | networkSettings.setupApplicationProxy(); 108 | // Should we do something for authentication at all?.. 109 | accept(); 110 | } 111 | 112 | void NetworkConfigDialog::load() 113 | { 114 | Quotient::NetworkSettings networkSettings; 115 | auto proxyType = networkSettings.proxyType(); 116 | if (proxyType == QNetworkProxy::DefaultProxy) 117 | { 118 | useProxyBox->setChecked(false); 119 | } else { 120 | useProxyBox->setChecked(true); 121 | if (auto b = proxyTypeGroup->button(proxyType)) 122 | b->setChecked(true); 123 | } 124 | proxyHostName->setText(networkSettings.proxyHostName()); 125 | auto port = networkSettings.proxyPort(); 126 | if (port > 0) 127 | proxyPort->setValue(port); 128 | } 129 | -------------------------------------------------------------------------------- /client/networkconfigdialog.h: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2017 Kitsune Ral 3 | * 4 | * SPDX-License-Identifier: LGPL-2.1-or-later 5 | */ 6 | 7 | #pragma once 8 | 9 | #include "dialog.h" 10 | 11 | class QGroupBox; 12 | class QButtonGroup; 13 | class QLineEdit; 14 | class QSpinBox; 15 | 16 | class NetworkConfigDialog : public Dialog 17 | { 18 | Q_OBJECT 19 | public: 20 | explicit NetworkConfigDialog(QWidget* parent = nullptr); 21 | ~NetworkConfigDialog(); 22 | 23 | private slots: 24 | void apply() override; 25 | void load() override; 26 | void maybeDisableControls(); 27 | 28 | private: 29 | QGroupBox* useProxyBox; 30 | QButtonGroup* proxyTypeGroup; 31 | QLineEdit* proxyHostName; 32 | QSpinBox* proxyPort; 33 | QLineEdit* proxyUserName; 34 | }; 35 | -------------------------------------------------------------------------------- /client/profiledialog.h: -------------------------------------------------------------------------------- 1 | /************************************************************************** 2 | * * 3 | * SPDX-FileCopyrightText: 2019 Karol Kosek * 4 | * * 5 | * SPDX-License-Identifier: GPL-3.0-or-later 6 | * * 7 | **************************************************************************/ 8 | 9 | #pragma once 10 | 11 | #include "dialog.h" 12 | 13 | #include 14 | #include 15 | 16 | #include 17 | 18 | class AccountSelector; 19 | class MainWindow; 20 | 21 | class QComboBox; 22 | class QLineEdit; 23 | 24 | namespace Quotient { 25 | class AccountRegistry; 26 | class GetDevicesJob; 27 | class Connection; 28 | class KeyVerificationSession; 29 | } 30 | 31 | class ProfileDialog : public Dialog 32 | { 33 | Q_OBJECT 34 | public: 35 | explicit ProfileDialog(Quotient::AccountRegistry* accounts, 36 | MainWindow* parent); 37 | ~ProfileDialog() override; 38 | 39 | void setAccount(Quotient::Connection* newAccount); 40 | Quotient::Connection* account() const; 41 | 42 | private slots: 43 | void load() override; 44 | void apply() override; 45 | void uploadAvatar(); 46 | Quotient::KeyVerificationSession* initiateVerification(const QString& deviceId, 47 | QAction* verifyAction); 48 | 49 | private: 50 | Quotient::SettingsGroup m_settings; 51 | 52 | class DeviceTable; 53 | DeviceTable* m_deviceTable; 54 | QPushButton* m_avatar; 55 | AccountSelector* m_accountSelector; 56 | QLineEdit* m_displayName; 57 | QLabel* m_accessTokenLabel; 58 | 59 | Quotient::Connection* m_currentAccount; 60 | QString m_newAvatarPath; 61 | QPointer m_devicesJob; 62 | QVector m_devices; 63 | 64 | void setVerifiedItem(int row, const QString& deviceId); 65 | void refreshDevices(); 66 | }; 67 | -------------------------------------------------------------------------------- /client/qml/AnimatedTransition.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.0 2 | import Quotient 1.0 3 | 4 | Transition { 5 | property var settings: TimelineSettings { } 6 | enabled: settings.enable_animations 7 | } 8 | -------------------------------------------------------------------------------- /client/qml/AnimationBehavior.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.0 2 | import Quotient 1.0 3 | 4 | Behavior { 5 | property var settings: TimelineSettings { } 6 | enabled: settings.enable_animations 7 | } 8 | -------------------------------------------------------------------------------- /client/qml/Attachment.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.0 2 | import Quotient 1.0 3 | 4 | Item { 5 | width: parent.width 6 | height: visible ? childrenRect.height : 0 7 | 8 | required property bool openOnFinished 9 | readonly property bool downloaded: progressInfo && 10 | !progressInfo.isUpload && progressInfo.completed 11 | signal opened 12 | 13 | onDownloadedChanged: { 14 | if (downloaded && openOnFinished) 15 | openLocalFile() 16 | } 17 | 18 | function openExternally() 19 | { 20 | if (progressInfo.localPath.toString() || downloaded) 21 | openLocalFile() 22 | else 23 | room.downloadFile(eventId) 24 | } 25 | 26 | function openLocalFile() 27 | { 28 | if (!Qt.openUrlExternally(progressInfo.localPath)) { 29 | controller.showStatusMessage( 30 | "Couldn't determine how to open the file, " + 31 | "opening its folder instead", 5000) 32 | 33 | if (!Qt.openUrlExternally(progressInfo.localDir)) { 34 | controller.showStatusMessage( 35 | "Couldn't determine how to open the file or its folder.", 36 | 5000) 37 | return; 38 | } 39 | } 40 | opened() 41 | } 42 | 43 | Connections { 44 | target: controller 45 | function onOpenExternally(currentIndex) { 46 | if (currentIndex === index) 47 | openExternally() 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /client/qml/Avatar.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.15 2 | 3 | Image { 4 | id: avatar 5 | readonly property var forRoom: root.room 6 | /* readonly */ property var forMember 7 | 8 | property string sourceId: forMember?.avatarUrl ?? forRoom?.avatarUrl ?? "" 9 | source: sourceId 10 | cache: false // Quotient::Avatar takes care of caching 11 | fillMode: Image.PreserveAspectFit 12 | 13 | function reload() { 14 | source = "" 15 | source = Qt.binding(function() { return sourceId }) 16 | } 17 | 18 | Connections { 19 | target: forRoom 20 | function onAvatarChanged() { avatar.reload() } 21 | function onMemberAvatarUpdated(member) { 22 | if (avatar.forMember && member?.id === avatar.forMember.id) 23 | avatar.reload() 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /client/qml/FastNumberAnimation.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.0 2 | import Quotient 1.0 3 | 4 | NumberAnimation { 5 | property var settings: TimelineSettings { } 6 | duration: settings.fast_animations_duration_ms 7 | } 8 | -------------------------------------------------------------------------------- /client/qml/FileContent.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.15 2 | import QtQuick.Controls 2.15 3 | import QtQuick.Layouts 1.1 4 | 5 | Attachment { 6 | openOnFinished: openButton.checked 7 | 8 | TextEdit { 9 | id: fileTransferInfo 10 | width: parent.width 11 | 12 | function humanSize(bytes) 13 | { 14 | if (!bytes) 15 | return qsTr("Unknown", "Unknown attachment size") 16 | if (bytes < 4000) 17 | return qsTr("%Ln byte(s)", "", bytes) 18 | bytes = Math.round(bytes / 100) / 10 19 | if (bytes < 2000) 20 | return qsTr("%L1 kB").arg(bytes) 21 | bytes = Math.round(bytes / 100) / 10 22 | if (bytes < 2000) 23 | return qsTr("%L1 MB").arg(bytes) 24 | return qsTr("%L1 GB").arg(Math.round(bytes / 100) / 10) 25 | } 26 | 27 | selectByMouse: true; 28 | readOnly: true; 29 | font: timelabel.font 30 | color: foreground 31 | renderType: settings.render_type 32 | text: qsTr("Size: %1, declared type: %2") 33 | .arg(content.info ? humanSize(content.info.size) : "") 34 | .arg(content.info ? content.info.mimetype : "unknown") 35 | + (progressInfo && progressInfo.isUpload 36 | ? " (" + (progressInfo.completed 37 | ? qsTr("uploaded from %1", "%1 is a local file name") 38 | : qsTr("being uploaded from %1", "%1 is a local file name")) 39 | .arg(progressInfo.localPath) + ')' 40 | : downloaded 41 | ? " (" + qsTr("downloaded to %1", "%1 is a local file name") 42 | .arg(progressInfo.localPath) + ')' 43 | : "") 44 | textFormat: TextEdit.PlainText 45 | wrapMode: Text.Wrap; 46 | 47 | HoverHandler { 48 | id: fileContentHoverHandler 49 | cursorShape: Qt.IBeamCursor 50 | } 51 | ToolTip.visible: fileContentHoverHandler.hovered 52 | ToolTip.text: room && eventId ? room.fileSource(eventId) : "" 53 | 54 | TapHandler { 55 | acceptedButtons: Qt.RightButton 56 | onTapped: controller.showMenu(index, textFieldImpl.hoveredLink, 57 | textFieldImpl.selectedText, 58 | showingDetails) 59 | } 60 | } 61 | ProgressBar { 62 | id: transferProgress 63 | visible: progressInfo && progressInfo.started 64 | anchors.fill: fileTransferInfo 65 | 66 | value: progressInfo ? progressInfo.progress / progressInfo.total : -1 67 | indeterminate: !progressInfo || progressInfo.progress < 0 68 | } 69 | RowLayout { 70 | anchors.top: fileTransferInfo.bottom 71 | width: parent.width 72 | spacing: 2 73 | 74 | TimelineItemToolButton { 75 | id: openButton 76 | text: progressInfo && 77 | !progressInfo.isUpload && transferProgress.visible 78 | ? qsTr("Open after downloading") : qsTr("Open") 79 | checkable: !downloaded && !(progressInfo && progressInfo.isUpload) 80 | onClicked: { if (checked) openExternally() } 81 | } 82 | TimelineItemToolButton { 83 | text: qsTr("Cancel") 84 | visible: progressInfo && progressInfo.started 85 | onClicked: room.cancelFileTransfer(eventId) 86 | } 87 | TimelineItemToolButton { 88 | text: qsTr("Save as...") 89 | visible: !progressInfo || 90 | (!progressInfo.isUpload && !progressInfo.started) 91 | onClicked: controller.saveFileAs(eventId) 92 | } 93 | TimelineItemToolButton { 94 | text: qsTr("Open folder") 95 | visible: progressInfo && progressInfo.localDir 96 | onClicked: 97 | Qt.openUrlExternally(progressInfo.localDir) 98 | } 99 | } 100 | onOpened: openButton.checked = false 101 | } 102 | -------------------------------------------------------------------------------- /client/qml/ImageContent.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.15 2 | import QtQuick.Controls 2.15 3 | 4 | Attachment { 5 | id: content 6 | 7 | required property var sourceSize 8 | required property url source 9 | required property var maxHeight 10 | required property bool autoload 11 | openOnFinished: false 12 | 13 | Image { 14 | width: parent.width 15 | height: sourceSize.height * Math.min(parent.maxHeight / sourceSize.height * 0.9, 16 | Math.min(width / sourceSize.width, 1)) 17 | fillMode: Image.PreserveAspectFit 18 | horizontalAlignment: Image.AlignLeft 19 | 20 | // The spec says that the attachment URL SHOULD be mxc but is not required to be 21 | source: parent.source.toString().startsWith("mxc") 22 | ? room.makeMediaUrl(eventId, parent.source) : parent.source 23 | sourceSize: parent.sourceSize 24 | 25 | HoverHandler { 26 | id: imageHoverHandler 27 | cursorShape: Qt.PointingHandCursor 28 | } 29 | ToolTip.visible: imageHoverHandler.hovered 30 | ToolTip.text: room && eventId ? room.fileSource(eventId) : "" 31 | ToolTip.delay: Application.styleHints.mousePressAndHoldInterval 32 | 33 | TapHandler { 34 | acceptedButtons: Qt.LeftButton 35 | onTapped: { 36 | content.openOnFinished = true 37 | content.openExternally() 38 | } 39 | } 40 | TapHandler { 41 | acceptedButtons: Qt.RightButton 42 | onTapped: controller.showMenu(index, textFieldImpl.hoveredLink, 43 | textFieldImpl.selectedText, showingDetails) 44 | } 45 | 46 | Component.onCompleted: 47 | if (visible && content.autoload && !content.downloaded 48 | && !(progressInfo && progressInfo.isUpload)) 49 | room.downloadFile(eventId) 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /client/qml/Logger.qml: -------------------------------------------------------------------------------- 1 | import QtQml 2.15 2 | import QtQuick 2.15 3 | 4 | // This component can be used either as a logging category (pass it to 5 | // console.log() or any other console method) or as a basic logger itself: 6 | // if you only need log/warn/error kind of things you can save a few keystrokes 7 | // by writing logger.warn(...) instead of console.warn(logger, ...) 8 | LoggingCategory { 9 | name: 'quaternion.timeline.qml' 10 | defaultLogLevel: LoggingCategory.Info 11 | 12 | function debug() { return console.log(this, arguments) } 13 | function log() { return debug(arguments) } 14 | function warn() { return console.warn(this, arguments) } 15 | function error() { return console.error(this, arguments) } 16 | } 17 | -------------------------------------------------------------------------------- /client/qml/NormalNumberAnimation.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.0 2 | import Quotient 1.0 3 | 4 | NumberAnimation { 5 | property var settings: TimelineSettings { } 6 | duration: settings.animations_duration_ms 7 | } 8 | -------------------------------------------------------------------------------- /client/qml/RoomHeader.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.15 2 | import QtQuick.Controls 2.3 3 | import Quotient 1.0 4 | 5 | Frame { 6 | id: roomHeader 7 | 8 | required property Room room 9 | property bool showTopic: true 10 | 11 | height: headerText.height + 11 12 | padding: 3 13 | visible: !!room 14 | 15 | Avatar { 16 | id: roomAvatar 17 | anchors.verticalCenter: headerText.verticalCenter 18 | anchors.left: parent.left 19 | anchors.margins: 2 20 | height: headerText.height 21 | // implicitWidth on its own doesn't respect the scale down of 22 | // the received image (that almost always happens) 23 | width: Math.min(implicitHeight > 0 ? headerText.height / implicitHeight * implicitWidth 24 | : 0, 25 | parent.width / 2.618) // Golden ratio - just for fun 26 | 27 | // Safe upper limit (see also topicField) 28 | sourceSize: Qt.size(-1, settings.lineSpacing * 9) 29 | 30 | AnimationBehavior on width { 31 | NormalNumberAnimation { easing.type: Easing.OutQuad } 32 | } 33 | } 34 | 35 | Column { 36 | id: headerText 37 | anchors.left: roomAvatar.right 38 | anchors.right: versionActionButton.left 39 | anchors.top: parent.top 40 | anchors.margins: 2 41 | 42 | spacing: 2 43 | 44 | readonly property int innerLeftPadding: 4 45 | 46 | TextArea { 47 | id: roomName 48 | width: roomNameMetrics.advanceWidth + leftPadding 49 | height: roomNameMetrics.height 50 | clip: true 51 | padding: 0 52 | leftPadding: headerText.innerLeftPadding 53 | 54 | TextMetrics { 55 | id: roomNameMetrics 56 | font: roomName.font 57 | elide: Text.ElideRight 58 | elideWidth: headerText.width 59 | text: roomHeader.room?.displayName ?? "" 60 | } 61 | 62 | text: roomNameMetrics.elidedText 63 | placeholderText: qsTr("(no name)") 64 | 65 | font.bold: true 66 | renderType: settings.render_type 67 | readOnly: true 68 | 69 | hoverEnabled: text !== "" && 70 | (roomNameMetrics.text != roomNameMetrics.elidedText 71 | || roomName.lineCount > 1) 72 | ToolTip.visible: hovered 73 | ToolTip.text: roomHeader.room?.displayNameForHtml ?? "" 74 | } 75 | 76 | Label { 77 | id: versionNotice 78 | 79 | property alias room: roomHeader.room 80 | 81 | visible: room?.isUnstable || room?.successorId !== "" 82 | width: parent.width 83 | leftPadding: headerText.innerLeftPadding 84 | 85 | text: room?.successorId !== "" ? qsTr("This room has been upgraded.") 86 | : room?.isUnstable ? qsTr("Unstable room version!") : "" 87 | elide: Text.ElideRight 88 | font.italic: true 89 | renderType: settings.render_type 90 | 91 | HoverHandler { 92 | id: versionHoverHandler 93 | enabled: parent.truncated 94 | } 95 | ToolTip.text: text 96 | ToolTip.visible: versionHoverHandler.hovered 97 | } 98 | 99 | ScrollView { 100 | id: topicField 101 | visible: roomHeader.showTopic 102 | width: parent.width 103 | // Allow 5 full (actually, 6 minus padding) lines of the topic 104 | // but not more than 20% of the timeline vertical space 105 | height: 106 | Math.min(topicText.implicitHeight, root.height / 5, settings.lineSpacing * 6) 107 | 108 | ScrollBar.horizontal.policy: ScrollBar.AlwaysOff 109 | ScrollBar.vertical.policy: ScrollBar.AsNeeded 110 | 111 | AnimationBehavior on height { 112 | NormalNumberAnimation { easing.type: Easing.OutQuad } 113 | } 114 | 115 | // FIXME: The below TextArea+MouseArea is a massive copy-paste 116 | // from textFieldImpl and its respective MouseArea in 117 | // TimelineItem.qml. Maybe make a separate component for these 118 | // (RichTextField?). 119 | TextArea { 120 | id: topicText 121 | padding: 2 122 | leftPadding: headerText.innerLeftPadding 123 | rightPadding: topicField.ScrollBar.vertical.visible 124 | ? topicField.ScrollBar.vertical.width : padding 125 | 126 | text: roomHeader.room?.prettyPrint(roomHeader.room?.topic) ?? "" 127 | placeholderText: qsTr("(no topic)") 128 | textFormat: TextEdit.RichText 129 | renderType: settings.render_type 130 | readOnly: true 131 | wrapMode: TextEdit.Wrap 132 | selectByMouse: true 133 | hoverEnabled: true 134 | 135 | onLinkActivated: 136 | (link) => controller.resourceRequested(link) 137 | } 138 | } 139 | } 140 | MouseArea { 141 | anchors.fill: headerText 142 | acceptedButtons: Qt.MiddleButton | Qt.RightButton 143 | cursorShape: topicText.hoveredLink ? Qt.PointingHandCursor : Qt.IBeamCursor 144 | 145 | onClicked: (mouse) => { 146 | if (topicText.hoveredLink) 147 | controller.resourceRequested(topicText.hoveredLink, 148 | "_interactive") 149 | else if (mouse.button === Qt.RightButton) 150 | headerContextMenu.popup() 151 | } 152 | Menu { 153 | id: headerContextMenu 154 | MenuItem { 155 | text: roomHeader.showTopic ? qsTr("Hide topic") : qsTr("Show topic") 156 | onTriggered: roomHeader.showTopic = !roomHeader.showTopic 157 | } 158 | } 159 | } 160 | Button { 161 | id: versionActionButton 162 | 163 | property alias room: roomHeader.room 164 | 165 | visible: (room?.isUnstable && room?.canSwitchVersions()) || room?.successorId !== "" 166 | anchors.verticalCenter: headerText.verticalCenter 167 | anchors.right: parent.right 168 | width: visible * implicitWidth 169 | text: room?.successorId !== "" ? qsTr("Go to\nnew room") : qsTr("Room\nsettings") 170 | 171 | onClicked: 172 | if (room.successorId !== "") 173 | controller.resourceRequested(room.successorId, "join") 174 | else 175 | controller.roomSettingsRequested() 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /client/qml/ScrollFinisher.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.15 2 | 3 | Timer { 4 | property int targetIndex: -1 5 | property int positionMode: ListView.End 6 | property int round: 1 7 | 8 | readonly property var lc: root.lc 9 | 10 | /** @brief Scroll to the position making sure the timeline is actually at it 11 | * 12 | * Qt is not fabulous at positioning the list view when the delegate 13 | * sizes vary too much; this function runs scrollFinisher timer to adjust 14 | * the position as needed shortly after the list was positioned. 15 | * Nothing good in that, just a workaround. 16 | * 17 | * This function is the entry point to get the whole component do its job. 18 | */ 19 | function scrollViewTo(newTargetIndex, newPositionMode) { 20 | console.log(lc, "Jumping to item", newTargetIndex) 21 | parent.animateNextScroll = true 22 | parent.positionViewAtIndex(newTargetIndex, newPositionMode) 23 | targetIndex = newTargetIndex 24 | positionMode = newPositionMode 25 | round = 1 26 | start() 27 | } 28 | 29 | function logFixup(nameForLog, topContentY, bottomContentY) { 30 | const contentX = parent.contentX 31 | const topShownIndex = parent.indexAt(contentX, topContentY) 32 | const bottomShownIndex = parent.indexAt(contentX, bottomContentY - 1) 33 | if (bottomShownIndex !== -1 34 | && (targetIndex > topShownIndex || targetIndex < bottomShownIndex)) 35 | console.log(lc, "Fixing up item", targetIndex, "to be", nameForLog, 36 | "- fixup round #" + scrollFinisher.round, 37 | "(" + topShownIndex + "-" + bottomShownIndex, 38 | "range is shown now)") 39 | } 40 | 41 | /** @return true if no further action is needed; false if the finisher has to be restarted. */ 42 | function adjustPosition() { 43 | if (targetIndex === 0) { 44 | if (parent.bottommostVisibleIndex === 0) 45 | return true // Positioning is correct 46 | 47 | // This normally shouldn't happen even with the current 48 | // imperfect positioning code in Qt 49 | console.warn(lc, "Fixing up the viewport to be at sync edge") 50 | parent.positionViewAtBeginning() 51 | } else { 52 | const height = parent.height 53 | const contentY = parent.contentY 54 | const viewport1stThird = contentY + height / 3 55 | const item = parent.itemAtIndex(targetIndex) 56 | if (item) { 57 | // The viewport is divided into thirds; ListView.End should 58 | // place targetIndex at the top third, Center corresponds 59 | // to the middle third; Beginning is not used for now. 60 | switch (positionMode) { 61 | case ListView.Contain: 62 | if (item.y >= contentY || item.y + item.height < contentY + height) 63 | return true // Positioning successful 64 | logFixup("fully visible", contentY, contentY + height) 65 | break 66 | case ListView.Center: 67 | const viewport2ndThird = contentY + 2 * height / 3 68 | const itemMidPoint = item.y + item.height / 2 69 | if (itemMidPoint >= viewport1stThird && itemMidPoint < viewport2ndThird) 70 | return true 71 | logFixup("in the centre", viewport1stThird, viewport2ndThird) 72 | break 73 | case ListView.End: 74 | if (item.y >= contentY && item.y < viewport1stThird) 75 | return true 76 | logFixup("at the top", contentY, viewport1stThird) 77 | break 78 | default: 79 | console.warn(lc, "fixupPosition: Unsupported positioning mode:", positionMode) 80 | return true // Refuse to do anything with it 81 | } 82 | } 83 | // If the target item moved away too far and got destroyed, repeat positioning 84 | parent.animateNextScroll = true 85 | parent.positionViewAtIndex(targetIndex, positionMode) 86 | return false 87 | } 88 | } 89 | 90 | interval: 120 // small enough to avoid visual stutter 91 | onTriggered: { 92 | if (parent.count === 0) 93 | return 94 | 95 | if (adjustPosition() || ++round > 3 /* Give up after 3 rounds */) { 96 | targetIndex = -1 97 | parent.saveViewport(true) 98 | } else // Positioning is still in flux, might need another round 99 | start() 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /client/qml/TimelineItemToolButton.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.6 2 | import QtQuick.Controls 2.15 3 | 4 | Button { 5 | id: tButton 6 | contentItem: Text { 7 | text: tButton.text 8 | fontSizeMode: Text.VerticalFit 9 | minimumPointSize: settings.font.pointSize - 3 10 | color: foreground 11 | renderType: settings.render_type 12 | verticalAlignment: Text.AlignVCenter 13 | horizontalAlignment: Text.AlignHCenter 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /client/qml/TimelineMouseArea.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.2 2 | 3 | MouseArea { 4 | onWheel: (wheel) => { chatView.onWheel(wheel) } 5 | onReleased: controller.focusInput() 6 | } 7 | -------------------------------------------------------------------------------- /client/qml/TimelineSettings.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.4 2 | import Quotient 1.0 3 | 4 | Settings { 5 | readonly property int animations_duration_ms_impl: value("UI/animations_duration_ms", 400) 6 | readonly property bool enable_animations: animations_duration_ms_impl > 0 7 | readonly property int animations_duration_ms: 8 | animations_duration_ms_impl == 0 ? 10 : animations_duration_ms_impl 9 | readonly property int fast_animations_duration_ms: animations_duration_ms / 2 10 | 11 | readonly property string timeline_style: value("UI/timeline_style", "") 12 | readonly property bool timelineStyleIsXChat: timeline_style === "xchat" 13 | 14 | readonly property string font_family_impl: value("UI/Fonts/timeline_family", "") 15 | readonly property real font_pointSize_impl: parseFloat(value("UI/Fonts/timeline_pointSize", "")) 16 | readonly property var defaultMetrics: FontMetrics { } 17 | readonly property var fontInfo: FontMetrics { 18 | font.family: font_family_impl ? font_family_impl : defaultMetrics.font.family 19 | font.pointSize: font_pointSize_impl > 0 ? font_pointSize_impl : defaultMetrics.font.pointSize 20 | } 21 | readonly property var font: fontInfo.font 22 | readonly property real fontHeight: fontInfo.height 23 | readonly property real lineSpacing: fontInfo.lineSpacing 24 | /// 2 text line heights by default; 1 line height for XChat 25 | readonly property real minimalTimelineItemHeight: lineSpacing * (2 - timelineStyleIsXChat) 26 | 27 | readonly property var render_type_impl: value("UI/Fonts/render_type", "native") 28 | readonly property int render_type: 29 | ["native", "Native", "NativeRendering"].indexOf(render_type_impl) != -1 30 | ? Text.NativeRendering : Text.QtRendering 31 | readonly property bool use_shuttle_dial: value("UI/use_shuttle_dial", true) 32 | readonly property bool autoload_images: value("UI/autoload_images", true) 33 | 34 | readonly property var disabledPalette: SystemPalette { colorGroup: SystemPalette.Disabled } 35 | 36 | function mixColors(baseColor, mixedColor, mixRatio) 37 | { 38 | return Qt.tint(baseColor, Qt.rgba(mixedColor.r, mixedColor.g, mixedColor.b, mixRatio)) 39 | } 40 | 41 | readonly property string highlight_mode: value("UI/highlight_mode", "background") 42 | readonly property color highlight_color: value("UI/highlight_color", "orange") 43 | 44 | readonly property color lowlight_color: mixColors(disabledPalette.text, palette.text, 0.3) 45 | readonly property bool show_author_avatars: 46 | value("UI/show_author_avatars", !timelineStyleIsXChat) 47 | } 48 | -------------------------------------------------------------------------------- /client/qml/TimelineTextEditSelector.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.2 2 | 3 | /* 4 | * Unfortunately, TextEdit captures LeftButton events for text selection in a way which 5 | * is not compatible with our focus-cancelling mechanism, so we took over the task here. 6 | */ 7 | MouseArea { 8 | property var textEdit: parent 9 | property int selectionMode: TextEdit.SelectCharacters 10 | 11 | anchors.fill: parent 12 | acceptedButtons: Qt.LeftButton 13 | preventStealing: true 14 | 15 | onPressed: (mouse) => { 16 | var x = mouse.x 17 | var y = mouse.y 18 | if (textEdit.flickableItem) { 19 | x += textEdit.flickableItem.contentX 20 | y += textEdit.flickableItem.contentY 21 | } 22 | var hasSelection = textEdit.selectionEnd > textEdit.selectionStart 23 | if (hasSelection && controller.getModifierKeys() & Qt.ShiftModifier) { 24 | textEdit.moveCursorSelection(textEdit.positionAt(x, y), selectionMode) 25 | } else { 26 | textEdit.cursorPosition = textEdit.positionAt(x, y) 27 | if (chatView.textEditWithSelection) 28 | chatView.textEditWithSelection.deselect() 29 | } 30 | } 31 | onClicked: { 32 | if (textEdit.hoveredLink) 33 | textEdit.linkActivated(textEdit.hoveredLink) 34 | } 35 | onDoubleClicked: { 36 | selectionMode = TextEdit.SelectWords 37 | textEdit.selectWord() 38 | } 39 | onReleased: { 40 | selectionMode = TextEdit.SelectCharacters 41 | controller.setGlobalSelectionBuffer(textEdit.selectedText) 42 | chatView.textEditWithSelection = textEdit 43 | 44 | controller.focusInput() 45 | } 46 | onPositionChanged: (mouse) => { 47 | var x = mouse.x 48 | var y = mouse.y 49 | if (textEdit.flickableItem) { 50 | x += textEdit.flickableItem.contentX 51 | y += textEdit.flickableItem.contentY 52 | } 53 | textEdit.moveCursorSelection(textEdit.positionAt(x, y), selectionMode) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /client/quaternionroom.h: -------------------------------------------------------------------------------- 1 | /************************************************************************** 2 | * * 3 | * SPDX-FileCopyrightText: 2016 Felix Rohrbach * 4 | * * 5 | * SPDX-License-Identifier: GPL-3.0-or-later 6 | * * 7 | **************************************************************************/ 8 | 9 | #pragma once 10 | 11 | #include "htmlfilter.h" 12 | 13 | #include 14 | 15 | #include 16 | 17 | #include 18 | 19 | class QTextDocumentFragment; 20 | 21 | class QuaternionRoom: public Quotient::Room 22 | { 23 | Q_OBJECT 24 | public: 25 | using RoomEvent = Quotient::RoomEvent; 26 | 27 | QuaternionRoom(Quotient::Connection* connection, QString roomId, 28 | Quotient::JoinState joinState); 29 | 30 | const QString& cachedUserFilter() const; 31 | void setCachedUserFilter(const QString& input); 32 | 33 | bool isEventHighlighted(const Quotient::RoomEvent* e) const; 34 | 35 | Q_INVOKABLE int savedTopVisibleIndex() const; 36 | Q_INVOKABLE int savedBottomVisibleIndex() const; 37 | Q_INVOKABLE void saveViewport(int topIndex, int bottomIndex, bool force = false); 38 | 39 | bool canRedact(const Quotient::EventId& eventId) const; 40 | 41 | using EventFuture = QFuture; 42 | 43 | //! \brief Loads the message history until the specified event id is found 44 | //! 45 | //! This is potentially heavy; use it sparingly. One intended use case is loading the timeline 46 | //! until the last read event, assuming that the last read event is not too far back and that 47 | //! the user will read or at least scroll through the just loaded events anyway. This will not 48 | //! be necessary once we move to sliding sync but sliding sync support is still a bit away in 49 | //! the future. 50 | //! 51 | //! Because the process is heavy (particularly on the homeserver), ensureHistory() will cancel 52 | //! after \p maxWaitSeconds. 53 | //! \return the future that resolves to the event with \p eventId, or self-cancels if the event 54 | //! is not found 55 | Q_INVOKABLE EventFuture ensureHistory(const QString& upToEventId, quint16 maxWaitSeconds = 20); 56 | 57 | //! \brief Obtain an arbitrary room event by its id that is available locally 58 | //! 59 | Q_INVOKABLE const Quotient::RoomEvent* getSingleEvent(const QString& eventId, 60 | const QString& originEventId); 61 | 62 | void sendMessage(const QTextDocumentFragment& richText, 63 | HtmlFilter::Options htmlFilterOptions = HtmlFilter::Default); 64 | 65 | private: 66 | using EventPromise = QPromise; 67 | using EventId = Quotient::EventId; 68 | 69 | struct HistoryRequest { 70 | EventId upToEventId; 71 | QDeadlineTimer deadline; 72 | EventPromise promise{}; 73 | }; 74 | std::vector historyRequests; 75 | 76 | struct SingleEventRequest { 77 | EventId eventId; 78 | Quotient::JobHandle requestHandle; 79 | std::vector eventIdsToRefresh{}; 80 | }; 81 | std::vector singleEventRequests; 82 | std::unordered_map> cachedEvents; 83 | 84 | QSet highlights; 85 | QString m_cachedUserFilter; 86 | 87 | void onAddNewTimelineEvents(timeline_iter_t from) override; 88 | void onAddHistoricalTimelineEvents(rev_iter_t from) override; 89 | 90 | void checkForHighlights(const Quotient::TimelineItem& ti); 91 | void checkForRequestedEvents(const rev_iter_t& from); 92 | void onGettingSingleEvent(const QString& evtId); 93 | }; 94 | -------------------------------------------------------------------------------- /client/resources.qrc: -------------------------------------------------------------------------------- 1 | 2 | 3 | qml/Timeline.qml 4 | ../icons/quaternion/128-apps-quaternion.png 5 | ../icons/breeze/irc-channel-joined.svg 6 | ../icons/breeze/irc-channel-parted.svg 7 | ../icons/irc-channel-invited.svg 8 | ../icons/scrolldown.svg 9 | ../icons/scrollup.svg 10 | ../icons/busy_16x16.gif 11 | qml/Attachment.qml 12 | qml/ImageContent.qml 13 | qml/FileContent.qml 14 | qml/TimelineItem.qml 15 | qml/TimelineMouseArea.qml 16 | qml/TimelineTextEditSelector.qml 17 | qml/TimelineItemToolButton.qml 18 | qml/TimelineSettings.qml 19 | qml/NormalNumberAnimation.qml 20 | qml/FastNumberAnimation.qml 21 | qml/AnimationBehavior.qml 22 | qml/AnimatedTransition.qml 23 | qml/ScrollFinisher.qml 24 | qml/Logger.qml 25 | qml/Avatar.qml 26 | qml/RoomHeader.qml 27 | 28 | 29 | -------------------------------------------------------------------------------- /client/roomdialogs.h: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2017 Kitsune Ral 3 | * 4 | * SPDX-License-Identifier: LGPL-2.1-or-later 5 | */ 6 | 7 | #pragma once 8 | 9 | #include "dialog.h" 10 | 11 | #include 12 | 13 | namespace Quotient { 14 | class AccountRegistry; 15 | class Connection; 16 | } 17 | 18 | class MainWindow; 19 | class QuaternionRoom; 20 | class AccountSelector; 21 | 22 | class QComboBox; 23 | class QLineEdit; 24 | class QPlainTextEdit; 25 | class QCheckBox; 26 | class QPushButton; 27 | class QListWidget; 28 | class QFormLayout; 29 | class QStandardItemModel; 30 | 31 | class RoomDialogBase : public Dialog 32 | { 33 | Q_OBJECT 34 | protected: 35 | using Connection = Quotient::Connection; 36 | 37 | RoomDialogBase(const QString& title, const QString& applyButtonText, 38 | QuaternionRoom* r, QWidget* parent, 39 | QDialogButtonBox::StandardButtons extraButtons = QDialogButtonBox::Reset); 40 | 41 | protected: 42 | QuaternionRoom* room; 43 | 44 | QLabel* avatar; 45 | QLineEdit* roomName; 46 | QLabel* aliasServer; 47 | QLineEdit* alias; 48 | QPlainTextEdit* topic; 49 | QString previousTopic; 50 | QCheckBox* publishRoom; 51 | QCheckBox* guestCanJoin; 52 | QFormLayout* mainFormLayout; 53 | QFormLayout* essentialsLayout = nullptr; 54 | 55 | QComboBox* addVersionSelector(QLayout* layout); 56 | void refillVersionSelector(QComboBox* selector, Connection* account); 57 | void addEssentials(QWidget* accountControl, QLayout* versionBox); 58 | bool checkRoomVersion(QString version, Connection* account); 59 | }; 60 | 61 | class RoomSettingsDialog : public RoomDialogBase 62 | { 63 | Q_OBJECT 64 | public: 65 | RoomSettingsDialog(QuaternionRoom* room, MainWindow* parent = nullptr); 66 | 67 | private slots: 68 | void load() override; 69 | bool validate() override; 70 | void apply() override; 71 | 72 | private: 73 | QLabel* account; 74 | QLabel* version; 75 | QListWidget* tagsList; 76 | bool userChangedAvatar = false; 77 | }; 78 | 79 | class CreateRoomDialog : public RoomDialogBase 80 | { 81 | Q_OBJECT 82 | public: 83 | CreateRoomDialog(Quotient::AccountRegistry* accounts, 84 | QWidget* parent = nullptr); 85 | 86 | public slots: 87 | void updatePushButtons(); 88 | 89 | private slots: 90 | void load() override; 91 | bool validate() override; 92 | void apply() override; 93 | void accountSwitched(); 94 | 95 | private: 96 | AccountSelector* accountChooser; 97 | QComboBox* version; 98 | QComboBox* nextInvitee; 99 | QPushButton* addToInviteesButton; 100 | QPushButton* removeFromInviteesButton; 101 | QListWidget* invitees; 102 | 103 | QHash userLists; 104 | }; 105 | -------------------------------------------------------------------------------- /client/roomlistdock.h: -------------------------------------------------------------------------------- 1 | /************************************************************************** 2 | * * 3 | * SPDX-FileCopyrightText: 2015 Felix Rohrbach * 4 | * * 5 | * SPDX-License-Identifier: GPL-3.0-or-later 6 | * * 7 | **************************************************************************/ 8 | 9 | #pragma once 10 | 11 | #include 12 | #include 13 | #include 14 | //#include 15 | 16 | class MainWindow; 17 | class RoomListModel; 18 | class QuaternionRoom; 19 | 20 | namespace Quotient { 21 | class Connection; 22 | } 23 | 24 | class RoomListDock : public QDockWidget 25 | { 26 | Q_OBJECT 27 | public: 28 | explicit RoomListDock(MainWindow* parent = nullptr); 29 | 30 | void addConnection(Quotient::Connection* connection); 31 | void deleteConnection(Quotient::Connection* connection); 32 | 33 | public slots: 34 | void updateSortingMode(); 35 | void setSelectedRoom(QuaternionRoom* room); 36 | 37 | signals: 38 | void roomSelected(QuaternionRoom* room); 39 | 40 | private slots: 41 | void rowSelected(const QModelIndex& index); 42 | void showContextMenu(const QPoint& pos); 43 | void addTagsSelected(); 44 | void refreshTitle(); 45 | 46 | private: 47 | QTreeView* view = nullptr; 48 | RoomListModel* model = nullptr; 49 | // QSortFilterProxyModel* proxyModel; 50 | QMenu* roomContextMenu = nullptr; 51 | QMenu* groupContextMenu = nullptr; 52 | QAction* markAsReadAction = nullptr; 53 | QAction* addTagsAction = nullptr; 54 | QAction* joinAction = nullptr; 55 | QAction* leaveAction = nullptr; 56 | QAction* forgetAction = nullptr; 57 | QAction* deleteTagAction = nullptr; 58 | QAction* roomSettingsAction = nullptr; 59 | QAction* roomPermalinkAction = nullptr; 60 | QVariant selectedGroupCache = {}; 61 | QuaternionRoom* selectedRoomCache = nullptr; 62 | 63 | QVariant getSelectedGroup() const; 64 | QuaternionRoom* getSelectedRoom() const; 65 | }; 66 | -------------------------------------------------------------------------------- /client/systemtrayicon.cpp: -------------------------------------------------------------------------------- 1 | /************************************************************************** 2 | * * 3 | * SPDX-FileCopyrightText: 2016 Felix Rohrbach * 4 | * * 5 | * SPDX-License-Identifier: GPL-3.0-or-later 6 | * * 7 | **************************************************************************/ 8 | 9 | #include "systemtrayicon.h" 10 | 11 | #include 12 | #include 13 | #include 14 | 15 | #include "mainwindow.h" 16 | #include "quaternionroom.h" 17 | #include "desktop_integration.h" 18 | 19 | #include 20 | #include 21 | 22 | using namespace Qt::StringLiterals; 23 | 24 | SystemTrayIcon::SystemTrayIcon(MainWindow* parent) : QSystemTrayIcon(parent) 25 | { 26 | auto contextMenu = new QMenu(parent); 27 | auto showHideAction = 28 | contextMenu->addAction(tr("Hide"), this, &SystemTrayIcon::showHide); 29 | contextMenu->addAction(tr("Quit"), this, QApplication::quit); 30 | mainWindow()->winId(); // To make sure mainWindow()->windowHandle() is initialised 31 | connect(mainWindow()->windowHandle(), &QWindow::visibleChanged, [showHideAction](bool visible) { 32 | showHideAction->setText(visible ? tr("Hide") : tr("Show")); 33 | }); 34 | 35 | setIcon(appIcon()); 36 | setToolTip("Quaternion"); 37 | setContextMenu(contextMenu); 38 | connect(this, &SystemTrayIcon::activated, this, &SystemTrayIcon::systemTrayIconAction); 39 | connect(qApp, &QApplication::focusChanged, this, &SystemTrayIcon::focusChanged); 40 | } 41 | 42 | void SystemTrayIcon::newRoom(Quotient::Room* room) 43 | { 44 | unreadStatsChanged(); 45 | highlightCountChanged(room); 46 | connect(room, &Quotient::Room::unreadStatsChanged, this, &SystemTrayIcon::unreadStatsChanged); 47 | connect(room, &Quotient::Room::highlightCountChanged, this, 48 | [this, room] { highlightCountChanged(room); }); 49 | } 50 | 51 | void SystemTrayIcon::unreadStatsChanged() 52 | { 53 | const auto mode = notificationMode(); 54 | if (mode == u"none") 55 | return; 56 | 57 | int nNotifs = 0; 58 | for (auto* c: mainWindow()->registry()->accounts()) 59 | for (auto* r: c->allRooms()) 60 | nNotifs += r->notificationCount(); 61 | setToolTip(tr("%Ln unread message(s) across all rooms", "", nNotifs)); 62 | 63 | if (m_notified || qApp->activeWindow() != nullptr) 64 | return; 65 | 66 | if (nNotifs == 0) { 67 | setIcon(appIcon()); 68 | return; 69 | } 70 | 71 | static const auto unreadIcon = QIcon::fromTheme(u"mail-unread"_s, appIcon()); 72 | setIcon(unreadIcon); 73 | m_notified = true; 74 | } 75 | 76 | void SystemTrayIcon::highlightCountChanged(Quotient::Room* room) 77 | { 78 | if (qApp->activeWindow() != nullptr || room->highlightCount() == 0) 79 | return; 80 | 81 | const auto mode = notificationMode(); 82 | if (mode == u"none") 83 | return; 84 | 85 | //: %1 is the room display name 86 | showMessage(tr("Highlight in %1").arg(room->displayName()), 87 | tr("%Ln highlight(s)", "", static_cast(room->highlightCount()))); 88 | if (mode == u"intrusive") 89 | mainWindow()->activateWindow(); 90 | 91 | connect(this, &SystemTrayIcon::messageClicked, mainWindow(), 92 | [this, r = static_cast(room)] { mainWindow()->selectRoom(r); }, 93 | Qt::SingleShotConnection); 94 | } 95 | 96 | void SystemTrayIcon::systemTrayIconAction(QSystemTrayIcon::ActivationReason reason) 97 | { 98 | if (reason == QSystemTrayIcon::Trigger 99 | || reason == QSystemTrayIcon::DoubleClick) 100 | showHide(); 101 | } 102 | 103 | void SystemTrayIcon::showHide() 104 | { 105 | if (mainWindow()->isVisible()) 106 | mainWindow()->hide(); 107 | else { 108 | mainWindow()->show(); 109 | mainWindow()->activateWindow(); 110 | mainWindow()->raise(); 111 | mainWindow()->setFocus(); 112 | } 113 | } 114 | 115 | MainWindow* SystemTrayIcon::mainWindow() const { return static_cast(parent()); } 116 | 117 | QString SystemTrayIcon::notificationMode() const 118 | { 119 | static const Quotient::Settings settings{}; 120 | return settings.get("UI/notifications", u"intrusive"_s); 121 | } 122 | 123 | void SystemTrayIcon::focusChanged(QWidget* old) 124 | { 125 | if (m_notified && old == nullptr && qApp->activeWindow() != nullptr) { 126 | setIcon(appIcon()); 127 | m_notified = false; 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /client/systemtrayicon.h: -------------------------------------------------------------------------------- 1 | /************************************************************************** 2 | * * 3 | * SPDX-FileCopyrightText: 2016 Felix Rohrbach * 4 | * * 5 | * SPDX-License-Identifier: GPL-3.0-or-later 6 | * * 7 | **************************************************************************/ 8 | 9 | #pragma once 10 | 11 | #include 12 | 13 | namespace Quotient 14 | { 15 | class Room; 16 | } 17 | 18 | class MainWindow; 19 | 20 | class SystemTrayIcon: public QSystemTrayIcon 21 | { 22 | Q_OBJECT 23 | public: 24 | explicit SystemTrayIcon(MainWindow* parent = nullptr); 25 | 26 | public slots: 27 | void newRoom(Quotient::Room* room); 28 | 29 | private slots: 30 | void unreadStatsChanged(); 31 | void highlightCountChanged(Quotient::Room* room); 32 | void systemTrayIconAction(QSystemTrayIcon::ActivationReason reason); 33 | void focusChanged(QWidget* old); 34 | 35 | private: 36 | bool m_notified; 37 | 38 | void showHide(); 39 | MainWindow* mainWindow() const; 40 | QString notificationMode() const; 41 | }; 42 | -------------------------------------------------------------------------------- /client/timelinewidget.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "activitydetector.h" 4 | 5 | #include 6 | 7 | #include 8 | #include 9 | 10 | #include 11 | 12 | class ChatRoomWidget; 13 | class MessageEventModel; 14 | class QuaternionRoom; 15 | 16 | class TimelineWidget : public QQuickWidget { 17 | Q_OBJECT 18 | public: 19 | TimelineWidget(ChatRoomWidget* chatRoomWidget); 20 | ~TimelineWidget() override; 21 | QString selectedText() const; 22 | QuaternionRoom* currentRoom() const; 23 | Q_INVOKABLE Qt::KeyboardModifiers getModifierKeys() const; 24 | Q_INVOKABLE bool isHistoryRequestRunning() const; 25 | 26 | signals: 27 | void resourceRequested(const QString& idOrUri, const QString& action = {}); 28 | void roomSettingsRequested(); 29 | void showStatusMessage(const QString& message, int timeout = 0) const; 30 | void pageUpPressed(); 31 | void pageDownPressed(); 32 | void openExternally(int currentIndex); 33 | void showDetails(int currentIndex); 34 | void viewPositionRequested(int index); 35 | void animateMessage(int currentIndex); 36 | void historyRequestChanged(); 37 | 38 | public slots: 39 | void setRoom(QuaternionRoom* room); 40 | void focusInput(); 41 | void spotlightEvent(const QString& eventId); 42 | void onMessageShownChanged(int visualIndex, bool shown, bool hasReadMarker); 43 | void markShownAsRead(); 44 | void saveFileAs(const QString& eventId); 45 | void showMenu(int index, const QString& hoveredLink, 46 | const QString& selectedText, bool showingDetails); 47 | void reactionButtonClicked(const QString& eventId, const QString& key); 48 | void setGlobalSelectionBuffer(const QString& text); 49 | void ensureLastReadEvent(); 50 | 51 | private: 52 | MessageEventModel* m_messageModel; 53 | QString m_selectedText; 54 | 55 | using timeline_index_t = Quotient::TimelineItem::index_t; 56 | std::vector indicesOnScreen; 57 | timeline_index_t indexToMaybeRead; 58 | QBasicTimer maybeReadTimer; 59 | bool readMarkerOnScreen; 60 | ActivityDetector activityDetector; 61 | QFuture historyRequest; 62 | 63 | class NamFactory : public QQmlNetworkAccessManagerFactory { 64 | public: 65 | QNetworkAccessManager* create(QObject* parent) override; 66 | }; 67 | NamFactory namFactory; 68 | 69 | ChatRoomWidget* roomWidget() const; 70 | void reStartShownTimer(); 71 | void timerEvent(QTimerEvent* qte) override; 72 | bool pendingMarkRead() const; 73 | }; 74 | -------------------------------------------------------------------------------- /client/userlistdock.cpp: -------------------------------------------------------------------------------- 1 | /************************************************************************** 2 | * * 3 | * SPDX-FileCopyrightText: 2015 Felix Rohrbach * 4 | * * 5 | * SPDX-License-Identifier: GPL-3.0-or-later 6 | * * 7 | **************************************************************************/ 8 | 9 | #include "userlistdock.h" 10 | 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | 18 | #include 19 | #include 20 | #include 21 | #include 22 | 23 | #include "models/userlistmodel.h" 24 | #include "quaternionroom.h" 25 | 26 | UserListDock::UserListDock(QWidget* parent) 27 | : QDockWidget(tr("Users"), parent) 28 | { 29 | setObjectName(QStringLiteral("UsersDock")); 30 | 31 | m_box = new QVBoxLayout(); 32 | 33 | m_box->addSpacing(1); 34 | m_filterline = new QLineEdit(this); 35 | m_filterline->setPlaceholderText(tr("Search")); 36 | m_filterline->setDisabled(true); 37 | m_box->addWidget(m_filterline); 38 | 39 | m_view = new QTableView(this); 40 | m_view->setShowGrid(false); 41 | // Derive the member icon size from that of the default icon used when 42 | // the member doesn't have an avatar 43 | const auto iconExtent = m_view->fontMetrics().height() * 3 / 2; 44 | m_view->setIconSize( 45 | QIcon::fromTheme("user-available", QIcon(":/irc-channel-joined")) 46 | .actualSize({ iconExtent, iconExtent })); 47 | m_view->horizontalHeader()->setStretchLastSection(true); 48 | m_view->horizontalHeader()->setVisible(false); 49 | m_view->verticalHeader()->setVisible(false); 50 | m_box->addWidget(m_view); 51 | 52 | m_widget = new QWidget(this); 53 | m_widget->setLayout(m_box); 54 | setWidget(m_widget); 55 | 56 | connect(m_view, &QTableView::activated, 57 | this, &UserListDock::requestUserMention); 58 | connect( m_view, &QTableView::pressed, this, [this] { 59 | if (QGuiApplication::mouseButtons() & Qt::MiddleButton) 60 | startChatSelected(); 61 | }); 62 | 63 | m_model = new UserListModel(m_view); 64 | m_view->setModel(m_model); 65 | 66 | connect( m_model, &UserListModel::membersChanged, 67 | this, &UserListDock::refreshTitle ); 68 | connect( m_model, &QAbstractListModel::modelReset, 69 | this, &UserListDock::refreshTitle ); 70 | connect(m_filterline, &QLineEdit::textEdited, 71 | m_model, &UserListModel::filter); 72 | 73 | setContextMenuPolicy(Qt::CustomContextMenu); 74 | connect(this, &QWidget::customContextMenuRequested, 75 | this, &UserListDock::showContextMenu); 76 | } 77 | 78 | void UserListDock::setRoom(QuaternionRoom* room) 79 | { 80 | if (m_currentRoom) 81 | m_currentRoom->setCachedUserFilter(m_filterline->text()); 82 | m_currentRoom = room; 83 | m_model->setRoom(room); 84 | m_filterline->setEnabled(room); 85 | m_filterline->setText(room ? room->cachedUserFilter() : ""); 86 | m_model->filter(m_filterline->text()); 87 | } 88 | 89 | void UserListDock::refreshTitle() 90 | { 91 | setWindowTitle(tr("Users") + 92 | (!m_currentRoom ? QString() : 93 | ' ' + (m_model->rowCount() == m_currentRoom->joinedCount() ? 94 | QStringLiteral("(%L1)").arg(m_currentRoom->joinedCount()) : 95 | tr("(%L1 out of %L2)", "%found out of %total users") 96 | .arg(m_model->rowCount()).arg(m_currentRoom->joinedCount()))) 97 | ); 98 | } 99 | 100 | void UserListDock::showContextMenu(QPoint pos) 101 | { 102 | if (getSelectedUser().isEmpty()) 103 | return; 104 | 105 | auto* contextMenu = new QMenu(this); 106 | 107 | contextMenu->addAction(QIcon::fromTheme("contact-new"), 108 | tr("Open direct chat"), this, &UserListDock::startChatSelected); 109 | contextMenu->addAction(tr("Mention user"), this, 110 | &UserListDock::requestUserMention); 111 | QAction* ignoreAction = 112 | contextMenu->addAction(QIcon::fromTheme("mail-thread-ignored"), 113 | tr("Ignore user"), this, &UserListDock::ignoreUser); 114 | ignoreAction->setCheckable(true); 115 | contextMenu->addSeparator(); 116 | 117 | const auto* plEvt = 118 | m_currentRoom->currentState().get(); 119 | const int userPl = 120 | plEvt ? plEvt->powerLevelForUser(m_currentRoom->localMember().id()) : 0; 121 | 122 | if (!plEvt || userPl >= plEvt->kick()) { 123 | contextMenu->addAction(QIcon::fromTheme("im-ban-kick-user"), 124 | tr("Kick user"), this,&UserListDock::kickUser); 125 | } 126 | if (!plEvt || userPl >= plEvt->ban()) { 127 | contextMenu->addAction(QIcon::fromTheme("im-ban-user"), 128 | tr("Ban user"), this, &UserListDock::banUser); 129 | } 130 | 131 | contextMenu->popup(mapToGlobal(pos)); 132 | ignoreAction->setChecked(isIgnored()); 133 | } 134 | 135 | void UserListDock::startChatSelected() 136 | { 137 | if (auto userId = getSelectedUser(); !userId.isEmpty()) 138 | m_currentRoom->connection()->requestDirectChat(userId); 139 | } 140 | 141 | void UserListDock::requestUserMention() 142 | { 143 | if (auto userId = getSelectedUser(); !userId.isEmpty()) 144 | emit userMentionRequested(userId); 145 | } 146 | 147 | void UserListDock::kickUser() 148 | { 149 | if (auto userId = getSelectedUser(); !userId.isEmpty()) 150 | { 151 | bool ok; 152 | const auto reason = QInputDialog::getText(this, 153 | tr("Kick %1").arg(userId), tr("Reason"), 154 | QLineEdit::Normal, nullptr, &ok); 155 | if (ok) { 156 | m_currentRoom->kickMember(userId, reason); 157 | } 158 | } 159 | } 160 | 161 | void UserListDock::banUser() 162 | { 163 | if (auto userId = getSelectedUser(); !userId.isEmpty()) 164 | { 165 | bool ok; 166 | const auto reason = QInputDialog::getText(this, 167 | tr("Ban %1").arg(userId), tr("Reason"), 168 | QLineEdit::Normal, nullptr, &ok); 169 | if (ok) { 170 | m_currentRoom->ban(userId, reason); 171 | } 172 | } 173 | } 174 | 175 | void UserListDock::ignoreUser() 176 | { 177 | if (auto* user = m_currentRoom->connection()->user(getSelectedUser())) { 178 | if (!user->isIgnored()) 179 | user->ignore(); 180 | else 181 | user->unmarkIgnore(); 182 | } 183 | } 184 | 185 | bool UserListDock::isIgnored() 186 | { 187 | if (auto memberId = getSelectedUser(); !memberId.isEmpty()) 188 | return m_currentRoom->connection()->isIgnored(memberId); 189 | return false; 190 | } 191 | 192 | QString UserListDock::getSelectedUser() const 193 | { 194 | auto index = m_view->currentIndex(); 195 | if (!index.isValid()) 196 | return {}; 197 | const auto member = m_model->userAt(index); 198 | Q_ASSERT(!member.isEmpty()); 199 | return member.id(); 200 | } 201 | -------------------------------------------------------------------------------- /client/userlistdock.h: -------------------------------------------------------------------------------- 1 | /************************************************************************** 2 | * * 3 | * SPDX-FileCopyrightText: 2015 Felix Rohrbach * 4 | * * 5 | * SPDX-License-Identifier: GPL-3.0-or-later 6 | * * 7 | **************************************************************************/ 8 | 9 | #pragma once 10 | 11 | #include 12 | #include 13 | 14 | class UserListModel; 15 | class QuaternionRoom; 16 | class QTableView; 17 | class QLineEdit; 18 | 19 | class UserListDock: public QDockWidget 20 | { 21 | Q_OBJECT 22 | public: 23 | explicit UserListDock(QWidget* parent = nullptr); 24 | 25 | void setRoom( QuaternionRoom* room ); 26 | 27 | signals: 28 | void userMentionRequested(QString userId); 29 | 30 | private slots: 31 | void refreshTitle(); 32 | void showContextMenu(QPoint pos); 33 | void startChatSelected(); 34 | void requestUserMention(); 35 | void kickUser(); 36 | void banUser(); 37 | void ignoreUser(); 38 | bool isIgnored(); 39 | 40 | private: 41 | QWidget* m_widget; 42 | QVBoxLayout* m_box; 43 | QTableView* m_view; 44 | QLineEdit* m_filterline; 45 | UserListModel* m_model; 46 | QuaternionRoom* m_currentRoom = nullptr; 47 | 48 | QString getSelectedUser() const; 49 | }; 50 | -------------------------------------------------------------------------------- /client/verificationdialog.cpp: -------------------------------------------------------------------------------- 1 | #include "verificationdialog.h" 2 | 3 | #include 4 | 5 | #include 6 | #include 7 | 8 | #include 9 | 10 | using namespace Qt::StringLiterals; 11 | 12 | VerificationDialog::VerificationDialog(Session* session, QWidget* parent) 13 | : Dialog(tr("Verifying device %1").arg(session->remoteDeviceId()), 14 | QDialogButtonBox::Ok | QDialogButtonBox::Discard, parent) 15 | , session(session) 16 | { 17 | // The same check as in Session::handleEvent() in the KeyVerificationKeyEvent branch 18 | QUO_CHECK(session->state() == Session::WAITINGFORKEY || session->state() == Session::ACCEPTED); 19 | 20 | addWidget(new QLabel( 21 | tr("Confirm that the same icons, in the same order are displayed on the other side"))); 22 | const auto emojis = session->sasEmojis(); 23 | constexpr auto rowsCount = 2; 24 | const auto rowSize = (emojis.size() + 1) / rowsCount; 25 | auto emojiGrid = addLayout(); 26 | for (int i = 0; i < emojis.size(); ++i) { 27 | auto emojiLayout = new QVBoxLayout(); 28 | auto emoji = new QLabel(emojis[i].emoji); 29 | emoji->setFont({ u"emoji"_s, emoji->font().pointSize() * 4 }); 30 | for (auto* const l : { emoji, new QLabel(emojis[i].description) }) { 31 | emojiLayout->addWidget(l); 32 | emojiLayout->setAlignment(l, Qt::AlignCenter); 33 | } 34 | emojiGrid->addLayout(emojiLayout); 35 | if (i % rowSize == rowSize - 1) 36 | emojiGrid = addLayout(); // Start new line 37 | } 38 | button(QDialogButtonBox::Ok)->setText(tr("They match")); 39 | button(QDialogButtonBox::Discard)->setText(tr("They DON'T match")); 40 | 41 | // Pin lifecycles of the dialog and the session, avoiding recursion (by the time 42 | // QObject::destroyed is emitted, KeyVerificationSession signals are disconnected) 43 | connect(session, &QObject::destroyed, this, &QDialog::reject); 44 | // NB: this is only triggered when a dialog is closed using a window close button; 45 | // QDialogButtonBox::Discard doesn't trigger QDialog::rejected as it has DestructiveRole 46 | connect(this, &QDialog::rejected, session, [session] { 47 | if (session->state() != Session::CANCELED) 48 | session->cancelVerification(Session::USER); 49 | }); 50 | } 51 | 52 | VerificationDialog::~VerificationDialog() = default; 53 | 54 | void VerificationDialog::buttonClicked(QAbstractButton* button) 55 | { 56 | if (button == this->button(QDialogButtonBox::Ok)) { 57 | session->sendMac(); 58 | accept(); 59 | } else if (button == this->button(QDialogButtonBox::Discard)) { 60 | session->cancelVerification(Session::MISMATCHED_SAS); 61 | reject(); 62 | } else 63 | QUO_ALARM_X(false, "Unknown button: " % button->text()); 64 | } 65 | -------------------------------------------------------------------------------- /client/verificationdialog.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "dialog.h" 4 | 5 | namespace Quotient { 6 | class KeyVerificationSession; 7 | } 8 | 9 | class VerificationDialog : public Dialog { 10 | Q_OBJECT 11 | public: 12 | using Session = Quotient::KeyVerificationSession; 13 | 14 | VerificationDialog(Session* session, QWidget* parent); 15 | ~VerificationDialog() override; 16 | 17 | private: // Overrides 18 | void buttonClicked(QAbstractButton* button) override; 19 | 20 | private: // Data 21 | Session* session; 22 | }; 23 | -------------------------------------------------------------------------------- /cmake/MacOSXBundleInfo.plist.in: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | English 7 | CFBundleExecutable 8 | ${MACOSX_BUNDLE_EXECUTABLE_NAME} 9 | CFBundleIconFile 10 | ${MACOSX_BUNDLE_ICON_FILE} 11 | CFBundleIdentifier 12 | ${MACOSX_BUNDLE_GUI_IDENTIFIER} 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | ${MACOSX_BUNDLE_BUNDLE_NAME} 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | ${MACOSX_BUNDLE_SHORT_VERSION_STRING} 21 | CFBundleVersion 22 | ${MACOSX_BUNDLE_BUNDLE_VERSION} 23 | CSResourcesFileMapped 24 | 25 | NSHumanReadableCopyright 26 | ${MACOSX_BUNDLE_COPYRIGHT} 27 | NSPrincipalClass 28 | NSApplication 29 | NSHighResolutionCapable 30 | True 31 | 32 | 33 | -------------------------------------------------------------------------------- /flatpak/build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | flatpak-builder --ccache --force-clean --require-changes --repo=repo --subject="Nightly build of Quaternion, `date`" ${EXPORT_ARGS-} app io.github.quotient_im.Quaternion.yaml 4 | 5 | flatpak --user remote-add --if-not-exists quaternion-nightly repo/ --no-gpg-verify 6 | -------------------------------------------------------------------------------- /flatpak/io.github.quotient_im.Quaternion.yaml: -------------------------------------------------------------------------------- 1 | id: io.github.quotient_im.Quaternion 2 | rename-icon: quaternion 3 | runtime: org.kde.Platform 4 | runtime-version: '6.8' 5 | sdk: org.kde.Sdk 6 | command: quaternion 7 | finish-args: 8 | - --share=ipc 9 | - --share=network 10 | - --socket=wayland 11 | - --socket=fallback-x11 12 | - --device=dri 13 | - --filesystem=xdg-download 14 | - --talk-name=org.freedesktop.secrets 15 | - --talk-name=org.kde.kwalletd5 16 | - --talk-name=org.freedesktop.Notifications 17 | - --talk-name=org.kde.StatusNotifierWatcher 18 | cleanup: 19 | - /include 20 | - /lib/pkgconfig 21 | - /share/man 22 | modules: 23 | - name: libolm 24 | buildsystem: cmake-ninja 25 | builddir: true 26 | sources: 27 | - type: git 28 | url: https://gitlab.matrix.org/matrix-org/olm.git 29 | tag: '3.2.15' 30 | config-opts: 31 | - -DBUILD_SHARED_LIBS=OFF 32 | - -DOLM_TESTS=OFF 33 | cleanup: 34 | - /lib 35 | - /share 36 | 37 | - name: libsecret 38 | buildsystem: meson 39 | builddir: true 40 | config-opts: 41 | - -Dmanpage=false 42 | - -Dvapi=false 43 | - -Dgtk_doc=false 44 | - -Dintrospection=false 45 | - -Dcrypto=disabled 46 | cleanup: 47 | - /bin 48 | sources: 49 | - type: git 50 | url: https://gitlab.gnome.org/GNOME/libsecret.git 51 | tag: '0.21.4' 52 | 53 | - name: qtkeychain 54 | buildsystem: cmake-ninja 55 | builddir: true 56 | sources: 57 | - type: git 58 | url: https://github.com/frankosterfeld/qtkeychain.git 59 | tag: '0.14.3' 60 | cleanup: 61 | - mkspecs 62 | - /lib/cmake 63 | config-opts: 64 | - -DBUILD_WITH_QT6=ON 65 | # When linked statically, Qt Keychain fails to work with all kinds of strange DBus errors 66 | # - -DBUILD_SHARED_LIBS=OFF 67 | - -DCMAKE_INSTALL_LIBDIR=/app/lib 68 | - -DLIB_INSTALL_DIR=/app/lib 69 | - -DBUILD_TEST_APPLICATION=OFF 70 | 71 | - name: quaternion 72 | buildsystem: cmake-ninja 73 | builddir: true 74 | sources: 75 | - type: dir 76 | path: "../" 77 | config-opts: 78 | - -DBUILD_TESTING=OFF 79 | - -DBUILD_SHARED_LIBS=OFF 80 | cleanup: 81 | - /lib 82 | - /share/ndk-modules 83 | -------------------------------------------------------------------------------- /flatpak/setup_runtime.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | flatpak --user remote-add flathub --if-not-exists --from https://flathub.org/repo/flathub.flatpakrepo 4 | flatpak --user install flathub org.kde.Platform//6.8 5 | flatpak --user install flathub org.kde.Sdk//6.8 6 | -------------------------------------------------------------------------------- /icons/breeze/COPYING.breeze: -------------------------------------------------------------------------------- 1 | The Breeze Icon Theme in this folder 2 | 3 | Copyright (C) 2014 Uri Herrera and others 4 | 5 | This library is free software; you can redistribute it and/or 6 | modify it under the terms of the GNU Lesser General Public 7 | License as published by the Free Software Foundation; either 8 | version 3 of the License, or (at your option) any later version. 9 | 10 | This library is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 13 | Lesser General Public License for more details. 14 | 15 | You should have received a copy of the GNU Lesser General Public 16 | License along with this library. If not, see . 17 | 18 | Clarification: 19 | 20 | The GNU Lesser General Public License or LGPL is written for 21 | software libraries in the first place. We expressly want the LGPL to 22 | be valid for this artwork library too. 23 | 24 | KDE Breeze theme icons is a special kind of software library, it is an 25 | artwork library, it's elements can be used in a Graphical User Interface, or 26 | GUI. 27 | 28 | Source code, for this library means: 29 | - where they exist, SVG; 30 | - otherwise, if applicable, the multi-layered formats xcf or psd, or 31 | otherwise png. 32 | 33 | The LGPL in some sections obliges you to make the files carry 34 | notices. With images this is in some cases impossible or hardly useful. 35 | 36 | With this library a notice is placed at a prominent place in the directory 37 | containing the elements. You may follow this practice. 38 | 39 | The exception in section 5 of the GNU Lesser General Public License covers 40 | the use of elements of this art library in a GUI. 41 | 42 | https://vdesign.kde.org/ 43 | 44 | ----- 45 | GNU LESSER GENERAL PUBLIC LICENSE 46 | Version 3, 29 June 2007 47 | 48 | Copyright (C) 2007 Free Software Foundation, Inc. 49 | Everyone is permitted to copy and distribute verbatim copies 50 | of this license document, but changing it is not allowed. 51 | 52 | 53 | This version of the GNU Lesser General Public License incorporates 54 | the terms and conditions of version 3 of the GNU General Public 55 | License, supplemented by the additional permissions listed below. 56 | 57 | 0. Additional Definitions. 58 | 59 | As used herein, "this License" refers to version 3 of the GNU Lesser 60 | General Public License, and the "GNU GPL" refers to version 3 of the GNU 61 | General Public License. 62 | 63 | "The Library" refers to a covered work governed by this License, 64 | other than an Application or a Combined Work as defined below. 65 | 66 | An "Application" is any work that makes use of an interface provided 67 | by the Library, but which is not otherwise based on the Library. 68 | Defining a subclass of a class defined by the Library is deemed a mode 69 | of using an interface provided by the Library. 70 | 71 | A "Combined Work" is a work produced by combining or linking an 72 | Application with the Library. The particular version of the Library 73 | with which the Combined Work was made is also called the "Linked 74 | Version". 75 | 76 | The "Minimal Corresponding Source" for a Combined Work means the 77 | Corresponding Source for the Combined Work, excluding any source code 78 | for portions of the Combined Work that, considered in isolation, are 79 | based on the Application, and not on the Linked Version. 80 | 81 | The "Corresponding Application Code" for a Combined Work means the 82 | object code and/or source code for the Application, including any data 83 | and utility programs needed for reproducing the Combined Work from the 84 | Application, but excluding the System Libraries of the Combined Work. 85 | 86 | 1. Exception to Section 3 of the GNU GPL. 87 | 88 | You may convey a covered work under sections 3 and 4 of this License 89 | without being bound by section 3 of the GNU GPL. 90 | 91 | 2. Conveying Modified Versions. 92 | 93 | If you modify a copy of the Library, and, in your modifications, a 94 | facility refers to a function or data to be supplied by an Application 95 | that uses the facility (other than as an argument passed when the 96 | facility is invoked), then you may convey a copy of the modified 97 | version: 98 | 99 | a) under this License, provided that you make a good faith effort to 100 | ensure that, in the event an Application does not supply the 101 | function or data, the facility still operates, and performs 102 | whatever part of its purpose remains meaningful, or 103 | 104 | b) under the GNU GPL, with none of the additional permissions of 105 | this License applicable to that copy. 106 | 107 | 3. Object Code Incorporating Material from Library Header Files. 108 | 109 | The object code form of an Application may incorporate material from 110 | a header file that is part of the Library. You may convey such object 111 | code under terms of your choice, provided that, if the incorporated 112 | material is not limited to numerical parameters, data structure 113 | layouts and accessors, or small macros, inline functions and templates 114 | (ten or fewer lines in length), you do both of the following: 115 | 116 | a) Give prominent notice with each copy of the object code that the 117 | Library is used in it and that the Library and its use are 118 | covered by this License. 119 | 120 | b) Accompany the object code with a copy of the GNU GPL and this license 121 | document. 122 | 123 | 4. Combined Works. 124 | 125 | You may convey a Combined Work under terms of your choice that, 126 | taken together, effectively do not restrict modification of the 127 | portions of the Library contained in the Combined Work and reverse 128 | engineering for debugging such modifications, if you also do each of 129 | the following: 130 | 131 | a) Give prominent notice with each copy of the Combined Work that 132 | the Library is used in it and that the Library and its use are 133 | covered by this License. 134 | 135 | b) Accompany the Combined Work with a copy of the GNU GPL and this license 136 | document. 137 | 138 | c) For a Combined Work that displays copyright notices during 139 | execution, include the copyright notice for the Library among 140 | these notices, as well as a reference directing the user to the 141 | copies of the GNU GPL and this license document. 142 | 143 | d) Do one of the following: 144 | 145 | 0) Convey the Minimal Corresponding Source under the terms of this 146 | License, and the Corresponding Application Code in a form 147 | suitable for, and under terms that permit, the user to 148 | recombine or relink the Application with a modified version of 149 | the Linked Version to produce a modified Combined Work, in the 150 | manner specified by section 6 of the GNU GPL for conveying 151 | Corresponding Source. 152 | 153 | 1) Use a suitable shared library mechanism for linking with the 154 | Library. A suitable mechanism is one that (a) uses at run time 155 | a copy of the Library already present on the user's computer 156 | system, and (b) will operate properly with a modified version 157 | of the Library that is interface-compatible with the Linked 158 | Version. 159 | 160 | e) Provide Installation Information, but only if you would otherwise 161 | be required to provide such information under section 6 of the 162 | GNU GPL, and only to the extent that such information is 163 | necessary to install and execute a modified version of the 164 | Combined Work produced by recombining or relinking the 165 | Application with a modified version of the Linked Version. (If 166 | you use option 4d0, the Installation Information must accompany 167 | the Minimal Corresponding Source and Corresponding Application 168 | Code. If you use option 4d1, you must provide the Installation 169 | Information in the manner specified by section 6 of the GNU GPL 170 | for conveying Corresponding Source.) 171 | 172 | 5. Combined Libraries. 173 | 174 | You may place library facilities that are a work based on the 175 | Library side by side in a single library together with other library 176 | facilities that are not Applications and are not covered by this 177 | License, and convey such a combined library under terms of your 178 | choice, if you do both of the following: 179 | 180 | a) Accompany the combined library with a copy of the same work based 181 | on the Library, uncombined with any other library facilities, 182 | conveyed under the terms of this License. 183 | 184 | b) Give prominent notice with the combined library that part of it 185 | is a work based on the Library, and explaining where to find the 186 | accompanying uncombined form of the same work. 187 | 188 | 6. Revised Versions of the GNU Lesser General Public License. 189 | 190 | The Free Software Foundation may publish revised and/or new versions 191 | of the GNU Lesser General Public License from time to time. Such new 192 | versions will be similar in spirit to the present version, but may 193 | differ in detail to address new problems or concerns. 194 | 195 | Each version is given a distinguishing version number. If the 196 | Library as you received it specifies that a certain numbered version 197 | of the GNU Lesser General Public License "or any later version" 198 | applies to it, you have the option of following the terms and 199 | conditions either of that published version or of any later version 200 | published by the Free Software Foundation. If the Library as you 201 | received it does not specify a version number of the GNU Lesser 202 | General Public License, you may choose any version of the GNU Lesser 203 | General Public License ever published by the Free Software Foundation. 204 | 205 | If the Library as you received it specifies that a proxy can decide 206 | whether future versions of the GNU Lesser General Public License shall 207 | apply, that proxy's public statement of acceptance of any version is 208 | permanent authorization for you to choose that version for the 209 | Library. 210 | 211 | -------------------------------------------------------------------------------- /icons/breeze/README.breeze: -------------------------------------------------------------------------------- 1 | The icons in this folder where imported from the breeze icon set. 2 | 3 | Repository: anongit.kde.org:breeze-icons.git 4 | Commit: 00f3ea7a763dde4d676ece8186c1cdbe52f6c2fc -------------------------------------------------------------------------------- /icons/breeze/irc-channel-joined.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | image/svg+xml 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | -------------------------------------------------------------------------------- /icons/breeze/irc-channel-parted.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | image/svg+xml 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | -------------------------------------------------------------------------------- /icons/busy_16x16.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quotient-im/Quaternion/c85d16312ee42a341c10c55e5bfb9c4c3b97c20b/icons/busy_16x16.gif -------------------------------------------------------------------------------- /icons/irc-channel-invited.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 18 | 20 | 57 | 58 | 81 | 85 | 89 | 93 | 97 | 101 | 105 | 109 | 113 | 117 | 121 | 125 | 128 | 129 | 131 | 132 | 134 | image/svg+xml 135 | 137 | 138 | 139 | 140 | 141 | 146 | 152 | 160 | 161 | 162 | -------------------------------------------------------------------------------- /icons/quaternion.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quotient-im/Quaternion/c85d16312ee42a341c10c55e5bfb9c4c3b97c20b/icons/quaternion.icns -------------------------------------------------------------------------------- /icons/quaternion.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quotient-im/Quaternion/c85d16312ee42a341c10c55e5bfb9c4c3b97c20b/icons/quaternion.ico -------------------------------------------------------------------------------- /icons/quaternion/128-apps-quaternion.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quotient-im/Quaternion/c85d16312ee42a341c10c55e5bfb9c4c3b97c20b/icons/quaternion/128-apps-quaternion.png -------------------------------------------------------------------------------- /icons/quaternion/16-apps-quaternion.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quotient-im/Quaternion/c85d16312ee42a341c10c55e5bfb9c4c3b97c20b/icons/quaternion/16-apps-quaternion.png -------------------------------------------------------------------------------- /icons/quaternion/22-apps-quaternion.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quotient-im/Quaternion/c85d16312ee42a341c10c55e5bfb9c4c3b97c20b/icons/quaternion/22-apps-quaternion.png -------------------------------------------------------------------------------- /icons/quaternion/32-apps-quaternion.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quotient-im/Quaternion/c85d16312ee42a341c10c55e5bfb9c4c3b97c20b/icons/quaternion/32-apps-quaternion.png -------------------------------------------------------------------------------- /icons/quaternion/48-apps-quaternion.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quotient-im/Quaternion/c85d16312ee42a341c10c55e5bfb9c4c3b97c20b/icons/quaternion/48-apps-quaternion.png -------------------------------------------------------------------------------- /icons/quaternion/64-apps-quaternion.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quotient-im/Quaternion/c85d16312ee42a341c10c55e5bfb9c4c3b97c20b/icons/quaternion/64-apps-quaternion.png -------------------------------------------------------------------------------- /icons/quaternion/sc-apps-quaternion.svgz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quotient-im/Quaternion/c85d16312ee42a341c10c55e5bfb9c4c3b97c20b/icons/quaternion/sc-apps-quaternion.svgz -------------------------------------------------------------------------------- /icons/scrolldown.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | icon_newmessages 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /icons/scrollup.svg: -------------------------------------------------------------------------------- 1 | 2 | 14 | 16 | 17 | 19 | image/svg+xml 20 | 22 | icon_newmessages 23 | 24 | 25 | 26 | icon_newmessages 28 | Created with Sketch. 30 | 32 | 39 | 43 | 47 | 55 | 60 | 61 | 62 | 63 | 64 | -------------------------------------------------------------------------------- /linux/io.github.quotient_im.Quaternion.appdata.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | io.github.quotient_im.Quaternion 4 | CC-BY-4.0 5 | GPL-3.0+ 6 | Quaternion 7 | Qt-based client for Matrix networks 8 | 9 | 10 |

11 | Quaternion is a cross-platform desktop IM client for the Matrix protocol. 12 |

13 |
14 | 15 | kitsune-ral_AT_users.sf.net 16 | 17 | 18 | InstantMessaging 19 | Network 20 | 21 | 22 | io.github.quotient_im.Quaternion.desktop 23 | 24 | 25 | 26 | https://raw.githubusercontent.com/quotient-im/Quaternion/master/Screenshot.png 27 | UI screenshot 28 | 29 | 30 | 31 | https://github.com/quotient-im/Quaternion 32 | https://github.com/quotient-im/Quaternion/issues 33 | https://github.com/quotient-im/Quaternion/blob/master/README.md 34 | 35 | 36 | quaternion 37 | com.github.quaternion.desktop 38 | 39 | 40 | 41 | com.github.quaternion.desktop 42 | 43 | 44 | 45 | 46 | https://github.com/quotient-im/Quaternion/releases/tag/0.0.97.1 47 |

0.0.97.1

48 |
    49 |
  • You can now verify Quaternion sessions for E2EE communication (have to start 50 | verification in Quaternion for now; the other direction is not implemented yet)
  • 51 |
  • Change systray icon when new messages come and Quaternion is not an active window
  • 52 |
  • Event numbers now show up in the scroller area if the shuttle scroller is used
  • 53 |
  • Various smaller UI tweaks and fixes
  • 54 |
55 |
56 |
57 | 58 | https://github.com/quotient-im/Quaternion/releases/tag/0.0.97 59 |

0.0.97

60 |
    61 |
  • Use libQuotient 0.9 underneath
  • 62 |
  • E2EE is always on now
  • 63 |
  • Add nice replies visualisation instead of relying on reply fallbacks
  • 64 |
  • New look for the shuttle scroller and scroll-to buttons
  • 65 |
  • 66 | Clicking on scroll-to-read-marker button now loads history all the way until 67 | the marker - no need to click on the button repeatedly
  • 68 |
  • Display images embedded in rich text messages
  • 69 |
  • New login dialog
  • 70 |
  • Forgetting a room requires a confirmation
  • 71 |
  • Fix: actually log out accounts on exit that are not set to stay logged in
  • 72 |
  • Regression fix: message text again uses the font configured for the timeline
  • 73 |
  • New application id, to comply with Flathub verification requirements
  • 74 |
  • Other code cleanup, performance and visual tweaks and smaller fixes
  • 75 |
76 |
77 |
78 | 79 | https://github.com/quotient-im/Quaternion/releases/tag/0.0.96.1 80 |

Changes since 0.0.96:

81 |
    82 |
  • Fix regressions in attaching files
  • 83 |
  • Allow `mxc` scheme in hyperlinks (MSC2398)
  • 84 |
85 |
86 |
87 | 88 | https://github.com/quotient-im/Quaternion/releases/tag/0.0.96 89 |

0.0.96

90 |

Changes since 0.0.95.1:

91 |
    92 |
  • Qt 6 support
  • 93 |
  • Beta-quality support of end-to-end encryption (E2EE)
  • 94 |
  • Fixes and improvements in the timeline display and text selection
  • 95 |
  • Ctrl-Shift-V to paste content as plain text
  • 96 |
  • Fixed problems around drag-n-dropping/pasting some content
  • 97 |
  • It's now possible to re-dock floating panels from the menu (especially relevant on Wayland)
  • 98 |
  • Using room-specific member avatars in the timeline, not just in the user list
  • 99 |
  • Performance and stability improvements
  • 100 |
101 |
102 |
103 | 104 | 105 | https://github.com/quotient-im/Quaternion/releases/tag/0.0.96-rc1 106 |

0.0.96 RC

107 |

Minor tweaks and fixes

108 |
109 |
110 | 111 | https://github.com/quotient-im/Quaternion/releases/tag/0.0.96-beta4 112 |

0.0.96 beta4

113 |
    114 |
  • Restored workability with Qt 5 (but only until the final release; after that, only Qt 6 will be supported)
  • 115 |
  • Timeline scrolling with mouse wheels and touchpads is (should be) no more sluggish
  • 116 |
  • Pasting HTML from web pages and other applications works in much more cases
  • 117 |
  • More discernible text colours for state events and emotes
  • 118 |
119 |
120 |
121 | 122 | https://github.com/quotient-im/Quaternion/releases/tag/0.0.96-beta3 123 |

0.0.96 beta3

124 |
    125 |
  • Switch to libQuotient 0.8 to make E2EE opt-in at runtime
  • 126 |
  • Complete transition to Qt 6
  • 127 |
  • Other minor tweaks and fixes
  • 128 |
129 |
130 |
131 | 132 | https://github.com/quotient-im/Quaternion/releases/tag/0.0.96-beta1 133 |

134 | Switch to libQuotient 0.7, with its new read receipts/fully read markers 135 | API and experimental support of E2EE; fix a few bugs. More to come 136 | before the final release! 137 |

138 |
139 | 140 | https://github.com/quotient-im/Quaternion/releases/tag/0.0.95.1 141 |

142 | This is mostly about bug fixes, including more accurate scrolling back 143 | in the timeline (to read marker or previously saved position); 144 | actually coloured user names in the timeline; rich text pasting from LibreOffice; 145 | and a few limited HTML injections. Also, the "Scroll to read marker" button 146 | will load more history if the event with the read marker is not loaded yet 147 | (though you'll have to click again to actually scroll after that). 148 |

149 |
150 | 151 | https://github.com/quotient-im/Quaternion/releases/tag/0.0.95 152 |

0.0.95 final release

153 |
    154 |
  • Revamped read marker and "scroll to read marker" button
  • 155 |
  • Initial reactions support
  • 156 |
  • Tint for outgoing messages
  • 157 |
  • Improvements for the shuttle dial
  • 158 |
  • User profile dialog with editable name and avatar
  • 159 |
  • Initial Markdown and rich text entry support (still a bit experimental)
  • 160 |
  • Different colours for different user ids
  • 161 |
162 |
163 |
164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 |
173 | 174 | qt 175 | qtkeychain 176 | quaternion 177 | 178 | The Quotient Project 179 | 180 | intense 181 | intense 182 | intense 183 | 184 |
185 | -------------------------------------------------------------------------------- /linux/io.github.quotient_im.Quaternion.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Name=Quaternion 3 | GenericName=Matrix Client 4 | Comment=IM client for the Matrix protocol 5 | Comment[de]=IM Client für das Matrix Protokoll 6 | Exec=quaternion 7 | Terminal=false 8 | Icon=quaternion 9 | Type=Application 10 | Categories=Network;InstantMessaging; 11 | -------------------------------------------------------------------------------- /quaternion.kdev4: -------------------------------------------------------------------------------- 1 | [Project] 2 | Manager=KDevCMakeManager 3 | Name=quaternion 4 | -------------------------------------------------------------------------------- /quaternion.supp: -------------------------------------------------------------------------------- 1 | { 2 | operator new(unsigned long)[Memcheck:Leak] 3 | Memcheck:Leak 4 | fun:_Znwm 5 | fun:_ZN11QCursorData10initializeEv 6 | fun:_ZN22QGuiApplicationPrivate4initEv 7 | fun:_ZN19QApplicationPrivate4initEv 8 | fun:main 9 | } 10 | -------------------------------------------------------------------------------- /quaternion_win32.rc: -------------------------------------------------------------------------------- 1 | IDI_ICON1 ICON DISCARDABLE "icons/quaternion.ico" --------------------------------------------------------------------------------