Compare commits

...

37 Commits

Author SHA1 Message Date
Joshua Goins
2b1b21dd07 Add spell checking hint to ChatBar again 2023-01-05 21:52:52 -05:00
Gary Wang
85a562d469 Move encrypt room option to Security page 2023-01-05 16:13:59 +00:00
Akseli Lahtinen
f50c62ba12 Use local file path instead of url
If the localFile url is used, it creates a folder called `file:` in the home folder.

So get filepath from url instead :)
2023-01-05 16:09:20 +00:00
Gary Wang
13f05a0995 Move invite option to header of members section
This patch moves the invite option to header of members section. We also check if user can send invitation event and we won't show the invite button if it's not allowed.

This patch also added an toggle button to show the member search bar since it won't needed by the user in most of the cases.
2023-01-05 14:56:29 +00:00
l10n daemon script
1adddcc0d9 GIT_SILENT Sync po/docbooks with svn 2023-01-05 02:21:03 +00:00
Gary Wang
f03cd3f4c6 Fix SSO login button needs to click twice to open login URL
Before this patch, the `ssoUrlChanged` signal might emitted before the URL actually get changed. This patch ensure the signal emitted at the correct place, and also disable the Login button before `LoginHelper` done its work.

To test this change, you can simply enter `@a:mozilla.org` (or whatever the account name is) and attempt to login it by click "continue". Then click the "Login" button and see if the first click can let your browser open the SSO login URL.
2023-01-05 00:42:56 +00:00
James Graham
29a2e4eb99 Room Settings - Permissions
Work to add the ability to set user power levels and modify the power levels required for certain actions.

Updated

![image](/uploads/50bce18f5eb31bb0c3508e03a39e7589/image.png)
2023-01-05 00:36:13 +00:00
Tobias Fella
7137a5808f Fix building room test on windows
Making protected public seems to mess things up
2023-01-04 19:27:49 +00:00
l10n daemon script
fa6f451e11 GIT_SILENT Sync po/docbooks with svn 2023-01-04 02:46:43 +00:00
James Graham
666f247185 Fix Ctrl + F not working
Fix Crtl + F not working by only having the shortcut assigned to a single searchfield. 
- Ctrl + F is now for the roomlist search
- The user search in the room drawer is now ctrl + shift + f
- for the emoji picker and qucik switcher these have the shortcut removed and focus is managed by the popup.

BUG: 462524
2023-01-03 18:06:51 +00:00
Ingo Klöcker
93dd25f954 Add the Microsoft Store URL 2023-01-03 08:13:57 +01:00
l10n daemon script
8e2ba9552f GIT_SILENT Sync po/docbooks with svn 2023-01-03 03:02:20 +00:00
l10n daemon script
1f02c5ea5e SVN_SILENT made messages (.desktop file) - always resolve ours
In case of conflict in i18n, keep the version of the branch "ours"
To resolve a particular conflict, "git checkout --ours path/to/file.desktop"
2023-01-03 02:01:04 +00:00
l10n daemon script
112b26c39d GIT_SILENT made messages (after extraction) 2023-01-03 00:47:28 +00:00
l10n daemon script
5e243ed01b GIT_SILENT Sync po/docbooks with svn 2023-01-02 02:55:40 +00:00
Carl Schwan
706809d12a Make space list smaller
Signed-off-by: Carl Schwan <carl@carlschwan.eu>
2023-01-01 23:38:15 +00:00
l10n daemon script
3fc06c2a74 GIT_SILENT Sync po/docbooks with svn 2023-01-01 02:52:37 +00:00
James Graham
4bc2c42982 Remove stary consol.log statement 2022-12-31 15:25:00 +00:00
James Graham
6651fa4fa3 Improve the sizing of notification count with longer numbers
![image](/uploads/ad2128bbfc0ebfd58e2d58926fceb844/image.png)
2022-12-31 12:54:45 +00:00
l10n daemon script
07d65a0046 GIT_SILENT Sync po/docbooks with svn 2022-12-31 02:17:03 +00:00
Gary Wang
4c9f062a70 Fix from and to is not defined in NetworkProxyPage 2022-12-30 14:08:20 +00:00
James Graham
893ee4a763 Add room upgrade button
Add button to upgrade the room if the user has a high enough power level and the room is not at the highest available version.
2022-12-30 13:51:34 +00:00
Tobias Fella
fa67d174d2 Move room actions out of RoomDrawer header 2022-12-30 13:14:00 +00:00
Gary Wang
bb542521fb Support opening space in room list
While it's not *that* useful since we cannot view all the rooms under the space, it can be useful to view all members inside a Space, and invite people to the Space.
2022-12-30 13:13:39 +00:00
l10n daemon script
85d3cf2d77 GIT_SILENT Sync po/docbooks with svn 2022-12-30 02:30:36 +00:00
Nicolas Fella
ebf4cfb825 Add QCoro to third-party deps 2022-12-29 20:50:52 +01:00
l10n daemon script
d7daa697df GIT_SILENT Sync po/docbooks with svn 2022-12-29 02:20:28 +00:00
James Graham
884484922d Fix Room Initial Position
When the room is changed set the roomlist view to the bottom making sure that it isn't in some random place.

BUG: 456647
2022-12-28 17:36:17 +00:00
James Graham
83ab751d4a Add room id and room version to room settings
Add internal room ID and room version number
2022-12-28 13:06:43 +00:00
l10n daemon script
fcee5bfa92 GIT_SILENT Sync po/docbooks with svn 2022-12-28 02:16:59 +00:00
Gary Wang
eb610ffe81 Allow set room join rule to knock 2022-12-27 13:22:35 +00:00
James Graham
647cc25e57 Improve fileDelegate
Make the buttons in file delegate always align left even in compact layout as this looks better. Also fix margins.

BUG: 463327
2022-12-27 13:19:04 +00:00
Wang Zichong
3912b8e096 Support inviting people from UserDetailDialog 2022-12-27 19:22:38 +08:00
Gary Wang
67f88416f1 Use the correct config option for 'In sidebar'
BUG: 463512
2022-12-27 18:13:33 +08:00
James Graham
8e3398df34 Fix icons only mode
- Fix the headers in icons only mode.
- Allow section collapsing
- Show scrollbar when needed

[2022-12-12_19-06-46.mkv](/uploads/e1633058b2b4a24ef7b6144bf5392b9c/2022-12-12_19-06-46.mkv)

BUG: 462576
2022-12-27 09:57:53 +00:00
l10n daemon script
1a09405829 GIT_SILENT Sync po/docbooks with svn 2022-12-27 02:09:12 +00:00
Gary Wang
304054a4bb Add a compact room list option 2022-12-26 21:31:36 +00:00
74 changed files with 15685 additions and 8979 deletions

View File

@@ -18,6 +18,7 @@ Dependencies:
'third-party/libquotient': '@latest'
'third-party/qtkeychain': '@latest'
'third-party/cmark': '@latest'
'third-party/qcoro': '@latest'
- 'on': ['Windows', 'Linux', 'FreeBSD']
'require':
'frameworks/qqc2-desktop-style': '@stable'

View File

@@ -5,9 +5,7 @@
#include <QSignalSpy>
#include <QTest>
#define protected public // please don't hate me
#include "neochatroom.h"
#undef protected
#include <connection.h>
#include <quotient_common.h>
@@ -15,12 +13,23 @@
using namespace Quotient;
class TestRoom : public NeoChatRoom
{
public:
using NeoChatRoom::NeoChatRoom;
void update(SyncRoomData &&data, bool fromCache = false)
{
Room::updateData(std::move(data), fromCache);
}
};
class NeoChatRoomTest : public QObject {
Q_OBJECT
private:
Connection *connection = nullptr;
NeoChatRoom *room = nullptr;
TestRoom *room = nullptr;
private Q_SLOTS:
void initTestCase();
@@ -31,7 +40,7 @@ private Q_SLOTS:
void NeoChatRoomTest::initTestCase()
{
connection = Connection::makeMockConnection(QStringLiteral("@bob:kde.org"));
room = new NeoChatRoom(connection, QStringLiteral("#myroom:kde.org"), JoinState::Join);
room = new TestRoom(connection, QStringLiteral("#myroom:kde.org"), JoinState::Join);
auto json = QJsonDocument::fromJson(R"EVENT({
"account_data": {
@@ -121,7 +130,7 @@ void NeoChatRoomTest::initTestCase()
}
})EVENT");
SyncRoomData roomData(QStringLiteral("@bob:kde.org"), JoinState::Join, json.object());
room->updateData(std::move(roomData));
room->update(std::move(roomData));
}
void NeoChatRoomTest::subtitleTextTest()

View File

@@ -16,6 +16,7 @@
<name xml:lang="ca-valencia">NeoChat</name>
<name xml:lang="cs">NeoChat</name>
<name xml:lang="de">NeoChat</name>
<name xml:lang="el">NeoChat</name>
<name xml:lang="en-GB">NeoChat</name>
<name xml:lang="es">NeoChat</name>
<name xml:lang="eu">NeoChat</name>
@@ -50,6 +51,7 @@
<summary xml:lang="ca-valencia">Un client per a Matrix, el protocol de comunicacions descentralitzat</summary>
<summary xml:lang="cs">Klient pro decentralizovaný komunikační protokol matrix</summary>
<summary xml:lang="de">Ein Programm für Matrix, das dezentrale Kommunikationsprotokoll</summary>
<summary xml:lang="el">Ένας πελάτης για το Matrix, το αποκεντρωμένο πρωτόκολλο επικοινωνίας</summary>
<summary xml:lang="en-GB">A client for matrix, the decentralised communication protocol</summary>
<summary xml:lang="es">Un cliente para Matrix, el protocolo de comunicaciones descentralizado</summary>
<summary xml:lang="eu">Matrix, deszentralizatutako komunikazio protokolorako bezero bat</summary>
@@ -83,6 +85,7 @@
<p xml:lang="ca">El NeoChat és un client de Matrix. Permet enviar missatges de text, fitxers de vídeo i d'àudio a la família, col·legues i amics usant el protocol Matrix.</p>
<p xml:lang="ca-valencia">NeoChat és un client de Matrix. Permet enviar missatges de text, fitxers de vídeo i d'àudio a la família, col·legues i amics utilitzant el protocol Matrix.</p>
<p xml:lang="de">NeoChat ist ein Matrix-Client. Er ermöglicht Ihnen das Senden von Textnachrichten, Videos und Audiodateien an Ihre Familie, Kollegen und Freunde unter Verwendung des Matrix-Protokolls.</p>
<p xml:lang="el">Το NeoChat είναι μια εφαρμογή του Matrix. Σας επιτρέπει να στέλνετε μηνύματα κειμένου, βίντεο και ήχο στην οικογένειά σας, σε συναδέλφους και φίλους με το πρωτόκολλο Matrix.</p>
<p xml:lang="en-GB">NeoChat is a Matrix client. It allows you to send text messages, videos and audio files to your family, colleagues and friends using the Matrix protocol.</p>
<p xml:lang="es">NeoChat es un cliente para Matrix. Le permite enviar mensajes de texto, vídeos y archivos de sonido a su familia, compañeros de trabajo y amigos usando el protocolo Matrix.</p>
<p xml:lang="eu">NeoChat Matrix bezero bat da. Familiari, lankideei eta lagunei testu-mezuak, bideoak eta audio-fitxategiak bidaltzeko aukera ematen du, Matrix protokoloa erabiliz.</p>
@@ -113,6 +116,7 @@
<p xml:lang="ca">Matrix és un protocol de comunicacions descentralitzat, que retorna el control a l'usuari. Actualment el NeoChat implementa una gran part del protocol amb l'excepció dels xats encriptats i els xats de vídeo.</p>
<p xml:lang="ca-valencia">Matrix és un protocol de comunicacions descentralitzat, que retorna el control a l'usuari. Actualment NeoChat implementa una gran part del protocol amb l'excepció dels xats encriptats i els xats de vídeo.</p>
<p xml:lang="de">Matrix ist ein dezentralisiertes Kommunikationsprotokoll, das dem Benutzer wieder die Kontrolle zurückgibt. Derzeit implementiert NeoChat einen großen Teil des Protokolls mit der Ausnahme von verschlüsselten Chats und Video-Chat.</p>
<p xml:lang="el">Το Matrix είναι ένα αποκεντρωμένο πρωτόκολλο επικοινωνίας, δίνοντας πίσω στον χρήστη τον έλεγχο. Προς το παρόν το NeoChat υλοποιεί ένα μεγάλο μέρος του πρωτοκόλλου με εξαίρεση τις κρυπτογραφημένες συνομιλίες και τη συνομιλία με βίντεο.</p>
<p xml:lang="en-GB">Matrix is a decentralised communication protocol, putting the user back in control. Currently NeoChat implements large part of the protocol with the exception of encrypted chats and video chat.</p>
<p xml:lang="es">Matrix es un protocolo de comunicaciones descentralizado, que devuelve el control al usuario. En la actualidad, NeoChat implementa gran parte del protocolo con la excepción de chats cifrados y chats de vídeo.</p>
<p xml:lang="eu">Matrix komunikazio-protokolo deszentralizatu bat da, erabiltzaileari kontrola itzultzen diona. Gaur egun, NeoChat-ek protokoloaren zati handi bat inplementatzen du, berriketa zifratuak eta bideo berriketak izan ezik.</p>
@@ -142,7 +146,8 @@
<p xml:lang="az">Vahid istifadəçi interfeysi ilə təmin olunan NeoChat, həm mobil telefonda həm də kompyuterlərdə işləyir.</p>
<p xml:lang="ca">El NeoChat funciona en els mòbils i a l'escriptori, proporcionant una experiència d'usuari coherent.</p>
<p xml:lang="ca-valencia">NeoChat funciona en els mòbils i en l'escriptori, proporcionant una experiència d'usuari coherent.</p>
<p xml:lang="de">NeoChat funktioniert sowohl auf dem Mobiltelefon als auch auf dem Arbeitsfläche und bietet ein einheitliches Benutzererlebnis. </p>
<p xml:lang="de">NeoChat funktioniert sowohl auf Mobilgeräten als auch auf dem PC und bietet ein einheitliches Benutzererlebnis.</p>
<p xml:lang="el">Το NeoChat λειτουργεί και στα κινητά και στους υπολογιστές γραφείου παρέχοντας μια αδιάλειπτη εμπειρία χρήσης.</p>
<p xml:lang="en-GB">NeoChat works both on mobile and desktop while providing a consistent user experience.</p>
<p xml:lang="es">NeoChat funciona en móviles y en el escritorio a la vez que proporciona una experiencia de usuario consistente.</p>
<p xml:lang="eu">NeoChat mugikorretan eta mahaigainean dabil, erabiltzaile esperientzia koherentea eskainiz.</p>
@@ -180,6 +185,7 @@
<developer_name xml:lang="ca-valencia">La comunitat KDE</developer_name>
<developer_name xml:lang="cs">Komunita KDE</developer_name>
<developer_name xml:lang="de">Die KDE-Gemeinschaft</developer_name>
<developer_name xml:lang="el">Η Κοινότητα του KDE</developer_name>
<developer_name xml:lang="en-GB">The KDE Community</developer_name>
<developer_name xml:lang="es">La comunidad KDE</developer_name>
<developer_name xml:lang="eu">KDE komunitatea</developer_name>
@@ -210,6 +216,7 @@
<project_license>GPL-3.0</project_license>
<custom>
<value key="KDE::matrix">#neochat:kde.org</value>
<value key="KDE::windows_store">https://www.microsoft.com/store/apps/9PNXWVNRC29H</value>
</custom>
<launchable type="desktop-id">org.kde.neochat.desktop</launchable>
<screenshots>

View File

@@ -9,6 +9,7 @@ Name[ca]=NeoChat
Name[ca@valencia]=NeoChat
Name[cs]=NeoChat
Name[de]=NeoChat
Name[el]=NeoChat
Name[en_GB]=NeoChat
Name[es]=NeoChat
Name[eu]=NeoChat
@@ -45,6 +46,7 @@ GenericName[ca]=Client de Matrix
GenericName[ca@valencia]=Client de Matrix
GenericName[cs]=Klient protokolu Matrix
GenericName[de]=Matrix-Programm
GenericName[el]=Εφαρμογή του Matrix
GenericName[en_GB]=Matrix Client
GenericName[es]=Cliente para Matrix
GenericName[eu]=Matrix bezeroa
@@ -80,6 +82,7 @@ Comment[az]=Matrix protokolu üçün müştəri
Comment[ca]=Client per al protocol Matrix
Comment[ca@valencia]=Client per al protocol Matrix
Comment[de]=Programm für das Matrix-Protokoll
Comment[el]=Πελάτης για το πρωτόκολλο Matrix
Comment[en_GB]=Client for the Matrix protocol
Comment[es]=Cliente para el protocolo Matrix
Comment[eu]=Matrix protokolorako bezeroa

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

3383
po/el/neochat.po Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,122 @@
<?xml version="1.0" ?>
<!DOCTYPE refentry PUBLIC "-//KDE//DTD DocBook XML V4.5-Based Variant V1.1//EN" "dtd/kdedbx45.dtd" [
<!ENTITY % Italian "INCLUDE">
]>
<!--
SPDX-FileCopyrightText: 2022 Carl Schwan <carl@carlschwan.eu>
SPDX-License-Identifier: CC-BY-SA-4.0
-->
<refentry lang="&language;">
<refentryinfo>
<title
>Manuale utente di NeoChat</title>
<author
><firstname
>Carl</firstname
><surname
>Schwan</surname
> <contrib
>Pagina man di NeoChat.</contrib
> <email
>carl@carlschwan.eu</email
></author>
<date
>1/11/2022</date>
<releaseinfo
>22.09</releaseinfo>
<productname
>NeoChat</productname>
</refentryinfo>
<refmeta>
<refentrytitle>
<command
>neochat</command>
</refentrytitle>
<manvolnum
>1</manvolnum>
</refmeta>
<refnamediv>
<refname
>neochat</refname>
<refpurpose
>Client per l'interazione con il protocollo di messaggistica matrix</refpurpose>
</refnamediv>
<!-- body begins here -->
<refsynopsisdiv id='synopsis'>
<cmdsynopsis
><command
>neochat</command
> <arg choice="opt"
><replaceable
>URI</replaceable
></arg
> </cmdsynopsis>
</refsynopsisdiv>
<refsect1 id="description">
<title
>Descrizione</title>
<para
><command
>neochat</command
> è un'applicazione di chat per il protocollo Matrix che funziona sia su desktop che su dispositivi mobili. </para>
</refsect1>
<refsect1 id="options"
><title
>Opzioni</title>
<variablelist>
<varlistentry>
<term
><option
>URI</option
></term>
<listitem>
<para
>L'URI matrix per un utente o una stanza. ad esempio matrix:u/utente:esempio.org e matrix:r/stanza:esempio.org. Questo farà in modo che NeoChat provi ad aprire la stanza o la conversazione specificata. </para>
</listitem>
</varlistentry>
</variablelist>
</refsect1>
<refsect1 id="bug">
<title
>Segnalazione bug</title>
<para
>Puoi segnalare bug e richiedere funzionalità su <ulink url="https://bugs.kde.org/enter_bug.cgi?product=NeoChat&amp;component=General"
>https://bugs.kde.org/enter_bug.cgi? product=NeoChat&amp;component=General</ulink
></para>
</refsect1>
<refsect1>
<title
>Vedi anche</title>
<simplelist>
<member
>Un elenco di domande frequenti su Matrix <ulink url="https://matrix.org/faq/"
>https://matrix.org/faq/</ulink
> </member>
<member
>kf5options(7)</member>
<member
>qt5options(7)</member>
</simplelist>
</refsect1>
<refsect1 id="copyright"
><title
>Copyright</title>
<para
>Copyright &copy; 2020-2022 Tobias Fella </para>
<para
>Copyright &copy; 2020-2022 Carl Schwan </para>
<para
>Licenza: GNU General Public Version 3 o successiva &lt;<ulink url="https://www.gnu.org/licenses/gpl-3.0.html"
>https://www.gnu.org/licenses/gpl-3.0.html</ulink
>&gt;</para>
</refsect1>
</refentry>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -20,6 +20,7 @@ add_library(neochat STATIC
neochatroom.cpp
neochatuser.cpp
userlistmodel.cpp
userfiltermodel.cpp
publicroomlistmodel.cpp
userdirectorylistmodel.cpp
keywordnotificationrulemodel.cpp
@@ -158,7 +159,7 @@ if(ANDROID)
"zoom-out"
"image-rotate-left-symbolic"
"image-rotate-right-symbolic"
"channel-insecure-symbolic"
"channel-secure-symbolic"
"download"
"smiley"
"tools-check-spelling"
@@ -169,6 +170,7 @@ if(ANDROID)
"favorite"
"window-new"
"globe"
"visibility"
)
else()
target_link_libraries(neochat PUBLIC Qt::Widgets KF5::KIOWidgets)

View File

@@ -52,8 +52,8 @@ QString Clipboard::saveImage(QString localPath) const
}
QDir dir;
if (!dir.exists(localPath)) {
dir.mkpath(localPath);
if (!dir.exists(QFileInfo(url.fileName()).absoluteFilePath())) {
dir.mkpath(QFileInfo(url.fileName()).absoluteFilePath());
}
image.save(url.toLocalFile());

View File

@@ -181,8 +181,8 @@ void Login::loginWithSso()
connectSingleShot(m_connection, &Connection::loginFlowsChanged, this, [this]() {
SsoSession *session = m_connection->prepareForSso(m_deviceName);
m_ssoUrl = session->ssoUrl();
Q_EMIT ssoUrlChanged();
});
Q_EMIT ssoUrlChanged();
}
bool Login::testing() const

View File

@@ -72,6 +72,7 @@
#include "spacehierarchycache.h"
#include "urlhelper.h"
#include "userdirectorylistmodel.h"
#include "userfiltermodel.h"
#include "userlistmodel.h"
#include "webshortcutmodel.h"
#include "windowcontroller.h"
@@ -220,6 +221,7 @@ int main(int argc, char *argv[])
qmlRegisterType<MessageEventModel>("org.kde.neochat", 1, 0, "MessageEventModel");
qmlRegisterType<CollapseStateProxyModel>("org.kde.neochat", 1, 0, "CollapseStateProxyModel");
qmlRegisterType<MessageFilterModel>("org.kde.neochat", 1, 0, "MessageFilterModel");
qmlRegisterType<UserFilterModel>("org.kde.neochat", 1, 0, "UserFilterModel");
qmlRegisterType<PublicRoomListModel>("org.kde.neochat", 1, 0, "PublicRoomListModel");
qmlRegisterType<UserDirectoryListModel>("org.kde.neochat", 1, 0, "UserDirectoryListModel");
qmlRegisterType<ServerListModel>("org.kde.neochat", 1, 0, "ServerListModel");

View File

@@ -7,6 +7,7 @@ Name[ca]=NeoChat
Name[ca@valencia]=NeoChat
Name[cs]=NeoChat
Name[de]=NeoChat
Name[el]=NeoChat
Name[en_GB]=NeoChat
Name[es]=NeoChat
Name[eu]=NeoChat
@@ -44,6 +45,7 @@ Comment[ca]=Un client per a Matrix, el protocol de comunicacions descentralitzat
Comment[ca@valencia]=Un client per a Matrix, el protocol de comunicacions descentralitzat
Comment[cs]=Klient pro decentralizovaný komunikační protokol matrix
Comment[de]=Ein Programm für Matrix, das dezentrale Kommunikationsprotokoll
Comment[el]=Ένας πελάτης για το Matrix, το αποκεντρωμένο πρωτόκολλο επικοινωνίας
Comment[en_GB]=A client for matrix, the decentralised communication protocol
Comment[es]=Un cliente para Matrix, el protocolo de comunicaciones descentralizado
Comment[eu]=Matrix, deszentralizatutako komunikazio protokolorako, bezero bat
@@ -82,6 +84,7 @@ Name[ca]=Missatge nou
Name[ca@valencia]=Missatge nou
Name[cs]=Nová zpráva
Name[de]=Neue Nachricht
Name[el]=Νέο μήνυμα
Name[en_GB]=New message
Name[es]=Nuevo mensaje
Name[eu]=Mezu berria
@@ -116,7 +119,8 @@ Comment[ar]=توجد رسالة جديدة
Comment[az]=Yeni ismarıc var
Comment[ca]=Hi ha un missatge nou
Comment[ca@valencia]=Hi ha un missatge nou
Comment[de]=Es ist eine neue Nachricht vorhanden
Comment[de]=Es gibt eine neue Nachricht
Comment[el]=Υπάρχει ένα νέο μήνυμα
Comment[en_GB]=There is a new message
Comment[es]=Hay un mensaje nuevo
Comment[eu]=Mezu berri bat dago
@@ -156,6 +160,7 @@ Name[ca]=Invitació nova
Name[ca@valencia]=Invitació nova
Name[cs]=Nová pozvánka
Name[de]=Neue Einladung
Name[el]=Νέα πρόσκληση
Name[en_GB]=New Invitation
Name[es]=Nueva invitación
Name[eu]=Gonbidapen berria
@@ -186,6 +191,7 @@ Comment[ca]=Hi ha una invitació nova a una sala
Comment[ca@valencia]=Hi ha una invitació nova a una sala
Comment[cs]=Máte novou pozvánku do místnosti
Comment[de]=Es gibt eine neue Einladung zu einem Raum
Comment[el]=Υπάρχει μια νέα πρόσκληση σε μια αίθουσα
Comment[en_GB]=There is a new invitation to a room
Comment[es]=Hay una nueva invitación a una sala
Comment[eu]=Gela baterako gonbidapen berri bat dago

View File

@@ -72,7 +72,11 @@
<default>true</default>
</entry>
<entry name="CompactLayout" type="bool">
<label>Use a compact layout</label>
<label>Use a compact chat layout</label>
<default>false</default>
</entry>
<entry name="CompactRoomList" type="bool">
<label>Use a compact room list layout</label>
<default>false</default>
</entry>
<entry name="ShowRename" type="bool">

View File

@@ -31,6 +31,9 @@
#include <events/roompowerlevelsevent.h>
#include <events/simplestateevents.h>
#include <jobs/downloadfilejob.h>
#ifndef QUOTIENT_07
#include <joinstate.h>
#endif
#include <qt_connection_util.h>
#include "controller.h"
@@ -94,6 +97,7 @@ NeoChatRoom::NeoChatRoom(Connection *connection, QString roomId, JoinState joinS
connect(this, &Room::changed, this, [this] {
Q_EMIT canEncryptRoomChanged();
});
connect(connection, &Connection::capabilitiesLoaded, this, &NeoChatRoom::maxRoomVersionChanged);
}
void NeoChatRoom::uploadFile(const QUrl &url, const QString &body)
@@ -600,8 +604,10 @@ QString NeoChatRoom::eventToString(const RoomEvent &evt, Qt::TextFormat format,
} else {
return i18n("self-banned from the room");
}
case MembershipType::Knock:
return i18n("requested an invite");
case MembershipType::Knock: {
QString reason(e.contentJson()["reason"_ls].toString().toHtmlEscaped());
return reason.isEmpty() ? i18n("requested an invite") : i18n("requested an invite with reason: %1", reason);
}
default:;
}
return i18n("made something unknown");
@@ -940,6 +946,305 @@ void NeoChatRoom::setHistoryVisibility(const QString &historyVisibilityRule)
// Not emitting historyVisibilityChanged() here, since that would override the change in the UI with the *current* value, which is not the *new* value.
}
void NeoChatRoom::setUserPowerLevel(const QString &userID, const int &powerLevel)
{
if (joinedCount() <= 1) {
qWarning() << "Cannot modify the power level of the only user";
return;
}
if (!canSendState("m.room.power_levels")) {
qWarning() << "Power level too low to set user power levels";
return;
}
#ifdef QUOTIENT_07
if (!isMember(userID)) {
#else
if (memberJoinState(user(userID)) == JoinState::Join) {
#endif
qWarning() << "User is not a member of this room so power level cannot be set";
return;
}
int clampPowerLevel = std::clamp(powerLevel, 0, 100);
#ifdef QUOTIENT_07
auto powerLevelContent = currentState().get("m.room.power_levels")->contentJson();
#else
auto powerLevelContent = getCurrentState<RoomPowerLevelsEvent>()->contentJson();
#endif
auto powerLevelUserOverrides = powerLevelContent["users"].toObject();
if (powerLevelUserOverrides[userID] != clampPowerLevel) {
powerLevelUserOverrides[userID] = clampPowerLevel;
powerLevelContent["users"] = powerLevelUserOverrides;
#ifdef QUOTIENT_07
setState("m.room.power_levels", "", powerLevelContent);
#else
setState<RoomPowerLevelsEvent>(QJsonObject{{"type", "m.room.power_levels"}, {"state_key", ""}, {"content", powerLevelContent}});
#endif
}
}
int NeoChatRoom::powerLevel(const QString &eventName, const bool &isStateEvent) const
{
#ifdef QUOTIENT_07
const auto powerLevelEvent = currentState().get<RoomPowerLevelsEvent>();
#else
const auto powerLevelEvent = getCurrentState<RoomPowerLevelsEvent>();
#endif
if (eventName == "ban") {
return powerLevelEvent->ban();
} else if (eventName == "kick") {
return powerLevelEvent->kick();
} else if (eventName == "invite") {
return powerLevelEvent->invite();
} else if (eventName == "redact") {
return powerLevelEvent->redact();
} else if (eventName == "users_default") {
return powerLevelEvent->usersDefault();
} else if (eventName == "state_default") {
return powerLevelEvent->stateDefault();
} else if (eventName == "events_default") {
return powerLevelEvent->eventsDefault();
} else if (isStateEvent) {
return powerLevelEvent->powerLevelForState(eventName);
} else {
return powerLevelEvent->powerLevelForEvent(eventName);
}
}
void NeoChatRoom::setPowerLevel(const QString &eventName, const int &newPowerLevel, const bool &isStateEvent)
{
#ifdef QUOTIENT_07
auto powerLevelContent = currentState().get("m.room.power_levels")->contentJson();
#else
auto powerLevelContent = getCurrentState<RoomPowerLevelsEvent>()->contentJson();
#endif
int clampPowerLevel = std::clamp(newPowerLevel, 0, 100);
int powerLevel = 0;
if (powerLevelContent.contains(eventName)) {
powerLevel = powerLevelContent[eventName].toInt();
if (powerLevel != clampPowerLevel) {
powerLevelContent[eventName] = clampPowerLevel;
}
} else {
auto eventPowerLevels = powerLevelContent["events"].toObject();
if (eventPowerLevels.contains(eventName)) {
powerLevel = eventPowerLevels[eventName].toInt();
} else {
if (isStateEvent) {
powerLevel = powerLevelContent["state_default"].toInt();
} else {
powerLevel = powerLevelContent["events_default"].toInt();
}
}
if (powerLevel != clampPowerLevel) {
eventPowerLevels[eventName] = clampPowerLevel;
powerLevelContent["events"] = eventPowerLevels;
}
}
#ifdef QUOTIENT_07
setState("m.room.power_levels", "", powerLevelContent);
#else
setState<RoomPowerLevelsEvent>(QJsonObject{{"type", "m.room.power_levels"}, {"state_key", ""}, {"content", powerLevelContent}});
#endif
}
int NeoChatRoom::defaultUserPowerLevel() const
{
return powerLevel("users_default");
}
void NeoChatRoom::setDefaultUserPowerLevel(const int &newPowerLevel)
{
setPowerLevel("users_default", newPowerLevel);
}
int NeoChatRoom::invitePowerLevel() const
{
return powerLevel("invite");
}
void NeoChatRoom::setInvitePowerLevel(const int &newPowerLevel)
{
setPowerLevel("invite", newPowerLevel);
}
int NeoChatRoom::kickPowerLevel() const
{
return powerLevel("kick");
}
void NeoChatRoom::setKickPowerLevel(const int &newPowerLevel)
{
setPowerLevel("kick", newPowerLevel);
}
int NeoChatRoom::banPowerLevel() const
{
return powerLevel("ban");
}
void NeoChatRoom::setBanPowerLevel(const int &newPowerLevel)
{
setPowerLevel("ban", newPowerLevel);
}
int NeoChatRoom::redactPowerLevel() const
{
return powerLevel("redact");
}
void NeoChatRoom::setRedactPowerLevel(const int &newPowerLevel)
{
setPowerLevel("redact", newPowerLevel);
}
int NeoChatRoom::statePowerLevel() const
{
return powerLevel("state_default");
}
void NeoChatRoom::setStatePowerLevel(const int &newPowerLevel)
{
setPowerLevel("state_default", newPowerLevel);
}
int NeoChatRoom::defaultEventPowerLevel() const
{
return powerLevel("events_default");
}
void NeoChatRoom::setDefaultEventPowerLevel(const int &newPowerLevel)
{
setPowerLevel("events_default", newPowerLevel);
}
int NeoChatRoom::powerLevelPowerLevel() const
{
return powerLevel("m.room.power_levels", true);
}
void NeoChatRoom::setPowerLevelPowerLevel(const int &newPowerLevel)
{
setPowerLevel("m.room.power_levels", newPowerLevel, true);
}
int NeoChatRoom::namePowerLevel() const
{
return powerLevel("m.room.name", true);
}
void NeoChatRoom::setNamePowerLevel(const int &newPowerLevel)
{
setPowerLevel("m.room.name", newPowerLevel, true);
}
int NeoChatRoom::avatarPowerLevel() const
{
return powerLevel("m.room.avatar", true);
}
void NeoChatRoom::setAvatarPowerLevel(const int &newPowerLevel)
{
setPowerLevel("m.room.avatar", newPowerLevel, true);
}
int NeoChatRoom::canonicalAliasPowerLevel() const
{
return powerLevel("m.room.canonical_alias", true);
}
void NeoChatRoom::setCanonicalAliasPowerLevel(const int &newPowerLevel)
{
setPowerLevel("m.room.canonical_alias", newPowerLevel, true);
}
int NeoChatRoom::topicPowerLevel() const
{
return powerLevel("m.room.topic", true);
}
void NeoChatRoom::setTopicPowerLevel(const int &newPowerLevel)
{
setPowerLevel("m.room.topic", newPowerLevel, true);
}
int NeoChatRoom::encryptionPowerLevel() const
{
return powerLevel("m.room.encryption", true);
}
void NeoChatRoom::setEncryptionPowerLevel(const int &newPowerLevel)
{
setPowerLevel("m.room.encryption", newPowerLevel, true);
}
int NeoChatRoom::historyVisibilityPowerLevel() const
{
return powerLevel("m.room.history_visibility", true);
}
void NeoChatRoom::setHistoryVisibilityPowerLevel(const int &newPowerLevel)
{
setPowerLevel("m.room.history_visibility", newPowerLevel, true);
}
int NeoChatRoom::pinnedEventsPowerLevel() const
{
return powerLevel("m.room.pinned_events", true);
}
void NeoChatRoom::setPinnedEventsPowerLevel(const int &newPowerLevel)
{
setPowerLevel("m.room.pinned_events", newPowerLevel, true);
}
int NeoChatRoom::tombstonePowerLevel() const
{
return powerLevel("m.room.tombstone", true);
}
void NeoChatRoom::setTombstonePowerLevel(const int &newPowerLevel)
{
setPowerLevel("m.room.tombstone", newPowerLevel, true);
}
int NeoChatRoom::serverAclPowerLevel() const
{
return powerLevel("m.room.server_acl", true);
}
void NeoChatRoom::setServerAclPowerLevel(const int &newPowerLevel)
{
setPowerLevel("m.room.server_acl", newPowerLevel, true);
}
int NeoChatRoom::spaceChildPowerLevel() const
{
return powerLevel("m.space.child", true);
}
void NeoChatRoom::setSpaceChildPowerLevel(const int &newPowerLevel)
{
setPowerLevel("m.space.child", newPowerLevel, true);
}
int NeoChatRoom::spaceParentPowerLevel() const
{
return powerLevel("m.space.parent", true);
}
void NeoChatRoom::setSpaceParentPowerLevel(const int &newPowerLevel)
{
setPowerLevel("m.space.parent", newPowerLevel, true);
}
QCoro::Task<void> NeoChatRoom::doDeleteMessagesByUser(const QString &user, QString reason)
{
QStringList events;
@@ -1368,3 +1673,14 @@ void NeoChatRoom::setCanonicalAlias(const QString &newAlias)
}
});
}
int NeoChatRoom::maxRoomVersion() const
{
int maxVersion = 0;
for (auto roomVersion : connection()->availableRoomVersions()) {
if (roomVersion.id.toInt() > maxVersion) {
maxVersion = roomVersion.id.toInt();
}
}
return maxVersion;
}

View File

@@ -47,9 +47,32 @@ class NeoChatRoom : public Quotient::Room
Q_PROPERTY(QString avatarMediaId READ avatarMediaId NOTIFY avatarChanged STORED false)
Q_PROPERTY(bool readMarkerLoaded READ readMarkerLoaded NOTIFY readMarkerLoadedChanged)
Q_PROPERTY(QDateTime lastActiveTime READ lastActiveTime NOTIFY lastActiveTimeChanged)
Q_PROPERTY(bool isSpace READ isSpace CONSTANT)
Q_PROPERTY(bool isInvite READ isInvite NOTIFY isInviteChanged)
Q_PROPERTY(QString joinRule READ joinRule WRITE setJoinRule NOTIFY joinRuleChanged)
Q_PROPERTY(QString historyVisibility READ historyVisibility WRITE setHistoryVisibility NOTIFY historyVisibilityChanged)
// Properties for the various permission levels for the room
Q_PROPERTY(int defaultUserPowerLevel READ defaultUserPowerLevel WRITE setDefaultUserPowerLevel NOTIFY defaultUserPowerLevelChanged)
Q_PROPERTY(int invitePowerLevel READ invitePowerLevel WRITE setInvitePowerLevel NOTIFY invitePowerLevelChanged)
Q_PROPERTY(int kickPowerLevel READ kickPowerLevel WRITE setKickPowerLevel NOTIFY kickPowerLevelChanged)
Q_PROPERTY(int banPowerLevel READ banPowerLevel WRITE setBanPowerLevel NOTIFY banPowerLevelChanged)
Q_PROPERTY(int redactPowerLevel READ redactPowerLevel WRITE setRedactPowerLevel NOTIFY redactPowerLevelChanged)
Q_PROPERTY(int statePowerLevel READ statePowerLevel WRITE setStatePowerLevel NOTIFY statePowerLevelChanged)
Q_PROPERTY(int defaultEventPowerLevel READ defaultEventPowerLevel WRITE setDefaultEventPowerLevel NOTIFY defaultEventPowerLevelChanged)
Q_PROPERTY(int powerLevelPowerLevel READ powerLevelPowerLevel WRITE setPowerLevelPowerLevel NOTIFY powerLevelPowerLevelChanged)
Q_PROPERTY(int namePowerLevel READ namePowerLevel WRITE setNamePowerLevel NOTIFY namePowerLevelChanged)
Q_PROPERTY(int avatarPowerLevel READ avatarPowerLevel WRITE setAvatarPowerLevel NOTIFY avatarPowerLevelChanged)
Q_PROPERTY(int canonicalAliasPowerLevel READ canonicalAliasPowerLevel WRITE setCanonicalAliasPowerLevel NOTIFY canonicalAliasPowerLevelChanged)
Q_PROPERTY(int topicPowerLevel READ topicPowerLevel WRITE setTopicPowerLevel NOTIFY topicPowerLevelChanged)
Q_PROPERTY(int encryptionPowerLevel READ encryptionPowerLevel WRITE setEncryptionPowerLevel NOTIFY encryptionPowerLevelChanged)
Q_PROPERTY(int historyVisibilityPowerLevel READ historyVisibilityPowerLevel WRITE setHistoryVisibilityPowerLevel NOTIFY historyVisibilityPowerLevelChanged)
Q_PROPERTY(int pinnedEventsPowerLevel READ pinnedEventsPowerLevel WRITE setPinnedEventsPowerLevel NOTIFY pinnedEventsPowerLevelChanged)
Q_PROPERTY(int tombstonePowerLevel READ tombstonePowerLevel WRITE setTombstonePowerLevel NOTIFY tombstonePowerLevelChanged)
Q_PROPERTY(int serverAclPowerLevel READ serverAclPowerLevel WRITE setServerAclPowerLevel NOTIFY serverAclPowerLevelChanged)
Q_PROPERTY(int spaceChildPowerLevel READ spaceChildPowerLevel WRITE setSpaceChildPowerLevel NOTIFY spaceChildPowerLevelChanged)
Q_PROPERTY(int spaceParentPowerLevel READ spaceParentPowerLevel WRITE setSpaceParentPowerLevel NOTIFY spaceParentPowerLevelChanged)
Q_PROPERTY(QString htmlSafeDisplayName READ htmlSafeDisplayName NOTIFY displayNameChanged)
Q_PROPERTY(PushNotificationState::State pushNotificationState MEMBER m_currentPushNotificationState WRITE setPushNotificationState NOTIFY
pushNotificationStateChanged)
@@ -65,6 +88,13 @@ class NeoChatRoom : public Quotient::Room
Q_PROPERTY(QString chatBoxAttachmentPath READ chatBoxAttachmentPath WRITE setChatBoxAttachmentPath NOTIFY chatBoxAttachmentPathChanged)
Q_PROPERTY(bool canEncryptRoom READ canEncryptRoom NOTIFY canEncryptRoomChanged)
/**
* @brief Get the maximum room version that the server supports.
*
* Only returns main integer room versions (i.e. no msc room versions).
*/
Q_PROPERTY(int maxRoomVersion READ maxRoomVersion NOTIFY maxRoomVersionChanged)
public:
enum MessageType {
Positive,
@@ -117,6 +147,68 @@ public:
[[nodiscard]] QString historyVisibility() const;
void setHistoryVisibility(const QString &historyVisibilityRule);
Q_INVOKABLE void setUserPowerLevel(const QString &userID, const int &powerLevel);
[[nodiscard]] int powerLevel(const QString &eventName, const bool &isStateEvent = false) const;
void setPowerLevel(const QString &eventName, const int &newPowerLevel, const bool &isStateEvent = false);
[[nodiscard]] int defaultUserPowerLevel() const;
void setDefaultUserPowerLevel(const int &newPowerLevel);
[[nodiscard]] int invitePowerLevel() const;
void setInvitePowerLevel(const int &newPowerLevel);
[[nodiscard]] int kickPowerLevel() const;
void setKickPowerLevel(const int &newPowerLevel);
[[nodiscard]] int banPowerLevel() const;
void setBanPowerLevel(const int &newPowerLevel);
[[nodiscard]] int redactPowerLevel() const;
void setRedactPowerLevel(const int &newPowerLevel);
[[nodiscard]] int statePowerLevel() const;
void setStatePowerLevel(const int &newPowerLevel);
[[nodiscard]] int defaultEventPowerLevel() const;
void setDefaultEventPowerLevel(const int &newPowerLevel);
[[nodiscard]] int powerLevelPowerLevel() const;
void setPowerLevelPowerLevel(const int &newPowerLevel);
[[nodiscard]] int namePowerLevel() const;
void setNamePowerLevel(const int &newPowerLevel);
[[nodiscard]] int avatarPowerLevel() const;
void setAvatarPowerLevel(const int &newPowerLevel);
[[nodiscard]] int canonicalAliasPowerLevel() const;
void setCanonicalAliasPowerLevel(const int &newPowerLevel);
[[nodiscard]] int topicPowerLevel() const;
void setTopicPowerLevel(const int &newPowerLevel);
[[nodiscard]] int encryptionPowerLevel() const;
void setEncryptionPowerLevel(const int &newPowerLevel);
[[nodiscard]] int historyVisibilityPowerLevel() const;
void setHistoryVisibilityPowerLevel(const int &newPowerLevel);
[[nodiscard]] int pinnedEventsPowerLevel() const;
void setPinnedEventsPowerLevel(const int &newPowerLevel);
[[nodiscard]] int tombstonePowerLevel() const;
void setTombstonePowerLevel(const int &newPowerLevel);
[[nodiscard]] int serverAclPowerLevel() const;
void setServerAclPowerLevel(const int &newPowerLevel);
[[nodiscard]] int spaceChildPowerLevel() const;
void setSpaceChildPowerLevel(const int &newPowerLevel);
[[nodiscard]] int spaceParentPowerLevel() const;
void setSpaceParentPowerLevel(const int &newPowerLevel);
[[nodiscard]] bool hasFileUploading() const
{
return m_hasFileUploading;
@@ -223,6 +315,8 @@ public:
}
#endif
int maxRoomVersion() const;
private:
QSet<const Quotient::RoomEvent *> highlights;
@@ -274,6 +368,26 @@ Q_SIGNALS:
void canEncryptRoomChanged();
void joinRuleChanged();
void historyVisibilityChanged();
void maxRoomVersionChanged();
void defaultUserPowerLevelChanged();
void invitePowerLevelChanged();
void kickPowerLevelChanged();
void banPowerLevelChanged();
void redactPowerLevelChanged();
void statePowerLevelChanged();
void defaultEventPowerLevelChanged();
void powerLevelPowerLevelChanged();
void namePowerLevelChanged();
void avatarPowerLevelChanged();
void canonicalAliasPowerLevelChanged();
void topicPowerLevelChanged();
void encryptionPowerLevelChanged();
void historyVisibilityPowerLevelChanged();
void pinnedEventsPowerLevelChanged();
void tombstonePowerLevelChanged();
void serverAclPowerLevelChanged();
void spaceChildPowerLevelChanged();
void spaceParentPowerLevelChanged();
public Q_SLOTS:
void uploadFile(const QUrl &url, const QString &body = QString());

View File

@@ -8,6 +8,7 @@ Name[ca]=NeoChat
Name[ca@valencia]=NeoChat
Name[cs]=NeoChat
Name[de]=NeoChat
Name[el]=NeoChat
Name[en_GB]=NeoChat
Name[es]=NeoChat
Name[eu]=NeoChat
@@ -42,6 +43,8 @@ Comment[ar]=اعثر على غرف في نيوتشات
Comment[az]=NeoChat-da otaqları tapın
Comment[ca]=Cerca sales en el NeoChat
Comment[ca@valencia]=Busca sales en NeoChat
Comment[de]=Räume in NeoChat finden
Comment[el]=Εύρεση αιθουσών στο NeoChat
Comment[en_GB]=Find rooms in NeoChat
Comment[es]=Buscar salas en NeoChat
Comment[eu]=Bilatu gelak NeoChat-en

View File

@@ -85,6 +85,7 @@ QQC2.ToolBar {
Kirigami.Theme.colorSet: Kirigami.Theme.View
Kirigami.Theme.inherit: false
Kirigami.SpellChecking.enabled: true
color: Kirigami.Theme.textColor
selectionColor: Kirigami.Theme.highlightColor

View File

@@ -27,6 +27,10 @@ ColumnLayout {
signal chosen(string emoji)
onActiveFocusChanged: if (activeFocus) {
searchField.forceActiveFocus()
}
spacing: 0
QQC2.ScrollView {
@@ -78,6 +82,12 @@ ColumnLayout {
id: searchField
Layout.margins: Kirigami.Units.smallSpacing
Layout.fillWidth: true
/**
* The focus is manged by the parent and we don't want to use the standard
* shortcut as it could block other SearchFields from using it.
*/
focusSequence: ""
}
EmojiGrid {

View File

@@ -20,6 +20,9 @@ LoginStep {
target: LoginHelper
function onSsoUrlChanged() {
UrlHelper.openUrl(LoginHelper.ssoUrl)
root.showMessage(i18n("Complete the authentication steps in your browser"))
loginButton.enabled = true
loginButton.text = i18n("Login")
}
function onConnected() {
processed("qrc:/Loading.qml")
@@ -34,10 +37,12 @@ LoginStep {
}
}
QQC2.Button {
id: loginButton
text: i18n("Login")
onClicked: {
LoginHelper.loginWithSso()
root.showMessage(i18n("Complete the authentication steps in your browser"))
loginButton.enabled = false
loginButton.text = i18n("Loading…")
}
Component.onCompleted: forceActiveFocus()
Keys.onReturnPressed: clicked()

View File

@@ -40,6 +40,11 @@ QQC2.Popup {
// we used to be able to expect that the text field wouldn't attempt to
// perform a mini-DDOS attack using signals.
autoAccept: false
/**
* The focus is manged by the popup and we don't want to use the standard
* shortcut as it could block other SearchFields from using it.
*/
focusSequence: ""
Layout.preferredWidth: Kirigami.Units.gridUnit * 21 // 3 * 7 = 21, roughly 7 avatars on screen
Keys.onLeftPressed: cView.decrementCurrentIndex()

View File

@@ -33,10 +33,7 @@ TimelineContainer {
}
innerObject: RowLayout {
Layout.fillWidth: true
Layout.maximumWidth: fileDelegate.contentMaxWidth
Layout.margins: Kirigami.Units.largeSpacing
Layout.maximumWidth: Math.min(fileDelegate.contentMaxWidth, implicitWidth)
spacing: Kirigami.Units.largeSpacing
@@ -111,24 +108,16 @@ TimelineContainer {
}
ColumnLayout {
Layout.alignment: Qt.AlignVCenter
Layout.fillWidth: true
spacing: 0
QQC2.Label {
text: model.display
wrapMode: Text.Wrap
Layout.fillWidth: true
}
QQC2.Label {
id: sizeLabel
text: Controller.formatByteSize(content.info ? content.info.size : 0)
opacity: 0.7
Layout.fillWidth: true
}
}

View File

@@ -24,6 +24,13 @@ QQC2.Popup {
}
}
onVisibleChanged: {
if (!visible) {
return
}
emojiPicker.forceActiveFocus()
}
background: Kirigami.ShadowedRectangle {
Kirigami.Theme.colorSet: Kirigami.Theme.View
color: Kirigami.Theme.backgroundColor
@@ -43,6 +50,7 @@ QQC2.Popup {
implicitHeight: Kirigami.Units.gridUnit * 20 + 2 * padding
width: Math.min(contentItem.categoryIconSize * contentItem.categoryCount + 2 * padding, QQC2.Overlay.overlay.width)
contentItem: EmojiPicker {
id: emojiPicker
height: 400
includeCustom: emojiPopup.includeCustom
showQuickReaction: emojiPopup.showQuickReaction

View File

@@ -97,6 +97,19 @@ Kirigami.OverlaySheet {
}
}
}
Kirigami.BasicListItem {
visible: user !== room.localUser && room.canSendState("invite") && !room.containsUser(user.id)
action: Kirigami.Action {
enabled: !room.isUserBanned(user.id)
text: i18n("Invite this user")
icon.name: "list-add-user"
onTriggered: {
room.inviteToRoom(user.id)
root.close()
}
}
}
Kirigami.BasicListItem {
visible: user !== room.localUser && room.canSendState("ban") && !room.isUserBanned(user.id)

View File

@@ -21,6 +21,11 @@ Loader {
Component {
id: regularMenu
QQC2.Menu {
QQC2.MenuItem {
text: i18nc("'Space' is a matrix space", "View Space")
onTriggered: RoomManager.enterRoom(room);
}
QQC2.MenuItem {
text: i18nc("@action:inmenu", "Copy Address to Clipboard")
onTriggered: if (room.canonicalAlias.length === 0) {

View File

@@ -28,7 +28,7 @@ Kirigami.ScrollablePage {
clip: true
visible: spaceList.count > 0
Layout.preferredHeight: Kirigami.Units.gridUnit * 3
Layout.preferredHeight: Kirigami.Units.gridUnit * 2
Layout.fillWidth: true
model: SortFilterSpaceListModel {
@@ -39,26 +39,28 @@ Kirigami.ScrollablePage {
}
}
header: QQC2.Control {
contentItem: QQC2.RoundButton {
id: homeButton
flat: true
padding: Kirigami.Units.gridUnit / 2
icon.name: "home"
text: i18nc("@action:button", "Show All Rooms")
display: QQC2.AbstractButton.IconOnly
header: QQC2.ItemDelegate {
id: homeButton
icon.name: "home"
text: i18nc("@action:button", "Show All Rooms")
height: parent.height
width: height
leftPadding: topPadding
rightPadding: topPadding
onClicked: {
sortFilterRoomListModel.activeSpaceId = "";
spaceList.activeSpaceId = '';
listView.positionViewAtIndex(0, ListView.Beginning);
}
QQC2.ToolTip {
text: homeButton.text
}
Accessible.name: text
contentItem: Kirigami.Icon {
source: "home"
}
onClicked: {
sortFilterRoomListModel.activeSpaceId = "";
spaceList.activeSpaceId = '';
listView.positionViewAtIndex(0, ListView.Beginning);
}
QQC2.ToolTip.text: homeButton.text
QQC2.ToolTip.visible: hovered
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
}
delegate: QQC2.ItemDelegate {
@@ -84,9 +86,9 @@ Kirigami.ScrollablePage {
Accessible.name: currentRoom.displayName
QQC2.ToolTip {
text: currentRoom.displayName
}
QQC2.ToolTip.text: currentRoom.displayName
QQC2.ToolTip.visible: hovered
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
onPressAndHold: {
spaceList.createContextMenu(currentRoom)
@@ -116,9 +118,7 @@ Kirigami.ScrollablePage {
title: i18n("Rooms")
property var enteredRoom
property bool collapsedMode: Config.roomListPageWidth === applicationWindow().collapsedPageWidth && applicationWindow().shouldUseSidebars
verticalScrollBarPolicy: collapsedMode ? QQC2.ScrollBar.AlwaysOff : QQC2.ScrollBar.AsNeeded
property bool collapsedMode: Config.roomListPageWidth < applicationWindow().minPageWidth && applicationWindow().shouldUseSidebars
onCollapsedModeChanged: if (collapsedMode) {
sortFilterRoomListModel.filterText = "";
@@ -180,7 +180,7 @@ Kirigami.ScrollablePage {
leftPadding: Kirigami.Units.largeSpacing
rightPadding: Kirigami.Units.largeSpacing
bottomPadding: Kirigami.Units.largeSpacing
width: visible ? page.width : 0
width: visible ? ListView.view.width : 0
height: visible ? Kirigami.Units.gridUnit * 2 : 0
Kirigami.Icon {
@@ -234,19 +234,44 @@ Kirigami.ScrollablePage {
}
section.property: sortFilterRoomListModel.filterText.length === 0 && !Config.mergeRoomList ? "category" : null
section.delegate: Kirigami.ListSectionHeader {
id: sectionHeader
height: implicitHeight
label: roomListModel.categoryName(section)
action: Kirigami.Action {
onTriggered: roomListModel.setCategoryVisible(section, !roomListModel.categoryVisible(section))
}
contentItem.children: QQC2.ToolButton {
icon.name: page.collapsedMode ? roomListModel.categoryIconName(section) : (roomListModel.categoryVisible(section) ? "go-up" : "go-down")
icon.width: Kirigami.Units.iconSizes.small
icon.height: Kirigami.Units.iconSizes.small
section.delegate: page.collapsedMode ? foldButton : sectionHeader
onClicked: roomListModel.setCategoryVisible(section, !roomListModel.categoryVisible(section))
Component {
id: sectionHeader
Kirigami.ListSectionHeader {
height: implicitHeight
label: roomListModel.categoryName(section)
action: Kirigami.Action {
onTriggered: roomListModel.setCategoryVisible(section, !roomListModel.categoryVisible(section))
}
contentItem.children: QQC2.ToolButton {
icon.name: (roomListModel.categoryVisible(section) ? "go-up" : "go-down")
icon.width: Kirigami.Units.iconSizes.small
icon.height: Kirigami.Units.iconSizes.small
onClicked: roomListModel.setCategoryVisible(section, !roomListModel.categoryVisible(section))
}
}
}
Component {
id: foldButton
Item {
width: ListView.view.width
height: visible ? width : 0
QQC2.ToolButton {
id: button
anchors.centerIn: parent
icon.name: hovered ? (roomListModel.categoryVisible(section) ? "go-up" : "go-down") : roomListModel.categoryIconName(section)
icon.width: Kirigami.Units.iconSizes.smallMedium
icon.height: Kirigami.Units.iconSizes.smallMedium
onClicked: roomListModel.setCategoryVisible(section, !roomListModel.categoryVisible(section))
QQC2.ToolTip.text: roomListModel.categoryName(section)
QQC2.ToolTip.visible: hovered
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
}
}
}
@@ -272,7 +297,8 @@ Kirigami.ScrollablePage {
rightPadding: Kirigami.Units.largeSpacing
bottomPadding: Kirigami.Units.largeSpacing
width: ListView.view.width
height: ListView.view.width
height: visible ? ListView.view.width : 0
visible: model.categoryVisible || sortFilterRoomListModel.filterText.length > 0 || Config.mergeRoomList
contentItem: Kirigami.Avatar {
source: avatar ? "image://mxc/" + avatar : ""
@@ -316,6 +342,7 @@ Kirigami.ScrollablePage {
labelItem.textFormat: Text.PlainText
subtitle: subtitleText
subtitleItem.textFormat: Text.PlainText
subtitleItem.visible: !Config.compactRoomList
onPressAndHold: {
createRoomListContextMenu()
}
@@ -329,7 +356,7 @@ Kirigami.ScrollablePage {
source: avatar ? "image://mxc/" + avatar : ""
name: model.name || i18n("No Name")
implicitWidth: visible ? height : 0
visible: Config.showAvatarInTimeline
visible: Config.showAvatarInRoomDrawer
sourceSize.width: Kirigami.Units.gridUnit + Kirigami.Units.largeSpacing * 2
sourceSize.height: Kirigami.Units.gridUnit + Kirigami.Units.largeSpacing * 2
}
@@ -345,11 +372,13 @@ Kirigami.ScrollablePage {
Accessible.name: i18n("Muted room")
}
QQC2.Label {
id: notificationCountLabel
text: notificationCount > 0 ? notificationCount : "●"
visible: unreadCount > 0
padding: Kirigami.Units.smallSpacing
color: Kirigami.Theme.textColor
Layout.minimumWidth: height
Layout.rightMargin: Kirigami.Units.smallSpacing
Layout.minimumHeight: Kirigami.Units.iconSizes.smallMedium
Layout.minimumWidth: Math.max(notificationCountTextMetrics.advanceWidth + Kirigami.Units.smallSpacing * 2, height)
horizontalAlignment: Text.AlignHCenter
background: Rectangle {
visible: notificationCount > 0
@@ -358,10 +387,15 @@ Kirigami.ScrollablePage {
opacity: highlightCount > 0 ? 1 : 0.3
radius: height / 2
}
TextMetrics {
id: notificationCountTextMetrics
text: notificationCountLabel.text
}
}
QQC2.Button {
id: configButton
visible: roomListItem.hovered && !Kirigami.Settings.isMobile
visible: roomListItem.hovered && !Kirigami.Settings.isMobile && !Config.compactRoomList
Accessible.name: i18n("Configure room")
action: Kirigami.Action {
@@ -376,13 +410,13 @@ Kirigami.ScrollablePage {
function createRoomListContextMenu() {
const menu = roomListContextMenu.createObject(page, {room: currentRoom})
if (!Kirigami.Settings.isMobile) {
if (!Kirigami.Settings.isMobile && !Config.compactRoomList) {
configButton.visible = true
configButton.down = true
}
menu.closed.connect(function() {
configButton.down = undefined
configButton.visible = Qt.binding(function() { return roomListItem.hovered && !Kirigami.Settings.isMobile })
configButton.visible = Qt.binding(function() { return roomListItem.hovered && !Kirigami.Settings.isMobile && !Config.compactRoomList })
})
menu.open()
}

View File

@@ -48,6 +48,7 @@ Kirigami.ScrollablePage {
onCurrentRoomChanged: {
applicationWindow().hoverLinkIndicator.text = "";
messageListView.positionViewAtBeginning();
hasScrolledUpBefore = false;
}

View File

@@ -75,7 +75,7 @@ Kirigami.OverlayDrawer {
id: columnLayout
property alias userSearchText: userListSearchField.text
property alias highlightedUser: userListView.currentIndex
spacing: 0
spacing: Kirigami.Units.largeSpacing
Kirigami.AbstractApplicationHeader {
Layout.fillWidth: true
@@ -86,111 +86,19 @@ Kirigami.OverlayDrawer {
RowLayout {
anchors.fill: parent
spacing: Kirigami.Units.smallSpacing
Kirigami.Heading {
Layout.fillWidth: true
text: i18n("Room information")
level: 1
}
QQC2.ToolButton {
id: devtoolsButton
Layout.alignment: Qt.AlignRight
icon.name: "tools"
text: i18n("Open developer tools")
display: QQC2.AbstractButton.IconOnly
visible: Config.developerTools && Controller.quotientMinorVersion > 6
onClicked: {
applicationWindow().pageStack.layers.push("qrc:/DevtoolsPage.qml", {room: room}, {title: i18n("Developer Tools")})
roomDrawer.close();
}
QQC2.ToolTip {
text: devtoolsButton.text
}
}
QQC2.ToolButton {
id: searchButton
Layout.alignment: Qt.AlignRight
icon.name: "search"
text: i18n("Search in this room")
display: QQC2.AbstractButton.IconOnly
visible: Controller.quotientMinorVersion > 6
onClicked: {
pageStack.pushDialogLayer("qrc:/SearchPage.qml", {
currentRoom: room
}, {
title: i18nc("@action:title", "Search")
})
}
}
QQC2.ToolButton {
id: inviteButton
Layout.alignment: Qt.AlignRight
icon.name: "list-add-user"
text: i18n("Invite user to room")
display: QQC2.AbstractButton.IconOnly
onClicked: {
applicationWindow().pageStack.layers.push("qrc:/InviteUserPage.qml", {room: room})
roomDrawer.close();
}
QQC2.ToolTip {
text: inviteButton.text
}
}
QQC2.ToolButton {
id: favouriteButton
Layout.alignment: Qt.AlignRight
icon.name: room && room.isFavourite ? "rating" : "rating-unrated"
checkable: true
checked: room && room.isFavourite
text: room && room.isFavourite ? i18n("Remove room from favorites") : i18n("Make room favorite")
display: QQC2.AbstractButton.IconOnly
onClicked: room.isFavourite ? room.removeTag("m.favourite") : room.addTag("m.favourite", 1.0)
QQC2.ToolTip {
text: favouriteButton.text
}
}
QQC2.ToolButton {
id: encryptButton
Layout.alignment: Qt.AlignRight
icon.name: 'channel-insecure-symbolic'
enabled: roomDrawer.room.canEncryptRoom
visible: !roomDrawer.room.usesEncryption && Controller.encryptionSupported
text: i18n("Enable encryption")
display: QQC2.AbstractButton.IconOnly
onClicked: {
let dialog = confirmEncryptionDialog.createObject(applicationWindow(), {room: roomDrawer.room});
roomDrawer.close();
dialog.open();
}
QQC2.ToolTip {
text: encryptButton.text
}
}
QQC2.ToolButton {
id: settingsButton
Layout.alignment: Qt.AlignRight
icon.name: 'settings-configure'
icon.name: "settings-configure"
text: i18n("Room settings")
display: QQC2.AbstractButton.IconOnly
onClicked: QQC2.ApplicationWindow.window.pageStack.pushDialogLayer('qrc:/Categories.qml', {room: room}, { title: i18n("Room Settings") })
QQC2.ToolTip {
text: settingsButton.text
}
@@ -198,74 +106,151 @@ Kirigami.OverlayDrawer {
}
}
ColumnLayout {
RowLayout {
Layout.fillWidth: true
Layout.margins: Kirigami.Units.largeSpacing
Layout.leftMargin: Kirigami.Units.largeSpacing
spacing: Kirigami.Units.largeSpacing
RowLayout {
Layout.fillWidth: true
Layout.leftMargin: Kirigami.Units.smallSpacing
spacing: Kirigami.Units.largeSpacing
Kirigami.Avatar {
Layout.preferredWidth: Kirigami.Units.gridUnit * 3.5
Layout.preferredHeight: Kirigami.Units.gridUnit * 3.5
Kirigami.Avatar {
Layout.preferredWidth: Kirigami.Units.gridUnit * 3.5
Layout.preferredHeight: Kirigami.Units.gridUnit * 3.5
name: room ? room.displayName : ""
source: room ? ("image://mxc/" + room.avatarMediaId) : ""
name: room ? room.displayName : ""
source: room ? ("image://mxc/" + room.avatarMediaId) : ""
Rectangle {
visible: room.usesEncryption
color: Kirigami.Theme.backgroundColor
width: Kirigami.Units.gridUnit
height: Kirigami.Units.gridUnit
anchors.bottom: parent.bottom
anchors.right: parent.right
radius: width / 2
Kirigami.Icon {
source: "channel-secure-symbolic"
anchors.fill: parent
}
}
}
ColumnLayout {
ColumnLayout {
Layout.fillWidth: true
Layout.alignment: Qt.AlignVCenter
spacing: 0
Kirigami.Heading {
Layout.fillWidth: true
Layout.alignment: Qt.AlignVCenter
spacing: 0
Kirigami.Heading {
Layout.fillWidth: true
level: 1
type: Kirigami.Heading.Type.Primary
wrapMode: QQC2.Label.Wrap
text: room ? room.displayName : i18n("No name")
textFormat: Text.PlainText
}
TextEdit {
Layout.fillWidth: true
textFormat: TextEdit.PlainText
wrapMode: Text.WordWrap
selectByMouse: true
color: Kirigami.Theme.textColor
selectedTextColor: Kirigami.Theme.highlightedTextColor
selectionColor: Kirigami.Theme.highlightColor
readOnly: true
text: room && room.canonicalAlias ? room.canonicalAlias : i18n("No Canonical Alias")
}
level: 1
type: Kirigami.Heading.Type.Primary
wrapMode: QQC2.Label.Wrap
text: room ? room.displayName : i18n("No name")
textFormat: Text.PlainText
}
TextEdit {
Layout.fillWidth: true
textFormat: TextEdit.PlainText
wrapMode: Text.WordWrap
selectByMouse: true
color: Kirigami.Theme.textColor
selectedTextColor: Kirigami.Theme.highlightedTextColor
selectionColor: Kirigami.Theme.highlightColor
readOnly: true
text: room && room.canonicalAlias ? room.canonicalAlias : i18n("No Canonical Alias")
}
}
}
TextEdit {
Layout.fillWidth: true
text: room && room.topic ? room.topic.replace(replaceLinks, "<a href=\"$1\">$1</a>") : i18n("No Topic")
readonly property var replaceLinks: /(http[s]?:\/\/[^ \r\n]*)/g
textFormat: TextEdit.MarkdownText
wrapMode: Text.WordWrap
selectByMouse: true
color: Kirigami.Theme.textColor
selectedTextColor: Kirigami.Theme.highlightedTextColor
selectionColor: Kirigami.Theme.highlightColor
onLinkActivated: UrlHelper.openUrl(link)
readOnly: true
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.NoButton
cursorShape: parent.hoveredLink ? Qt.PointingHandCursor : Qt.IBeamCursor
}
TextEdit {
Layout.leftMargin: Kirigami.Units.largeSpacing
Layout.rightMargin: Kirigami.Units.largeSpacing
Layout.fillWidth: true
text: room && room.topic ? room.topic.replace(replaceLinks, "<a href=\"$1\">$1</a>") : i18n("No Topic")
readonly property var replaceLinks: /(http[s]?:\/\/[^ \r\n]*)/g
textFormat: TextEdit.MarkdownText
wrapMode: Text.WordWrap
selectByMouse: true
color: Kirigami.Theme.textColor
selectedTextColor: Kirigami.Theme.highlightedTextColor
selectionColor: Kirigami.Theme.highlightColor
onLinkActivated: UrlHelper.openUrl(link)
readOnly: true
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.NoButton
cursorShape: parent.hoveredLink ? Qt.PointingHandCursor : Qt.IBeamCursor
}
}
Kirigami.ListSectionHeader {
label: i18n("Options")
activeFocusOnTab: false
}
Kirigami.BasicListItem {
id: devtoolsButton
icon: "tools"
text: i18n("Open developer tools")
visible: Config.developerTools
onClicked: {
applicationWindow().pageStack.layers.push("qrc:/DevtoolsPage.qml", {room: room}, {title: i18n("Developer Tools")})
roomDrawer.close();
}
}
Kirigami.BasicListItem {
id: searchButton
icon: "search"
text: i18n("Search in this room")
onClicked: {
pageStack.pushDialogLayer("qrc:/SearchPage.qml", {
currentRoom: room
}, {
title: i18nc("@action:title", "Search")
})
}
}
Kirigami.BasicListItem {
id: favouriteButton
icon: room && room.isFavourite ? "rating" : "rating-unrated"
text: room && room.isFavourite ? i18n("Remove room from favorites") : i18n("Make room favorite")
onClicked: room.isFavourite ? room.removeTag("m.favourite") : room.addTag("m.favourite", 1.0)
}
Kirigami.ListSectionHeader {
label: i18n("Members")
activeFocusOnTab: false
spacing: 0
QQC2.ToolButton {
id: memberSearchToggle
checkable: true
icon.name: "search"
QQC2.ToolTip.text: i18n("Search user in room")
QQC2.ToolTip.visible: hovered
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
}
QQC2.ToolButton {
visible: roomDrawer.room.canSendState("invite")
icon.name: "list-add-user"
onClicked: {
applicationWindow().pageStack.layers.push("qrc:/InviteUserPage.qml", {room: roomDrawer.room})
roomDrawer.close();
}
QQC2.ToolTip.text: i18n("Invite user to room")
QQC2.ToolTip.visible: hovered
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
}
QQC2.Label {
Layout.alignment: Qt.AlignRight
@@ -275,12 +260,15 @@ Kirigami.OverlayDrawer {
Kirigami.SearchField {
id: userListSearchField
visible: memberSearchToggle.checked
Layout.fillWidth: true
Layout.leftMargin: Kirigami.Units.largeSpacing - 1
Layout.rightMargin: Kirigami.Units.largeSpacing - 1
Layout.bottomMargin: Kirigami.Units.smallSpacing
focusSequence: "Ctrl+Shift+F"
onAccepted: sortedMessageEventModel.filterString = text;
}
@@ -378,10 +366,4 @@ Kirigami.OverlayDrawer {
UserDetailDialog {}
}
Component {
id: confirmEncryptionDialog
ConfirmEncryptionDialog {}
}
}

View File

@@ -31,6 +31,16 @@ Kirigami.CategorizedSettings {
}
}
},
Kirigami.SettingAction {
text: i18n("Permissions")
icon.name: "visibility"
page: Qt.resolvedUrl("Permissions.qml")
initialProperties: {
return {
room: root.room
}
}
},
Kirigami.SettingAction {
text: i18n("Notifications")
icon.name: "notifications"

View File

@@ -119,6 +119,33 @@ Kirigami.ScrollablePage {
}
}
}
MobileForm.FormTextDelegate {
text: i18n("Room ID")
description: room.id
}
MobileForm.FormTextDelegate {
text: i18n("Room version")
description: room.version
contentItem.children: QQC2.Button {
visible: room.canSwitchVersions()
enabled: room.version < room.maxRoomVersion
text: i18n("Upgrade Room")
icon.name: "arrow-up-double"
onClicked: {
if (room.canSwitchVersions()) {
roomUpgradeSheet.currentRoomVersion = room.version
roomUpgradeSheet.open()
}
}
QQC2.ToolTip {
text: text
delay: Kirigami.Units.toolTipDelay
}
}
}
}
}
MobileForm.FormCard {
@@ -256,6 +283,30 @@ Kirigami.ScrollablePage {
OpenFileDialog {}
}
Kirigami.OverlaySheet {
id: roomUpgradeSheet
property var currentRoomVersion
title: i18n("Upgrade the Room")
Kirigami.FormLayout {
QQC2.SpinBox {
id: spinBox
Kirigami.FormData.label: i18n("Select new version")
from: room.version
to: room.maxRoomVersion
value: room.version
}
QQC2.Button {
text: i18n("Confirm")
onClicked: {
room.switchVersion(spinBox.value)
roomUpgradeSheet.close()
}
}
}
}
}
}

View File

@@ -0,0 +1,453 @@
// SPDX-FileCopyrightText: 2022 James Graham <james.h.graham@protonmail.com>
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
import QtQuick 2.15
import QtQuick.Controls 2.15 as QQC2
import QtQuick.Layouts 1.15
import org.kde.kirigami 2.15 as Kirigami
import org.kde.kirigamiaddons.labs.mobileform 0.1 as MobileForm
import org.kde.kitemmodels 1.0
import org.kde.neochat 1.0
Kirigami.ScrollablePage {
id: root
property var room
title: i18nc('@title:window', 'Permissions')
leftPadding: 0
rightPadding: 0
UserListModel {
id: userListModel
room: root.room
}
ListModel {
id: powerLevelModel
ListElement {text: "Member (0)"; powerLevel: 0}
ListElement {text: "Moderator (50)"; powerLevel: 50}
ListElement {text: "Admin (100)"; powerLevel: 100}
}
ColumnLayout {
MobileForm.FormCard {
Layout.fillWidth: true
contentItem: ColumnLayout {
spacing: 0
MobileForm.FormCardHeader {
title: i18n("Privileged Users")
}
Repeater {
model: KSortFilterProxyModel {
sourceModel: userListModel
sortRole: "perm"
filterRowCallback: function(source_row, source_parent) {
let permRole = sourceModel.data(sourceModel.index(source_row, 0, source_parent), Qt.UserRole + 5)
return permRole != UserType.Muted && permRole != UserType.Member;
}
}
delegate: MobileForm.FormTextDelegate {
text: name
description: userId
contentItem.children: RowLayout {
spacing: Kirigami.Units.largeSpacing
QQC2.Label {
visible: !room.canSendState("m.room.power_levels")
text: {
switch (perm) {
case UserType.Owner:
return i18n("Owner");
case UserType.Admin:
return i18n("Admin");
case UserType.Moderator:
return i18n("Mod");
case UserType.Muted:
return i18n("Muted");
case UserType.Member:
return i18n("Member");
default:
return ""
}
}
color: Kirigami.Theme.disabledTextColor
}
QQC2.ComboBox {
focusPolicy: Qt.NoFocus // provided by parent
model: powerLevelModel
textRole: "text"
valueRole: "powerLevel"
visible: room.canSendState("m.room.power_levels")
Component.onCompleted: currentIndex = indexOfValue(powerLevel)
onActivated: {
room.setUserPowerLevel(userId, currentValue)
}
}
}
}
}
MobileForm.FormDelegateSeparator { below: userListSearchCard }
MobileForm.AbstractFormDelegate {
id: userListSearchCard
Layout.fillWidth: true
visible: room.canSendState("m.room.power_levels")
contentItem: Kirigami.SearchField {
id: userListSearchField
Layout.fillWidth: true
autoAccept: false
Keys.onUpPressed: userListView.decrementCurrentIndex()
Keys.onDownPressed: userListView.incrementCurrentIndex()
onAccepted: {
let currentUser = userListView.itemAtIndex(userListView.currentIndex);
currentUser.action.trigger();
}
}
QQC2.Popup {
id: userListSearchPopup
x: userListSearchField.x
y: userListSearchField.y - height
width: userListSearchField.width
height: {
let maxHeight = userListSearchField.mapToGlobal(userListSearchField.x, userListSearchField.y).y - Kirigami.Units.largeSpacing * 3;
let minHeight = Kirigami.Units.gridUnit * 2 + userListSearchPopup.padding * 2;
let filterContentHeight = userListView.contentHeight + userListSearchPopup.padding * 2;
return Math.max(Math.min(filterContentHeight, maxHeight), minHeight);
}
padding: Kirigami.Units.smallSpacing
modal: false
onClosed: userListSearchField.text = ""
background: Kirigami.ShadowedRectangle {
radius: 4
color: Kirigami.Theme.backgroundColor
property color borderColor: Kirigami.Theme.textColor
border.color: Qt.rgba(borderColor.r, borderColor.g, borderColor.b, 0.3)
border.width: 1
shadow.xOffset: 0
shadow.yOffset: 4
shadow.color: Qt.rgba(0, 0, 0, 0.3)
shadow.size: 8
}
contentItem: QQC2.ScrollView {
// HACK: Hide unnecessary horizontal scrollbar (https://bugreports.qt.io/browse/QTBUG-83890)
QQC2.ScrollBar.horizontal.policy: QQC2.ScrollBar.AlwaysOff
ListView {
id: userListView
clip: true
model: UserFilterModel {
id: userListFilterModel
sourceModel: userListModel
filterText: userListSearchField.text
onFilterTextChanged: {
if (filterText.length > 0 && !userListSearchPopup.visible) {
userListSearchPopup.open()
} else if (filterText.length <= 0 && userListSearchPopup.visible) {
userListSearchPopup.close()
}
}
}
delegate: Kirigami.BasicListItem {
id: userListItem
implicitHeight: Kirigami.Units.gridUnit * 2
leftPadding: Kirigami.Units.largeSpacing + Kirigami.Units.smallSpacing
label: name
labelItem.textFormat: Text.PlainText
subtitle: userId
subtitleItem.textFormat: Text.PlainText
action: Kirigami.Action {
id: editPowerLevelAction
onTriggered: {
userListSearchPopup.close()
powerLevelSheet.userId = userId
powerLevelSheet.powerLevel = powerLevel
powerLevelSheet.open()
}
}
leading: Kirigami.Avatar {
implicitWidth: height
sourceSize.height: Kirigami.Units.gridUnit + Kirigami.Units.smallSpacing * 2.5
sourceSize.width: Kirigami.Units.gridUnit + Kirigami.Units.smallSpacing * 2.5
source: avatar ? ("image://mxc/" + avatar) : ""
name: model.userId
}
trailing: QQC2.Label {
visible: perm != UserType.Member
text: {
switch (perm) {
case UserType.Owner:
return i18n("Owner");
case UserType.Admin:
return i18n("Admin");
case UserType.Moderator:
return i18n("Mod");
case UserType.Muted:
return i18n("Muted");
default:
return "";
}
}
color: Kirigami.Theme.disabledTextColor
textFormat: Text.PlainText
wrapMode: Text.NoWrap
}
}
}
}
}
}
}
}
MobileForm.FormCard {
Layout.fillWidth: true
Layout.topMargin: Kirigami.Units.largeSpacing
visible: room.canSendState("m.room.power_levels")
contentItem: ColumnLayout {
spacing: 0
MobileForm.FormCardHeader {
title: i18n("Default permissions")
}
MobileForm.FormComboBoxDelegate {
text: i18n("Default user power level")
description: i18n("This is power level for all new users when joining the room")
textRole: "text"
valueRole: "powerLevel"
model: powerLevelModel
Component.onCompleted: currentIndex = indexOfValue(room.defaultUserPowerLevel)
onCurrentValueChanged: if(room.canSendState("m.room.power_levels")) room.defaultUserPowerLevel = currentValue
}
MobileForm.FormComboBoxDelegate {
text: i18n("Default power level to set the room state")
description: i18n("This is used for all state events that do not have their own entry here")
textRole: "text"
valueRole: "powerLevel"
model: powerLevelModel
Component.onCompleted: currentIndex = indexOfValue(room.statePowerLevel)
onCurrentValueChanged: if(room.canSendState("m.room.power_levels")) room.statePowerLevel = currentValue
}
MobileForm.FormComboBoxDelegate {
text: i18n("Default power level to send messages")
description: i18n("This is used for all message events that do not have their own entry here")
textRole: "text"
valueRole: "powerLevel"
model: powerLevelModel
Component.onCompleted: currentIndex = indexOfValue(room.defaultEventPowerLevel)
onCurrentValueChanged: if(room.canSendState("m.room.power_levels")) room.defaultEventPowerLevel = currentValue
}
}
}
MobileForm.FormCard {
Layout.fillWidth: true
Layout.topMargin: Kirigami.Units.largeSpacing
visible: room.canSendState("m.room.power_levels")
contentItem: ColumnLayout {
spacing: 0
MobileForm.FormCardHeader {
title: i18n("Basic permissions")
}
MobileForm.FormComboBoxDelegate {
text: i18n("Invite users")
textRole: "text"
valueRole: "powerLevel"
model: powerLevelModel
Component.onCompleted: currentIndex = indexOfValue(room.invitePowerLevel)
onCurrentValueChanged: if(room.canSendState("m.room.power_levels")) room.invitePowerLevel = currentValue
}
MobileForm.FormComboBoxDelegate {
text: i18n("Kick users")
textRole: "text"
valueRole: "powerLevel"
model: powerLevelModel
Component.onCompleted: currentIndex = indexOfValue(room.kickPowerLevel)
onCurrentValueChanged: if(room.canSendState("m.room.power_levels")) room.kickPowerLevel = currentValue
}
MobileForm.FormComboBoxDelegate {
text: i18n("Ban users")
textRole: "text"
valueRole: "powerLevel"
model: powerLevelModel
Component.onCompleted: currentIndex = indexOfValue(room.banPowerLevel)
onCurrentValueChanged: if(room.canSendState("m.room.power_levels")) room.banPowerLevel = currentValue
}
MobileForm.FormComboBoxDelegate {
text: i18n("Remove message sent by other users")
textRole: "text"
valueRole: "powerLevel"
model: powerLevelModel
Component.onCompleted: currentIndex = indexOfValue(room.redactPowerLevel)
onCurrentValueChanged: if(room.canSendState("m.room.power_levels")) room.redactPowerLevel = currentValue
}
}
}
MobileForm.FormCard {
Layout.fillWidth: true
Layout.topMargin: Kirigami.Units.largeSpacing
visible: room.canSendState("m.room.power_levels")
contentItem: ColumnLayout {
spacing: 0
MobileForm.FormCardHeader {
title: i18n("Event permissions")
}
MobileForm.FormComboBoxDelegate {
text: i18n("Change user permissions")
description: "m.room.power_levels"
textRole: "text"
valueRole: "powerLevel"
model: powerLevelModel
Component.onCompleted: currentIndex = indexOfValue(room.powerLevelPowerLevel)
onCurrentValueChanged: if(room.canSendState("m.room.power_levels")) room.powerLevelPowerLevel = currentValue
}
MobileForm.FormComboBoxDelegate {
text: i18n("Change the room name")
description: "m.room.name"
textRole: "text"
valueRole: "powerLevel"
model: powerLevelModel
Component.onCompleted: currentIndex = indexOfValue(room.namePowerLevel)
onCurrentValueChanged: if(room.canSendState("m.room.power_levels")) room.namePowerLevel = currentValue
}
MobileForm.FormComboBoxDelegate {
text: i18n("Change the room avatar")
description: "m.room.avatar"
textRole: "text"
valueRole: "powerLevel"
model: powerLevelModel
Component.onCompleted: currentIndex = indexOfValue(room.avatarPowerLevel)
onCurrentValueChanged: if(room.canSendState("m.room.power_levels")) room.avatarPowerLevel = currentValue
}
MobileForm.FormComboBoxDelegate {
text: i18n("Change the room canonical alias")
description: "m.room.canonical_alias"
textRole: "text"
valueRole: "powerLevel"
model: powerLevelModel
Component.onCompleted: currentIndex = indexOfValue(room.canonicalAliasPowerLevel)
onCurrentValueChanged: if(room.canSendState("m.room.power_levels")) room.canonicalAliasPowerLevel = currentValue
}
MobileForm.FormComboBoxDelegate {
text: i18n("Change the room topic")
description: "m.room.topic"
textRole: "text"
valueRole: "powerLevel"
model: powerLevelModel
Component.onCompleted: currentIndex = indexOfValue(room.topicPowerLevel)
onCurrentValueChanged: if(room.canSendState("m.room.power_levels")) room.topicPowerLevel = currentValue
}
MobileForm.FormComboBoxDelegate {
text: i18n("Enable encryption for the room")
description: "m.room.encryption"
textRole: "text"
valueRole: "powerLevel"
model: powerLevelModel
Component.onCompleted: currentIndex = indexOfValue(room.encryptionPowerLevel)
onCurrentValueChanged: if(room.canSendState("m.room.power_levels")) room.encryptionPowerLevel = currentValue
}
MobileForm.FormComboBoxDelegate {
text: i18n("Change the room history visibility")
description: "m.room.history_visibility"
textRole: "text"
valueRole: "powerLevel"
model: powerLevelModel
Component.onCompleted: currentIndex = indexOfValue(room.historyVisibilityPowerLevel)
onCurrentValueChanged: if(room.canSendState("m.room.power_levels")) room.historyVisibilityPowerLevel = currentValue
}
MobileForm.FormComboBoxDelegate {
text: i18n("Set pinned events")
description: "m.room.pinned_events"
textRole: "text"
valueRole: "powerLevel"
model: powerLevelModel
Component.onCompleted: currentIndex = indexOfValue(room.pinnedEventsPowerLevel)
onCurrentValueChanged: if(room.canSendState("m.room.power_levels")) room.pinnedEventsPowerLevel = currentValue
}
MobileForm.FormComboBoxDelegate {
text: i18n("Upgrade the room")
description: "m.room.tombstone"
textRole: "text"
valueRole: "powerLevel"
model: powerLevelModel
Component.onCompleted: currentIndex = indexOfValue(room.tombstonePowerLevel)
onCurrentValueChanged: if(room.canSendState("m.room.power_levels")) room.tombstonePowerLevel = currentValue
}
MobileForm.FormComboBoxDelegate {
text: i18n("Set the room server access control list (ACL)")
description: "m.room.server_acl"
textRole: "text"
valueRole: "powerLevel"
model: powerLevelModel
Component.onCompleted: currentIndex = indexOfValue(room.serverAclPowerLevel)
onCurrentValueChanged: if(room.canSendState("m.room.power_levels")) room.serverAclPowerLevel = currentValue
}
MobileForm.FormComboBoxDelegate {
visible: room.isSpace
text: i18n("Set the children of this space")
description: "m.space.child"
textRole: "text"
valueRole: "powerLevel"
model: powerLevelModel
Component.onCompleted: currentIndex = indexOfValue(room.spaceChildPowerLevel)
onCurrentValueChanged: if(room.canSendState("m.room.power_levels")) room.spaceChildPowerLevel = currentValue
}
MobileForm.FormComboBoxDelegate {
text: i18n("Set the parent space of this room")
description: "m.space.parent"
textRole: "text"
valueRole: "powerLevel"
model: powerLevelModel
Component.onCompleted: currentIndex = indexOfValue(room.spaceChildPowerLevel)
onCurrentValueChanged: if(room.canSendState("m.room.power_levels")) room.spaceParentPowerLevel = currentValue
}
}
}
}
Kirigami.OverlaySheet {
id: powerLevelSheet
title: i18n("Edit user power level")
property var userId
property int powerLevel
onSheetOpenChanged: {
if (sheetOpen) {
powerLevelComboBox.currentIndex = powerLevelComboBox.indexOfValue(powerLevelSheet.powerLevel)
}
}
Kirigami.FormLayout {
QQC2.ComboBox {
id: powerLevelComboBox
focusPolicy: Qt.NoFocus // provided by parent
model: powerLevelModel
textRole: "text"
valueRole: "powerLevel"
visible: room.canSendState("m.room.power_levels")
}
QQC2.Button {
text: i18n("Confirm")
onClicked: {
room.setUserPowerLevel(powerLevelSheet.userId, powerLevelComboBox.currentValue)
powerLevelSheet.close()
}
}
}
}
}

View File

@@ -14,6 +14,7 @@ Kirigami.ScrollablePage {
id: root
property var room
property string needUpgradeRoom: i18n("You need to upgrade this room to a newer version to enable this setting.")
title: i18n("Security")
@@ -21,6 +22,29 @@ Kirigami.ScrollablePage {
rightPadding: 0
ColumnLayout {
MobileForm.FormCard {
visible: Controller.encryptionSupported
Layout.topMargin: Kirigami.Units.largeSpacing
Layout.fillWidth: true
contentItem: ColumnLayout {
spacing: 0
MobileForm.FormCardHeader {
title: i18nc("@option:check", "Encryption")
}
MobileForm.FormSwitchDelegate {
id: enableEncryptionSwitch
text: i18n("Enable encryption")
description: i18nc("option:check", "Once enabled, encryption cannot be disabled.")
enabled: room.canEncryptRoom
checked: room.usesEncryption
onToggled: if (checked) {
let dialog = confirmEncryptionDialog.createObject(applicationWindow(), {room: room});
dialog.open();
}
}
}
}
MobileForm.FormCard {
Layout.topMargin: Kirigami.Units.largeSpacing
Layout.fillWidth: true
@@ -40,16 +64,25 @@ Kirigami.ScrollablePage {
}
MobileForm.FormRadioDelegate {
text: i18nc("@option:check", "Space members")
description: !["8", "9", "10"].includes(room.version)
? i18n("Anyone in a space can find and join.") + '\n' +
i18n("You need to upgrade this room to a newer version to enable this setting.")
: i18n("Anyone in a space can find and join.")
description: i18n("Anyone in a space can find and join.") +
(!["8", "9", "10"].includes(room.version) ? `\n${needUpgradeRoom}` : "")
checked: room.joinRule === "restricted"
enabled: room.canSendState("m.room.join_rules") && ["8", "9", "10"].includes(room.version) && false
onCheckedChanged: if (checked) {
room.joinRule = "restricted";
}
}
MobileForm.FormRadioDelegate {
text: i18nc("@option:check", "Knock")
description: i18n("People not in the room need to request an invite to join the room.") +
(!["7", "8", "9", "10"].includes(room.version) ? `\n${needUpgradeRoom}` : "")
checked: room.joinRule === "knock"
// https://spec.matrix.org/v1.4/rooms/#feature-matrix
enabled: room.canSendState("m.room.join_rules") && ["7", "8", "9", "10"].includes(room.version)
onCheckedChanged: if (checked) {
room.joinRule = "knock";
}
}
MobileForm.FormRadioDelegate {
text: i18nc("@option:check", "Public")
description: i18nc("@option:check", "Anyone can find and join.")
@@ -122,5 +155,25 @@ Kirigami.ScrollablePage {
}
}
}
Component {
id: confirmEncryptionDialog
ConfirmEncryptionDialog {
onClosed: {
// At the point this is executed, the state in the room is not yet changed.
// The value will be updated when room.onEncryption() emitted.
// This is in case if user simply closed the dialog.
enableEncryptionSwitch.checked = false
}
}
}
Connections {
target: room
onEncryption: {
enableEncryptionSwitch.checked = room.usesEncryption
}
}
}

View File

@@ -199,8 +199,20 @@ Kirigami.ScrollablePage {
}
}
}
MobileForm.FormDelegateSeparator { below: compactRoomListDelegate }
MobileForm.FormCheckDelegate {
id: compactRoomListDelegate
text: i18n("Use compact room list")
checked: Config.compactRoomList
onToggled: {
Config.compactRoomList = checked;
Config.save();
}
}
MobileForm.FormDelegateSeparator { below: colorSchemeDelegate.item ; visible: colorSchemeDelegate.visible }
MobileForm.FormDelegateSeparator { above: compactRoomListDelegate ; below: colorSchemeDelegate.item ; visible: colorSchemeDelegate.visible }
Loader {
id: colorSchemeDelegate

View File

@@ -83,7 +83,7 @@ Kirigami.ScrollablePage {
value: Config.proxyPort
from: 0
to: 65536
validator: IntValidator {bottom: from; top: to}
validator: IntValidator {bottom: portField.from; top: portField.to}
textFromValue: function(value, locale) {
return value // it will add a thousands separator if we don't do this, not sure why
}

View File

@@ -24,6 +24,7 @@ Kirigami.ApplicationWindow {
pageStack.initialPage: LoadingPage {}
pageStack.globalToolBar.canContainHandles: true
property RoomListPage roomListPage
property bool roomListLoaded: false
property RoomPage roomPage
@@ -164,7 +165,7 @@ Kirigami.ApplicationWindow {
readonly property int defaultPageWidth: Kirigami.Units.gridUnit * 17
readonly property int minPageWidth: Kirigami.Units.gridUnit * 10
readonly property int collapsedPageWidth: Kirigami.Units.gridUnit * 3 - Kirigami.Units.smallSpacing * 3
readonly property int collapsedPageWidth: Kirigami.Units.gridUnit * 3 - Kirigami.Units.smallSpacing * 3 + (roomListPage.contentItem.QQC2.ScrollBar.vertical.visible ? roomListPage.contentItem.QQC2.ScrollBar.vertical.width : 0)
readonly property bool shouldUseSidebars: RoomManager.hasOpenRoom && (Config.roomListPageWidth > minPageWidth ? root.width >= Kirigami.Units.gridUnit * 35 : root.width > Kirigami.Units.gridUnit * 27) && roomListLoaded
readonly property int pageWidth: {
if (Config.roomListPageWidth === -1) {
@@ -351,6 +352,7 @@ Kirigami.ApplicationWindow {
activeConnection: Controller.activeConnection
});
roomListLoaded = true;
roomListPage = pageStack.currentItem
RoomManager.loadInitialRoom();
}
}

View File

@@ -16,6 +16,7 @@
<file alias="Security.qml">qml/RoomSettings/Security.qml</file>
<file alias="PushNotification.qml">qml/RoomSettings/PushNotification.qml</file>
<file alias="Categories.qml">qml/RoomSettings/Categories.qml</file>
<file alias="Permissions.qml">qml/RoomSettings/Permissions.qml</file>
<file alias="FullScreenImage.qml">qml/Component/FullScreenImage.qml</file>
<file alias="UserInfo.qml">qml/Component/UserInfo.qml</file>
<file alias="FancyEffectsContainer.qml">qml/Component/FancyEffectsContainer.qml</file>

28
src/userfiltermodel.cpp Normal file
View File

@@ -0,0 +1,28 @@
// SPDX-FileCopyrightText: 2022 James Graham <james.h.graham@protonmail.com>
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
#include "userfiltermodel.h"
#include "userlistmodel.h"
bool UserFilterModel::filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const
{
Q_UNUSED(sourceParent);
if (m_filterText.length() < 1) {
return false;
}
return sourceModel()->data(sourceModel()->index(sourceRow, 0), UserListModel::NameRole).toString().contains(m_filterText, Qt::CaseInsensitive)
|| sourceModel()->data(sourceModel()->index(sourceRow, 0), UserListModel::UserIdRole).toString().contains(m_filterText, Qt::CaseInsensitive);
}
QString UserFilterModel::filterText() const
{
return m_filterText;
}
void UserFilterModel::setFilterText(const QString &filterText)
{
m_filterText = filterText;
Q_EMIT filterTextChanged();
invalidateFilter();
}

42
src/userfiltermodel.h Normal file
View File

@@ -0,0 +1,42 @@
// SPDX-FileCopyrightText: 2022 James Graham <james.h.graham@protonmail.com>
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
#pragma once
#include <QSortFilterProxyModel>
/**
* @class UserFilterModel
*
* This class creates a custom QSortFilterProxyModel for filtering a users by either
* display name or matrix ID. The filter can accept a full matrix id i.e. example:kde.org
* to separate between accounts on different servers with similar names.
*/
class UserFilterModel : public QSortFilterProxyModel
{
Q_OBJECT
/**
* @brief This property hold the text of the filter.
*
* The text is either a desired display name or matrix id.
*/
Q_PROPERTY(QString filterText READ filterText WRITE setFilterText NOTIFY filterTextChanged)
public:
/**
* @brief Custom filter function checking boith the display name and matrix ID.
*
* @note The filter cannot be modified and will always use the same filter properties.
*/
bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const override;
QString filterText() const;
void setFilterText(const QString &filterText);
Q_SIGNALS:
void filterTextChanged();
private:
QString m_filterText;
};

View File

@@ -38,6 +38,7 @@ void UserListModel::setRoom(NeoChatRoom *room)
connect(m_currentRoom, &Room::userRemoved, this, &UserListModel::userRemoved);
connect(m_currentRoom, &Room::memberAboutToRename, this, &UserListModel::userRemoved);
connect(m_currentRoom, &Room::memberRenamed, this, &UserListModel::userAdded);
connect(m_currentRoom, &Room::changed, this, &UserListModel::refreshAll);
{
m_users = m_currentRoom->users();
std::sort(m_users.begin(), m_users.end(), room->memberSorter());
@@ -132,6 +133,10 @@ QVariant UserListModel::data(const QModelIndex &index, int role) const
return UserType::Member;
}
if (role == PowerLevelRole) {
auto pl = m_currentRoom->getCurrentState<RoomPowerLevelsEvent>();
return pl->powerLevelForUser(user->id());
}
return {};
}
@@ -183,6 +188,34 @@ void UserListModel::refresh(Quotient::User *user, const QVector<int> &roles)
}
}
void UserListModel::refreshAll()
{
beginResetModel();
for (User *user : std::as_const(m_users)) {
user->disconnect(this);
}
m_users.clear();
{
m_users = m_currentRoom->users();
std::sort(m_users.begin(), m_users.end(), m_currentRoom->memberSorter());
}
for (User *user : std::as_const(m_users)) {
#ifdef QUOTIENT_07
connect(user, &User::defaultAvatarChanged, this, [this, user]() {
avatarChanged(user, m_currentRoom);
});
#else
connect(user, &User::avatarChanged, this, &UserListModel::avatarChanged);
#endif
}
connect(m_currentRoom->connection(), &Connection::loggedOut, this, [this]() {
setRoom(nullptr);
});
endResetModel();
Q_EMIT usersRefreshed();
}
void UserListModel::avatarChanged(Quotient::User *user, const Quotient::Room *context)
{
if (context == m_currentRoom) {
@@ -209,6 +242,7 @@ QHash<int, QByteArray> UserListModel::roleNames() const
roles[AvatarRole] = "avatar";
roles[ObjectRole] = "user";
roles[PermRole] = "perm";
roles[PowerLevelRole] = "powerLevel";
return roles;
}

View File

@@ -3,6 +3,8 @@
#pragma once
#include <room.h>
#include <QAbstractListModel>
#include <QObject>
@@ -40,6 +42,7 @@ public:
AvatarRole,
ObjectRole,
PermRole,
PowerLevelRole,
};
UserListModel(QObject *parent = nullptr);
@@ -54,13 +57,17 @@ public:
[[nodiscard]] QHash<int, QByteArray> roleNames() const override;
// Q_INVOKABLE
Q_SIGNALS:
void roomChanged();
void usersRefreshed();
private Q_SLOTS:
void userAdded(Quotient::User *user);
void userRemoved(Quotient::User *user);
void refresh(Quotient::User *user, const QVector<int> &roles = {});
void refreshAll();
void avatarChanged(Quotient::User *user, const Quotient::Room *context);
private: