Compare commits

..

7 Commits

Author SHA1 Message Date
Carl Schwan
3748b6902c assert false 2024-03-03 19:09:49 +01:00
Carl Schwan
a4917d82e9 More unit tests 2024-03-03 18:57:17 +01:00
Carl Schwan
b9bf37089b More fixes
Need https://github.com/quotient-im/libQuotient/pull/723
2024-03-03 17:22:25 +01:00
Carl Schwan
1844a90fc0 I hate models 2024-03-03 16:26:16 +01:00
Carl Schwan
23099046a3 Fix compilation 2024-03-03 14:00:41 +01:00
Carl Schwan
bdf4ee43c8 Use TreeItem from qt tree model example in RoomTreeModel 2024-03-03 13:31:06 +01:00
Carl Schwan
c6300179d8 hack to fix crash in roomtreemodel 2024-03-03 13:29:05 +01:00
134 changed files with 13817 additions and 15165 deletions

View File

@@ -2,7 +2,7 @@
"id": "org.kde.neochat",
"branch": "master",
"runtime": "org.kde.Platform",
"runtime-version": "6.6",
"runtime-version": "6.6-kf6preview",
"sdk": "org.kde.Sdk",
"command": "neochat",
"tags": [

View File

@@ -49,7 +49,3 @@ License: CC0-1.0
Files: appiumtests/data/*
Copyright: 2023 Tobias Fella <tobias.fella@kde.org>
License: CC0-1.0
Files: src/purpose/purposeplugin.json
Copyright: 2023 Tobias Fella <tobias.fella@kde.org>
License: BSD-2-Clause

View File

@@ -14,7 +14,7 @@ set(RELEASE_SERVICE_VERSION "${RELEASE_SERVICE_VERSION_MAJOR}.${RELEASE_SERVICE_
project(NeoChat VERSION ${RELEASE_SERVICE_VERSION})
set(KF_MIN_VERSION "6.0")
set(KF_MIN_VERSION "5.240.0")
set(QT_MIN_VERSION "6.5")
find_package(ECM ${KF_MIN_VERSION} REQUIRED NO_MODULE)
@@ -24,7 +24,7 @@ set(CMAKE_MODULE_PATH ${ECM_MODULE_PATH} ${CMAKE_SOURCE_DIR}/cmake)
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(KDE_COMPILERSETTINGS_LEVEL 6.0)
set(KDE_COMPILERSETTINGS_LEVEL 5.105)
include(FeatureSummary)
include(ECMSetupVersion)
@@ -72,10 +72,6 @@ set_package_properties(KF6Kirigami PROPERTIES
)
find_package(KF6KirigamiAddons 0.7.2 REQUIRED)
if (UNIX AND NOT APPLE AND NOT ANDROID AND NOT NEOCHAT_FLATPAK AND NOT NEOCHAT_APPIMAGE)
find_package(KF6 ${KF_MIN_VERSION} REQUIRED COMPONENTS Purpose)
endif ()
if(ANDROID)
find_package(OpenSSL)
set_package_properties(OpenSSL PROPERTIES

View File

@@ -23,6 +23,11 @@ ecm_add_test(
TEST_NAME delegatesizehelpertest
)
ecm_add_test(
roomtreemodeltest.cpp
LINK_LIBRARIES neochat Qt::Test
)
ecm_add_test(
mediasizehelpertest.cpp
LINK_LIBRARIES neochat Qt::Test

View File

@@ -55,7 +55,6 @@ private Q_SLOTS:
void genericBody_data();
void genericBody();
void nullGenericBody();
void markdownBody();
void subtitle();
void nullSubtitle();
void mediaInfo();
@@ -294,13 +293,6 @@ void EventHandlerTest::nullGenericBody()
QCOMPARE(noEventHandler.getGenericBody(), QString());
}
void EventHandlerTest::markdownBody()
{
EventHandler eventHandler(room, room->messageEvents().at(0).get());
QCOMPARE(eventHandler.getMarkdownBody(), QStringLiteral("This is an example\ntext message"));
}
void EventHandlerTest::subtitle()
{
EventHandler eventHandler(room, room->messageEvents().at(0).get());

View File

@@ -5,9 +5,7 @@
#include <QSignalSpy>
#include <QTest>
#include <Quotient/connection.h>
#include <Quotient/quotient_common.h>
#include <Quotient/syncdata.h>
#include "neochatconnection.h"
#include "testutils.h"
@@ -27,7 +25,8 @@ private Q_SLOTS:
void NeoChatRoomTest::initTestCase()
{
connection = Connection::makeMockConnection(QStringLiteral("@bob:kde.org"));
auto connection = new NeoChatConnection;
Connection::makeMockConnection(connection, QStringLiteral("@bob:kde.org"));
room = new TestUtils::TestRoom(connection, QStringLiteral("#myroom:kde.org"), "test-min-sync.json"_ls);
}

View File

@@ -0,0 +1,70 @@
// SPDX-FileCopyrightText: 2024 Carl Schwan <carl@carlschwan.eu>
// SPDX-License-Identifier: LGPL-2.0-or-later
#include <QAbstractItemModelTester>
#include <QTest>
#include "enums/neochatroomtype.h"
#include "models/roomtreemodel.h"
#include "models/sortfilterroomtreemodel.h"
#include "neochatconnection.h"
#include "testutils.h"
using namespace Quotient;
class RoomTreeModelTest : public QObject
{
Q_OBJECT
private Q_SLOTS:
void testTreeModel();
};
void RoomTreeModelTest::testTreeModel()
{
auto connection = new NeoChatConnection;
Connection::makeMockConnection(connection, QStringLiteral("@bob:kde.org"));
auto room = dynamic_cast<NeoChatRoom *>(new TestUtils::TestRoom(connection, QStringLiteral("#myroom:kde.org"), QStringLiteral("test-min-sync.json")));
QVERIFY(room);
connection->addRoom(room);
RoomTreeModel model;
model.setConnection(connection);
SortFilterRoomTreeModel filterModel;
filterModel.setSourceModel(&model);
QAbstractItemModelTester tester(&model);
QAbstractItemModelTester testerFilter(&filterModel);
QCOMPARE(model.rowCount(), static_cast<int>(NeoChatRoomType::TypesCount));
// Check data category
auto category = static_cast<int>(NeoChatRoomType::typeForRoom(room));
QCOMPARE(category, NeoChatRoomType::Normal);
auto normalCategoryIdx = model.index(category, 0);
QCOMPARE(model.data(normalCategoryIdx, RoomTreeModel::DisplayNameRole).toString(), QStringLiteral("Normal"));
QCOMPARE(model.data(normalCategoryIdx, RoomTreeModel::DelegateTypeRole).toString(), QStringLiteral("section"));
QCOMPARE(model.data(normalCategoryIdx, RoomTreeModel::IconRole).toString(), QStringLiteral("group"));
QCOMPARE(model.data(normalCategoryIdx, RoomTreeModel::CategoryRole).toInt(), category);
QCOMPARE(model.rowCount(normalCategoryIdx), 1);
// Check data room
auto roomIdx = model.index(0, 0, normalCategoryIdx);
QCOMPARE(model.data(roomIdx, RoomTreeModel::CurrentRoomRole).value<NeoChatRoom *>(), room);
QCOMPARE(model.data(roomIdx, RoomTreeModel::CategoryRole).toInt(), category);
// Move room
room->setProperty("isFavorite", true);
model.moveRoom(room);
auto newCategory = static_cast<int>(NeoChatRoomType::typeForRoom(room));
QCOMPARE(newCategory, NeoChatRoomType::Favorite);
auto newCategoryIdx = model.index(newCategory, 0);
QVERIFY(newCategoryIdx != normalCategoryIdx);
}
QTEST_MAIN(RoomTreeModelTest)
#include "roomtreemodeltest.moc"

View File

@@ -513,7 +513,7 @@ void TextHandlerTest::componentOutput_data()
<< QList<MessageComponent>{MessageComponent{MessageComponentType::Text, QStringLiteral("Text"), {}},
MessageComponent{MessageComponentType::Code,
QStringLiteral("Some code"),
QVariantMap{{QStringLiteral("class"), QStringLiteral("html")}}}};
QVariantMap{{QStringLiteral("class"), QStringLiteral("HTML")}}}};
QTest::newRow("quote") << QStringLiteral("<p>Text</p>\n<blockquote>\n<p>blockquote</p>\n</blockquote>")
<< QList<MessageComponent>{MessageComponent{MessageComponentType::Text, QStringLiteral("Text"), {}},
MessageComponent{MessageComponentType::Quote, QStringLiteral("\"blockquote\""), {}}};

View File

@@ -59,7 +59,6 @@
<summary xml:lang="fi">Keskustelu ystäviesi kanssa Matrixissa</summary>
<summary xml:lang="fr">Discuter avec vos ami(e)s sur le réseau Matrix</summary>
<summary xml:lang="gl">Charle coas súas amizades en Matrix.</summary>
<summary xml:lang="hu">Csevegjen barátaival a matrixon</summary>
<summary xml:lang="ia">Starta Conversation con tu amicos sur matrix</summary>
<summary xml:lang="it">Conversa con i tuoi contatti su matrix</summary>
<summary xml:lang="ka">ესაუბრეთ მეგობრებს Matrix-ზე</summary>
@@ -79,11 +78,8 @@
<p>NeoChat is a chat app that lets you take full advantage of the Matrix network. It provides you with a secure way to send text messages, videos and audio files to your family, colleagues and friends.</p>
<p xml:lang="ca">El NeoChat és una aplicació de xat que us permet aprofitar plenament la xarxa Matrix. Proporciona una manera segura d'enviar missatges de text, vídeos i arxius d'àudio a la vostra família, companys i amics.</p>
<p xml:lang="ca-valencia">NeoChat és una aplicació de xat que us permet aprofitar plenament la xarxa Matrix. Proporciona una manera segura d'enviar missatges de text, vídeos i arxius d'àudio a la vostra família, companys i amics.</p>
<p xml:lang="eo">NeoChat estas babilej-apo, kiu ebligas al vi plene profiti de la Matrix-reto. Ĝi provizas al vi sekuran manieron sendi tekstmesaĝojn, filmetojn kaj sondosierojn al via familio, kolegoj kaj amikoj.</p>
<p xml:lang="es">NeoChat es una aplicación de chat que le permite aprovechar al máximo la red Matrix. Le proporciona un modo seguro de enviar mensajes de texto, vídeos y archivos de sonido a su familia, colegas y amigos.</p>
<p xml:lang="eu">NeoChat, Matrix sarearen abantaila guztiei probetsua ateratzeko aukera ematen dizun berriketa aplikaizo bat da. Zure familiari, kideei eta lagunei testu mezuak, bideoak eta audio fitxategiak era seguruan bidaltzeko aukera ematen dizu.</p>
<p xml:lang="fr">NeoChat est une application de discussions vous permettant de profiter pleinement du réseau Matrix. Elle vous offre un moyen sécurisé denvoyer des messages de texte, des vidéos et des fichiers audio à votre famille, vos collègues et vos ami(e)s.</p>
<p xml:lang="hu">A NeoChat egy olyan csevegőalkalmazás, amellyel teljes mértékben kihasználhatja a Matrix hálózatot. Biztonságos módot biztosít szöveges üzenetek, videók és hangfájlok küldéséhez családtagjainak, kollégáinak és barátainak.</p>
<p xml:lang="ia">NeoChat es un app de conversation que te permitte prender avantage plen del rete Matrix. Il te forni un modo secur de inviar messages de texto, videos e files audio a tui familia, collegas e amicos.</p>
<p xml:lang="it">NeoChat è un'applicazione di chat che ti consente di sfruttare appieno la rete Matrix. Ti fornisce un modo sicuro per inviare messaggi di testo, video e file audio a familiari, colleghi e amici.</p>
<p xml:lang="ka">NeoChat ჩატის აპია, რომელიც საშუალება გაძლევთ, Matrix-ის ქსელის საშუალებები ბოლომდე გამოიყენოთ. ის გაძლევთ უსაფრთხო გზას, გააგზავნოთ ტექსტური შეტყობინებები, ვიდეოებ და აუდიოფაილები თქვენს ოჯახთან, კოლეგებთან და მეგობრებთან.</p>
@@ -93,7 +89,6 @@
<p xml:lang="tr">NeoChat, Matrix ağının tüm özelliklerini kullanan bir sohbet uygulamasıdır. Ailenize, arkadaşlarınıza ve iş arkadaşlarınıza metin iletileri, ses ve video dosyaları göndermenin kolay bir yolunu sunar.</p>
<p xml:lang="uk">NeoChat є програмою для спілкування, за допомогою якої ви можете скористатися усіма перевагами мережі Matrix. За її допомогою ви можете безпечно надсилати текстові повідомлення, відео та звукові файли вашим родичам, колегам та друзям.</p>
<p xml:lang="x-test">xxNeoChat is a chat app that lets you take full advantage of the Matrix network. It provides you with a secure way to send text messages, videos and audio files to your family, colleagues and friends.xx</p>
<p xml:lang="zh-TW">NeoChat 是一個讓您能夠完全利用 Matrix 網路的聊天應用程式。它讓您安全地傳送文字訊息、影片或音訊檔給家人、同事或朋友等等。</p>
<p>NeoChat aims to be a fully featured application for the Matrix specification. As such everything in the current stable specification with the notable exceptions of VoIP, threads and some aspects of End-to-End Encryption are supported. There are a few other smaller omissions due to the fact that the Matrix spec is constantly evolving but the aim remains to provide eventual support for the entire spec.</p>
<p xml:lang="ar">يهدف نيوتشات إلى أن يكون تطبيقًا كامل الميزات لمواصفات ماتركس. على هذا النحو يتم دعم كل شيء في المواصفات المستقرة الحالية مع الاستثناءات الملحوظة لـ VoIP والخيوط وبعض جوانب التشفير من طرف إلى طرف. هناك عدد قليل من الإغفالات الصغيرة الأخرى بسبب حقيقة أن مواصفات ماتركس تتطور باستمرار ، ولكن يبقى الهدف توفير الدعم النهائي للمواصفات بأكملها.</p>
<p xml:lang="ca">NeoChat pretén ser una aplicació amb totes les característiques per a l'especificació de Matrix. Com a tal, s'ha implementat tota l'especificació actual estable amb les notables excepcions de la VoIP, fils i alguns aspectes de l'encriptatge d'extrem a extrem. Hi ha algunes altres omissions més petites a causa del fet que l'especificació de Matrix està evolucionant constantment, però l'objectiu segueix sent proporcionar suport eventual per a tota l'especificació.</p>
@@ -105,7 +100,6 @@
<p xml:lang="fi">NeoChat pyrkii olemaan Matrix-määritelmän täysominaisuuksinen sovellus, joten se tukee kaikkea nykyisessä vakaassa määritelmässä muutamaa huomattavaa poikkeusta lukuun ottamatta (VoIP, säikeet ja jotkin piirteet päästä päähän -salauksessa). Joitakin pienempiäkin puutteita on Matrix-määritelmän jatkuvan kehityksen vuoksi, mutta lopputavoitteena on tarjota määritelmän täysi tuki.</p>
<p xml:lang="fr">L'objectif de NeoChat est d'être une application complète pour le protocole Matrix. En tant que tel, tout dans la spécification stable actuelle avec les exceptions notables de VoIP, les processus et certains aspects du chiffrement de bout en bout sont pris en charge. Il y a quelques autres petites omissions en raison du fait que la spécification du protocole Matrix est en constante évolution. Cependant, l'objectif reste de fournir un soutien éventuel pour l'ensemble de la spécification.</p>
<p xml:lang="gl">NeoChat pretende ser unha aplicación completa para a especificación de Matrix. Coas excepcións de VoIP, conversas fiadas e algúns aspectos da cifraxe de extremo a extremo, a versión estábel segue as especificacións. Existen algunhas outras pequenas omisións debido ao feito de que Matrix está en continua evolución pero a intención é implementar a especificación completa.</p>
<p xml:lang="hu">A NeoChat célja, hogy a Matrix specifikációnak megfelelő teljes funkcionalitású alkalmazás legyen. Mint ilyen, a jelenlegi stabil specifikáció támogatott a VoIP, a szálak és a végpontok közötti titkosítás egyes elemeinek kivételével. Van még néhány kisebb hiányosság annak köszönhetően, hogy a Matrix specifikáció folyamatosan fejlődik, de végső cél a teljes specifikáció megvalósítása.</p>
<p xml:lang="ia">NeoChat aspira a esser un application plenemente eminente per le specification de Matrix. Tal como omne cosas in le specification currentemente stabile con le exceptiones notabile de VOIP, threads e alcun aspectos del cryptation End-to-End es supportate. Il ha ltere pauc omissiones, debite al facto que le specification de Matrix es in evolution constante ma le aspiration remane a fornir supporto eventual per le integre specification.</p>
<p xml:lang="it">NeoChat mira ad essere un'applicazione completa per le specifiche Matrix. Pertanto, sono supportati tutti gli elementi dell'attuale specifica stabile con le notevoli eccezioni di VoIP, conversazioni e alcuni aspetti della cifratura end-to-end. Ci sono alcune altre piccole omissioni dovute al fatto che le specifiche Matrix sono in continua evoluzione, ma l'obiettivo rimane quello di fornire un eventuale supporto per l'intera specifica.</p>
<p xml:lang="ka">NeoChat მიზნად ისახავს Matrix სპეციფიკაციის სრული განხორციელება ჰქონდეს. როგორც ასეთი, ყველაფერი მიმდინარე სპეციფიკაციიდან, VoIP-ის, ძაფებისა და გამჭოლი დაშიფვრის ზოგიერთი ასპექტის გარდა, მხარდაჭერილია. შეძლება ასევე იყოს მცირე ლაფსუსებიც იმის გამო, რომ Matrix-ის სპეციფიკაცია მუდმივად ვითარდება, მაგრამ ჩვენი მიზანი მისი სრული მხარდაჭერაა.</p>
@@ -131,7 +125,6 @@
<p xml:lang="fi">Matrix-määritelmän kehittyessä NeoChat tukee myös monia epävakaita ominaisuuksia. Tällä hetkellä näitä ovat:</p>
<p xml:lang="fr">En raison de la nature du développement des spécifications du protocole Matrix, NeoChat prend également en charge de nombreuses fonctionnalités instables. Actuellement, ce sont :</p>
<p xml:lang="gl">Debido á natureza do desenvolvemento da especificación de Matrix, NeoChat tamén inclúe varias funcionalidades non estábeis:</p>
<p xml:lang="hu">A Matrix specifikáció fejlesztésének jellegéből adódóan a NeoChat számos instabil funkciót is támogat. Jelenleg a következőket:</p>
<p xml:lang="ia">Debite al natura del disveloppamento de specification de Matrix NeoChat tamben supporta numerose characteristicas instabile. Currentemente istes es:</p>
<p xml:lang="it">A causa della natura dello sviluppo delle specifiche Matrix, NeoChat supporta anche numerose funzionalità instabili. Attualmente queste sono:</p>
<p xml:lang="ka">Matrix-ის სპეციფიკაციის განვითარების ბუნების გამო NeoChat-ს ასევე აქვს უამრავი არასტაბილური ფუნქციაც. ახლა ისინია:</p>
@@ -159,7 +152,6 @@
<li xml:lang="fi">Kyselyt MSC3381</li>
<li xml:lang="fr">Sondages - MSC3381</li>
<li xml:lang="gl">Enquisas — MSC3381</li>
<li xml:lang="hu">Szavazások - MSC3381</li>
<li xml:lang="ia">Inquestas - MSC3381</li>
<li xml:lang="it">Sondaggi - MSC3381</li>
<li xml:lang="ka">Polls - MSC3381</li>
@@ -186,7 +178,6 @@
<li xml:lang="fi">Tarrapakkaukset MSC2545</li>
<li xml:lang="fr">Paquets d'auto-collants - MSC2545</li>
<li xml:lang="gl">Paquetes de adhesivos — MSC2545</li>
<li xml:lang="hu">Matricacsomagok - MSC2545</li>
<li xml:lang="ia">Etiquetta gummate (sticker) -MSC2545</li>
<li xml:lang="it">Pacchetti di adesivi - MSC2545</li>
<li xml:lang="ka">სტიკერების პაკეტები - MSC2545</li>
@@ -213,7 +204,6 @@
<li xml:lang="fi">Sijaintitapahtumat MSC3488</li>
<li xml:lang="fr">Événements de lieu - MSC3488</li>
<li xml:lang="gl">Localización de eventos — MSC3488</li>
<li xml:lang="hu">Események helyadatai - MSC3488</li>
<li xml:lang="ia">Eventos de Location - MSC3488</li>
<li xml:lang="it">Località eventi - MSC3488</li>
<li xml:lang="ka">მდებარეობის მოვლენები - MSC3488</li>
@@ -244,7 +234,8 @@
<keyword>Matrix</keyword>
<keyword>Kirigami</keyword>
</keywords>
<developer id="kde.org">
<developer>
<id>kde.org</id>
<name>The KDE Community</name>
<url>https://kde.org</url>
</developer>
@@ -273,7 +264,6 @@
<caption xml:lang="fi">Päänäkymä, jossa huoneluettelo, keskustelu ja huoneen tiedot</caption>
<caption xml:lang="fr">Vue principale avec la liste des salons ainsi que des informations sur les salons et forums de discussions</caption>
<caption xml:lang="gl">Vista principal coa lista de salas, a charla, e información da sala.</caption>
<caption xml:lang="hu">A fő nézet a szobalistával, csevegéssel és szobainformációkkal</caption>
<caption xml:lang="ia">Vista principal con lista de sala, chat e information de sala</caption>
<caption xml:lang="it">Vista principale con elenco delle stanze, chat e informazioni sulla stanza</caption>
<caption xml:lang="ka">მთავარი ხედი სურათების სიით, ჩატით და ოთახის ინფორმაციით</caption>
@@ -295,11 +285,8 @@
<caption>Discover new communities with Matrix Spaces</caption>
<caption xml:lang="ca">Descobriu comunitats noves amb els espais de Matrix</caption>
<caption xml:lang="ca-valencia">Descobriu comunitats noves amb els espais de Matrix</caption>
<caption xml:lang="eo">Malkovru novajn komunumojn per Matrix Spaces</caption>
<caption xml:lang="es">Descubra nuevas comunidades con los espacios de Matrix</caption>
<caption xml:lang="eu">Ezagutu komunitate berriak Matrixeko Tokiak erabiliz</caption>
<caption xml:lang="fr">Découvrez de nouvelles communautés avec les espaces sous Matrix</caption>
<caption xml:lang="hu">Fedezzen fel új közösségeket a Matrix Terek segítségével</caption>
<caption xml:lang="ia">Discoperi nove communitate con Matrix Spaces (Spatios de Matrix)</caption>
<caption xml:lang="it">Scopri nuove comunità con Matrix Spaces</caption>
<caption xml:lang="ka">აღმოაჩინეთ ახალი საზოგადოებები Matrix Spaces-თან ერთად</caption>
@@ -309,7 +296,6 @@
<caption xml:lang="tr">Matrix Alanlar ile yeni topluluklar keşfedin</caption>
<caption xml:lang="uk">Пошук нових спільнот за допомогою Matrix Spaces</caption>
<caption xml:lang="x-test">xxDiscover new communities with Matrix Spacesxx</caption>
<caption xml:lang="zh-TW">利用 Matrix 聊天空間發現新的社群</caption>
</screenshot>
<!--
Currently invalid. See https://github.com/ximion/appstream/issues/611
@@ -330,7 +316,6 @@
<caption xml:lang="fi">Päänäkymä, jossa huoneluettelo, keskustelu ja huoneen tiedot</caption>
<caption xml:lang="fr">Vue principale avec la liste des salons ainsi que des informations sur les salons et forums de discussions</caption>
<caption xml:lang="gl">Vista principal coa lista de salas, a charla, e información da sala.</caption>
<caption xml:lang="hu">A fő nézet a szobalistával, csevegéssel és szobainformációkkal</caption>
<caption xml:lang="ia">Vista principal con lista de sala, chat e information de sala</caption>
<caption xml:lang="it">Vista principale con elenco delle stanze, chat e informazioni sulla stanza</caption>
<caption xml:lang="ka">მთავარი ხედი სურათების სიით, ჩატით და ოთახის ინფორმაციით</caption>
@@ -360,7 +345,6 @@
<caption xml:lang="fi">Kirjautumisnäkymä</caption>
<caption xml:lang="fr">Écran de connexion</caption>
<caption xml:lang="gl">Pantalla de identificación.</caption>
<caption xml:lang="hu">Bejelentkező képernyő</caption>
<caption xml:lang="ia">Schermo de accesso</caption>
<caption xml:lang="it">Schermata di accesso</caption>
<caption xml:lang="ka">შესვლის ეკრანი</caption>
@@ -382,7 +366,6 @@
<content_attribute id="social-chat">intense</content_attribute>
</content_rating>
<releases>
<release version="24.02.1" date="2024-03-21"/>
<release version="24.02.0" date="2024-02-28">
<url>https://kde.org/announcements/megarelease/6/#neochat</url>
<description>

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

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

@@ -3,10 +3,6 @@
# SPDX-FileCopyrightText: 2020-2021 Tobias Fella <tobias.fella@kde.org>
# SPDX-License-Identifier: BSD-2-Clause
if (NOT ANDROID AND NOT WIN32 AND NOT APPLE AND NOT NEOCHAT_FLATPAK AND NOT NEOCHAT_APPIMAGE)
add_subdirectory(purpose)
endif()
add_library(neochat STATIC
controller.cpp
controller.h
@@ -169,8 +165,6 @@ add_library(neochat STATIC
mediamanager.h
models/statekeysmodel.cpp
models/statekeysmodel.h
sharehandler.cpp
sharehandler.h
)
qt_add_qml_module(neochat URI org.kde.neochat NO_PLUGIN
@@ -213,6 +207,19 @@ qt_add_qml_module(neochat URI org.kde.neochat NO_PLUGIN
qml/RoomData.qml
qml/ServerData.qml
qml/EmojiPicker.qml
qml/TimelineDelegate.qml
qml/ReplyComponent.qml
qml/StateDelegate.qml
qml/MessageDelegate.qml
qml/Bubble.qml
qml/SectionDelegate.qml
qml/ReactionDelegate.qml
qml/EventDelegate.qml
qml/ReadMarkerDelegate.qml
qml/MimeComponent.qml
qml/StateComponent.qml
qml/MessageEditComponent.qml
qml/AvatarFlow.qml
qml/LoginStep.qml
qml/Login.qml
qml/Homeserver.qml
@@ -297,10 +304,24 @@ qt_add_qml_module(neochat URI org.kde.neochat NO_PLUGIN
qml/SelectSpacesDialog.qml
qml/AttachDialog.qml
qml/NotificationsView.qml
qml/LoadingDelegate.qml
qml/TimelineEndDelegate.qml
qml/SearchPage.qml
qml/ServerComboBox.qml
qml/UserSearchPage.qml
qml/ManualUserDialog.qml
qml/MessageComponentChooser.qml
qml/TextComponent.qml
qml/ImageComponent.qml
qml/VideoComponent.qml
qml/AudioComponent.qml
qml/EncryptedComponent.qml
qml/FileComponent.qml
qml/LocationComponent.qml
qml/LiveLocationComponent.qml
qml/PollComponent.qml
qml/LinkPreviewComponent.qml
qml/LoadComponent.qml
qml/RecommendedSpaceDialog.qml
qml/RoomTreeSection.qml
qml/DelegateContextMenu.qml
@@ -309,13 +330,13 @@ qt_add_qml_module(neochat URI org.kde.neochat NO_PLUGIN
qml/IgnoredUsersDialog.qml
qml/AccountData.qml
qml/StateKeys.qml
qml/CodeComponent.qml
qml/QuoteComponent.qml
RESOURCES
qml/confetti.png
qml/glowdot.png
)
add_subdirectory(timeline)
if(UNIX)
qt_target_qml_sources(neochat QML_FILES qml/ShareAction.qml)
else()
@@ -399,14 +420,9 @@ if (NOT ANDROID AND NOT WIN32 AND NOT APPLE)
target_compile_definitions(neochat PUBLIC -DHAVE_RUNNER)
target_compile_definitions(neochat PUBLIC -DHAVE_X11)
target_sources(neochat PRIVATE runner.cpp)
if (TARGET KUnifiedPush)
target_sources(neochat PRIVATE fakerunner.cpp)
endif()
endif()
target_include_directories(neochat PRIVATE ${CMAKE_BINARY_DIR} ${CMAKE_CURRENT_SOURCE_DIR}/models ${CMAKE_CURRENT_SOURCE_DIR}/enums)
target_link_libraries(neochat PRIVATE timelineplugin)
target_link_libraries(neochat PUBLIC
Qt::Core
Qt::Quick
@@ -540,7 +556,7 @@ if(NOT ANDROID)
set_target_properties(neochat-app PROPERTIES OUTPUT_NAME "neochat")
endif()
if(TARGET KF6::DBusAddons AND NOT WIN32)
if(TARGET KF6::DBusAddons)
target_link_libraries(neochat PUBLIC KF6::DBusAddons)
target_compile_definitions(neochat PUBLIC -DHAVE_KDBUSADDONS)
endif()

View File

@@ -3,7 +3,6 @@
#include "chatbarcache.h"
#include "chatdocumenthandler.h"
#include "eventhandler.h"
#include "neochatroom.h"
@@ -118,7 +117,7 @@ QString ChatBarCache::relationMessage() const
if (auto event = room->findInTimeline(m_relationId); event != room->historyEdge()) {
EventHandler eventhandler(room, &**event);
return eventhandler.getMarkdownBody();
return eventhandler.getPlainBody();
}
return {};
}
@@ -164,54 +163,6 @@ QList<Mention> *ChatBarCache::mentions()
return &m_mentions;
}
void ChatBarCache::updateMentions(QQuickTextDocument *document, ChatDocumentHandler *documentHandler)
{
documentHandler->setDocument(document);
if (parent() == nullptr) {
qWarning() << "ChatBarCache created with no parent, a NeoChatRoom must be set as the parent on creation.";
return;
}
if (m_relationId.isEmpty()) {
return;
}
auto room = dynamic_cast<NeoChatRoom *>(parent());
if (room == nullptr) {
qWarning() << "ChatBarCache created with incorrect parent, a NeoChatRoom must be set as the parent on creation.";
return;
}
if (auto event = room->findInTimeline(m_relationId); event != room->historyEdge()) {
if (const auto &roomMessageEvent = &*event->viewAs<Quotient::RoomMessageEvent>()) {
// Replaces the mentions that are baked into the HTML but plaintext in the original markdown
const QRegularExpression re(QStringLiteral(R"lit(<a\shref="https:\/\/matrix.to\/#\/([\S]*)"\s?>([\S]*)<\/a>)lit"));
m_mentions.clear();
int linkSize = 0;
auto matches = re.globalMatch(EventHandler::rawMessageBody(*roomMessageEvent));
while (matches.hasNext()) {
const QRegularExpressionMatch match = matches.next();
if (match.hasMatch()) {
const QString id = match.captured(1);
const QString name = match.captured(2);
const int position = match.capturedStart(0) - linkSize;
const int end = position + name.length();
linkSize += match.capturedLength(0) - name.length();
QTextCursor cursor(documentHandler->document()->textDocument());
cursor.setPosition(position);
cursor.setPosition(end, QTextCursor::KeepAnchor);
cursor.setKeepPositionOnInsert(true);
m_mentions.push_back(Mention{.cursor = cursor, .text = name, .start = position, .position = end, .id = id});
}
}
}
}
}
QString ChatBarCache::savedText() const
{
return m_savedText;

View File

@@ -5,11 +5,8 @@
#include <QObject>
#include <QQmlEngine>
#include <QQuickTextDocument>
#include <QTextCursor>
class ChatDocumentHandler;
/**
* @brief Defines a user mention in the current chat or edit text.
*/
@@ -177,11 +174,6 @@ public:
*/
QList<Mention> *mentions();
/**
* @brief Update the mentions in @p document when editing a message.
*/
Q_INVOKABLE void updateMentions(QQuickTextDocument *document, ChatDocumentHandler *documentHandler);
/**
* @brief Get the saved chat bar text.
*/

View File

@@ -40,7 +40,6 @@ public:
Code, /**< A code section. */
Quote, /**< A quote section. */
File, /**< A message that is a file. */
Itinerary, /**< A preview for a file that can integrate with KDE itinerary.. */
Poll, /**< The initial event for a poll. */
Location, /**< A location event. */
LiveLocation, /**< The initial event of a shared live location (i.e., the place where this is supposed to be shown in the timeline). */

View File

@@ -29,6 +29,7 @@ public:
Deprioritized, /**< The room is set as low priority. */
Space, /**< The room is a space. */
AddDirect, /**< So we can show the add friend delegate. */
TypesCount, /**< Number of different types. */
};
Q_ENUM(Types);
@@ -40,7 +41,8 @@ public:
if (room->joinState() == Quotient::JoinState::Invite) {
return NeoChatRoomType::Invited;
}
if (room->isFavourite()) {
// HACK for the unit tests
if (room->isFavourite() || room->property("isFavorite").toBool()) {
return NeoChatRoomType::Favorite;
}
if (room->isLowPriority()) {

View File

@@ -280,22 +280,6 @@ QString EventHandler::getPlainBody(bool stripNewlines) const
return getBody(m_event, Qt::PlainText, stripNewlines);
}
QString EventHandler::getMarkdownBody() const
{
if (m_event == nullptr) {
qCWarning(EventHandling) << "getMarkdownBody called with m_event set to nullptr.";
return {};
}
if (!m_event->is<RoomMessageEvent>()) {
qCWarning(EventHandling) << "getMarkdownBody called when m_event isn't a RoomMessageEvent.";
return {};
}
const auto roomMessageEvent = eventCast<const RoomMessageEvent>(m_event);
return roomMessageEvent->plainBody();
}
QString EventHandler::getBody(const Quotient::RoomEvent *event, Qt::TextFormat format, bool stripNewlines) const
{
if (event->isRedacted()) {

View File

@@ -185,13 +185,6 @@ public:
*/
QString getPlainBody(bool stripNewlines = false) const;
/**
* @brief Output the original body for the message content, useful for editing the original message.
*
* The event type must be a room message event.
*/
QString getMarkdownBody() const;
/**
* @brief Output a generic string for the message content ready for display.
*

View File

@@ -1,36 +0,0 @@
// SPDX-FileCopyrightText: 2024 Joshua Goins <josh@redstrate.com>
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
#include "fakerunner.h"
#include <QCoreApplication>
#include <QDBusMetaType>
Q_SCRIPTABLE RemoteActions FakeRunner::Actions()
{
QCoreApplication::quit();
return {};
}
Q_SCRIPTABLE RemoteMatches FakeRunner::Match(const QString &searchTerm)
{
QCoreApplication::quit();
return {};
}
Q_SCRIPTABLE void FakeRunner::Run(const QString &id, const QString &actionId)
{
QCoreApplication::quit();
}
FakeRunner::FakeRunner()
: QObject()
{
qDBusRegisterMetaType<RemoteMatch>();
qDBusRegisterMetaType<RemoteMatches>();
qDBusRegisterMetaType<RemoteAction>();
qDBusRegisterMetaType<RemoteActions>();
qDBusRegisterMetaType<RemoteImage>();
}
#include "moc_fakerunner.cpp"

View File

@@ -1,31 +0,0 @@
// SPDX-FileCopyrightText: 2024 Joshua Goins <josh@redstrate.com>
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
#pragma once
#include <QDBusContext>
#include "runner.h"
/**
* This is a close-to-identical copy of the regular Runner interface,
* only used when activated for push notifications. This stubs it out so
* Plasma Search and Kickoff doesn't accidentally activate the push notification
* service.
*
* @sa Runner
*/
class FakeRunner : public QObject, protected QDBusContext
{
Q_OBJECT
Q_CLASSINFO("D-Bus Interface", "org.kde.krunner1")
public:
Q_SCRIPTABLE RemoteActions Actions();
Q_SCRIPTABLE RemoteMatches Match(const QString &searchTerm);
Q_SCRIPTABLE void Run(const QString &id, const QString &actionId);
FakeRunner();
};

View File

@@ -11,7 +11,6 @@
#include <QQmlNetworkAccessManagerFactory>
#include <QQuickStyle>
#include <QQuickWindow>
#include <QtQml/QQmlExtensionPlugin>
#ifdef Q_OS_ANDROID
#include <QGuiApplication>
@@ -45,16 +44,10 @@
#include "neochatconfig.h"
#include "roommanager.h"
#include "windowcontroller.h"
#include "sharehandler.h"
#ifdef HAVE_RUNNER
#include "runner.h"
#include <QDBusConnection>
#include <QDBusMetaType>
#endif
#if defined(HAVE_RUNNER) && defined(HAVE_KUNIFIEDPUSH)
#include "fakerunner.h"
#endif
#ifdef Q_OS_WINDOWS
@@ -193,9 +186,6 @@ int main(int argc, char *argv[])
parser.addOption(dbusActivatedOption);
#endif
QCommandLineOption shareOption(QStringLiteral("share"), i18n("Share a URL to Matrix"), QStringLiteral("text"));
parser.addOption(shareOption);
about.setupCommandLine(&parser);
parser.process(app);
about.processCommandLine(&parser);
@@ -206,14 +196,6 @@ int main(int argc, char *argv[])
// We want to be replaceable by the main client
KDBusService service(KDBusService::Replace);
#ifdef HAVE_RUNNER
// If we are built with KRunner and KUnifiedPush support, we need to do something special.
// Because KRunner may call us on the D-Bus (under the same service name org.kde.neochat) then it may
// accidentally activate us for push notifications instead. If this happens, then immediately quit if the fake
// runner is called.
QDBusConnection::sessionBus().registerObject("/RoomRunner"_ls, new FakeRunner(), QDBusConnection::ExportScriptableContents);
#endif
Controller::listenForNotifications();
return QCoreApplication::exec();
}
@@ -223,8 +205,6 @@ int main(int argc, char *argv[])
KDBusService service(KDBusService::Unique);
#endif
Q_IMPORT_QML_PLUGIN(org_kde_neochat_timelinePlugin)
qml_register_types_org_kde_neochat();
qmlRegisterSingletonInstance("org.kde.neochat.config", 1, 0, "Config", NeoChatConfig::self());
qmlRegisterSingletonInstance("org.kde.neochat.accounts", 1, 0, "AccountRegistry", &Controller::instance().accounts());
@@ -235,32 +215,26 @@ int main(int argc, char *argv[])
#ifdef HAVE_KDBUSADDONS
service.connect(&service,
&KDBusService::activateRequested,
&RoomManager::instance(),
[&engine](const QStringList &arguments, const QString &workingDirectory) {
Q_UNUSED(workingDirectory);
&KDBusService::activateRequested,
&RoomManager::instance(),
[&engine](const QStringList &arguments, const QString &workingDirectory) {
Q_UNUSED(workingDirectory);
QWindow *window = windowFromEngine(&engine);
KWindowSystem::updateStartupId(window);
QWindow *window = windowFromEngine(&engine);
KWindowSystem::updateStartupId(window);
WindowController::instance().showAndRaiseWindow(QString());
WindowController::instance().showAndRaiseWindow(QString());
// Open matrix uri
if (arguments.isEmpty()) {
return;
}
auto args = arguments;
args.removeFirst();
if (args.length() == 2 && args[0] == "--share"_ls) {
ShareHandler::instance().setText(args[1]);
return;
}
for (const auto &arg : args) {
RoomManager::instance().resolveResource(arg);
}
});
// Open matrix uri
if (arguments.isEmpty()) {
return;
}
auto args = arguments;
args.removeFirst();
for (const auto &arg : args) {
RoomManager::instance().resolveResource(arg);
}
});
#endif
engine.rootContext()->setContextObject(new KLocalizedContext(&engine));
@@ -273,10 +247,6 @@ int main(int argc, char *argv[])
});
}
if (parser.isSet("share"_ls)) {
ShareHandler::instance().setText(parser.value(shareOption));
}
engine.addImageProvider(QLatin1String("mxc"), MatrixImageProvider::create(&engine, &engine));
engine.addImageProvider(QLatin1String("blurhash"), new BlurhashImageProvider);
@@ -285,7 +255,7 @@ int main(int argc, char *argv[])
return -1;
}
if (!parser.positionalArguments().isEmpty() && !parser.isSet("share"_ls)) {
if (!parser.positionalArguments().isEmpty()) {
RoomManager::instance().setUrlArgument(parser.positionalArguments()[0]);
}

View File

@@ -3,7 +3,6 @@
#include "itinerarymodel.h"
#include <QJsonDocument>
#include <QProcess>
#include "config-neochat.h"
@@ -17,6 +16,20 @@ ItineraryModel::ItineraryModel(QObject *parent)
{
}
void ItineraryModel::setConnection(NeoChatConnection *connection)
{
if (m_connection == connection) {
return;
}
m_connection = connection;
Q_EMIT connectionChanged();
}
NeoChatConnection *ItineraryModel::connection() const
{
return m_connection;
}
QVariant ItineraryModel::data(const QModelIndex &index, int role) const
{
if (!index.isValid()) {
@@ -120,7 +133,11 @@ QString ItineraryModel::path() const
void ItineraryModel::setPath(const QString &path)
{
if (path == m_path) {
return;
}
m_path = path;
Q_EMIT pathChanged();
loadData();
}

View File

@@ -4,16 +4,19 @@
#pragma once
#include <QAbstractListModel>
#include <QJsonArray>
#include <QPointer>
#include <QQmlEngine>
#include <QString>
#include "neochatconnection.h"
class ItineraryModel : public QAbstractListModel
{
Q_OBJECT
QML_ELEMENT
QML_UNCREATABLE("")
Q_PROPERTY(NeoChatConnection *connection READ connection WRITE setConnection NOTIFY connectionChanged)
Q_PROPERTY(QString path READ path WRITE setPath NOTIFY pathChanged)
public:
enum Roles {
@@ -34,6 +37,9 @@ public:
Q_ENUM(Roles)
explicit ItineraryModel(QObject *parent = nullptr);
void setConnection(NeoChatConnection *connection);
NeoChatConnection *connection() const;
QVariant data(const QModelIndex &index, int role) const override;
int rowCount(const QModelIndex &parent = {}) const override;
@@ -44,7 +50,12 @@ public:
Q_INVOKABLE void sendToItinerary();
Q_SIGNALS:
void connectionChanged();
void pathChanged();
private:
QPointer<NeoChatConnection> m_connection;
QJsonArray m_data;
QString m_path;
void loadData();

View File

@@ -64,13 +64,11 @@ MessageContentModel::MessageContentModel(const Quotient::RoomEvent *event, NeoCh
});
connect(m_room, &NeoChatRoom::fileTransferCompleted, this, [this](const QString &eventId) {
if (m_event != nullptr && eventId == m_event->id()) {
updateComponents();
Q_EMIT dataChanged(index(0), index(rowCount() - 1), {FileTransferInfoRole});
}
});
connect(m_room, &NeoChatRoom::fileTransferFailed, this, [this](const QString &eventId) {
if (m_event != nullptr && eventId == m_event->id()) {
updateComponents();
Q_EMIT dataChanged(index(0), index(rowCount() - 1), {FileTransferInfoRole});
}
});
@@ -154,9 +152,6 @@ QVariant MessageContentModel::data(const QModelIndex &index, int role) const
return QVariant::fromValue(m_room->fileTransferInfo(event->id()));
}
}
if (role == ItineraryModelRole) {
return QVariant::fromValue<ItineraryModel *>(m_itineraryModel);
}
if (role == LatitudeRole) {
return eventHandler.getLatitude();
}
@@ -214,7 +209,6 @@ QHash<int, QByteArray> MessageContentModel::roleNames() const
roles[AuthorRole] = "author";
roles[MediaInfoRole] = "mediaInfo";
roles[FileTransferInfoRole] = "fileTransferInfo";
roles[ItineraryModelRole] = "itineraryModel";
roles[LatitudeRole] = "latitude";
roles[LongitudeRole] = "longitude";
roles[AssetRole] = "asset";
@@ -246,19 +240,11 @@ void MessageContentModel::updateComponents(bool isEditing)
if (isEditing) {
m_components += MessageComponent{MessageComponentType::Edit, QString(), {}};
} else if (m_event->isRedacted()) {
m_components += MessageComponent{MessageComponentType::Text, QString(), {}};
} else {
if (eventHandler.messageComponentType() == MessageComponentType::Text) {
const auto event = eventCast<const Quotient::RoomMessageEvent>(m_event);
auto body = EventHandler::rawMessageBody(*event);
m_components.append(TextHandler().textComponents(body, EventHandler::messageBodyInputFormat(*event), m_room, event, event->isReplaced()));
} else if (eventHandler.messageComponentType() == MessageComponentType::File) {
m_components += MessageComponent{MessageComponentType::File, QString(), {}};
updateItineraryModel();
if (m_itineraryModel != nullptr) {
m_components += MessageComponent{MessageComponentType::Itinerary, QString(), {}};
}
} else {
m_components += MessageComponent{eventHandler.messageComponentType(), QString(), {}};
}
@@ -274,25 +260,3 @@ void MessageContentModel::updateComponents(bool isEditing)
endResetModel();
}
void MessageContentModel::updateItineraryModel()
{
if (m_room == nullptr || m_event == nullptr) {
return;
}
if (auto event = eventCast<const Quotient::RoomMessageEvent>(m_event)) {
if (event->hasFileContent()) {
auto filePath = m_room->fileTransferInfo(event->id()).localPath;
if (filePath.isEmpty() && m_itineraryModel != nullptr) {
delete m_itineraryModel;
m_itineraryModel = nullptr;
} else if (!filePath.isEmpty()) {
if (m_itineraryModel == nullptr) {
m_itineraryModel = new ItineraryModel(this);
}
m_itineraryModel->setPath(filePath.toString());
}
}
}
}

View File

@@ -8,7 +8,6 @@
#include "enums/messagecomponenttype.h"
#include "eventhandler.h"
#include "itinerarymodel.h"
#include "linkpreviewer.h"
#include "neochatroom.h"
@@ -46,7 +45,6 @@ public:
AuthorRole, /**< The author of the event. */
MediaInfoRole, /**< The media info for the event. */
FileTransferInfoRole, /**< FileTransferInfo for any downloading files. */
ItineraryModelRole, /**< The itinerary model for a file. */
LatitudeRole, /**< Latitude for a location event. */
LongitudeRole, /**< Longitude for a location event. */
AssetRole, /**< Type of location event, e.g. self pin of the user location. */
@@ -94,7 +92,4 @@ private:
void updateComponents(bool isEditing = false);
LinkPreviewer *m_linkPreviewer = nullptr;
ItineraryModel *m_itineraryModel = nullptr;
void updateItineraryModel();
};

View File

@@ -162,30 +162,6 @@ QHash<int, QByteArray> ReactionModel::roleNames() const
};
}
bool isEmoji(const QString &text)
{
#ifdef HAVE_ICU
QTextBoundaryFinder finder(QTextBoundaryFinder::Grapheme, text);
int from = 0;
while (finder.toNextBoundary() != -1) {
auto to = finder.position();
if (text[from].isSpace()) {
from = to;
continue;
}
auto first = text.mid(from, to - from).toUcs4()[0];
if (!u_hasBinaryProperty(first, UCHAR_EMOJI)) {
return false;
}
from = to;
}
return true;
#else
return false;
#endif
}
QString ReactionModel::reactionText(QString text) const
{
text = text.toHtmlEscaped();
@@ -198,6 +174,28 @@ QString ReactionModel::reactionText(QString text) const
return QStringLiteral("<img src=\"%1\" width=\"%2\" height=\"%2\">")
.arg(m_room->connection()->makeMediaUrl(QUrl(text)).toString(), QString::number(size));
}
const auto isEmoji = [](const QString &text) {
#ifdef HAVE_ICU
QTextBoundaryFinder finder(QTextBoundaryFinder::Grapheme, text);
int from = 0;
while (finder.toNextBoundary() != -1) {
auto to = finder.position();
if (text[from].isSpace()) {
from = to;
continue;
}
auto first = text.mid(from, to - from).toUcs4()[0];
if (!u_hasBinaryProperty(first, UCHAR_EMOJI_PRESENTATION)) {
return false;
}
from = to;
}
return true;
#else
return false;
#endif
};
return isEmoji(text) ? QStringLiteral("<span style=\"font-family: 'emoji';\">") + text + QStringLiteral("</span>") : text;
}

View File

@@ -13,6 +13,177 @@
using namespace Quotient;
TreeItem::TreeItem(TreeData treeData, TreeItem *parent)
: m_treeData(treeData)
, m_parentItem(parent)
{
}
void TreeItem::appendChild(std::unique_ptr<TreeItem> &&child)
{
m_childItems.push_back(std::move(child));
}
bool TreeItem::insertChildren(int position, int count, TreeData treeData)
{
if (position < 0 || position > qsizetype(m_childItems.size()))
return false;
for (int row = 0; row < count; ++row) {
m_childItems.insert(m_childItems.cbegin() + position, std::make_unique<TreeItem>(treeData, this));
}
return true;
}
bool TreeItem::removeChildren(int position, int count)
{
if (position < 0 || position + count > qsizetype(m_childItems.size())) {
return false;
}
for (int row = 0; row < count; ++row) {
m_childItems.erase(m_childItems.cbegin() + position);
qWarning() << "removing" << position;
}
return true;
}
TreeItem *TreeItem::child(int row)
{
return row >= 0 && row < childCount() ? m_childItems.at(row).get() : nullptr;
}
int TreeItem::childCount() const
{
return int(m_childItems.size());
}
int TreeItem::row() const
{
if (m_parentItem == nullptr) {
return 0;
}
const auto it = std::find_if(m_parentItem->m_childItems.cbegin(), m_parentItem->m_childItems.cend(), [this](const std::unique_ptr<TreeItem> &treeItem) {
return treeItem.get() == this;
});
if (it != m_parentItem->m_childItems.cend())
return std::distance(m_parentItem->m_childItems.cbegin(), it);
Q_ASSERT(false); // should not happen
return -1;
}
QVariant TreeItem::data(int role) const
{
if (!m_parentItem) {
return {};
}
if (std::holds_alternative<NeoChatRoomType::Types>(m_treeData)) {
const auto row = this->row();
switch (role) {
case RoomTreeModel::IsCategoryRole:
return true;
case RoomTreeModel::DisplayNameRole:
return NeoChatRoomType::typeName(row);
case RoomTreeModel::DelegateTypeRole:
if (row == NeoChatRoomType::Search) {
return QStringLiteral("search");
}
if (row == NeoChatRoomType::AddDirect) {
return QStringLiteral("addDirect");
}
return QStringLiteral("section");
case RoomTreeModel::IconRole:
return NeoChatRoomType::typeIconName(row);
case RoomTreeModel::CategoryRole:
return row;
default:
return {};
}
}
const auto room = std::get<NeoChatRoom *>(m_treeData);
switch (role) {
case RoomTreeModel::IsCategoryRole:
return false;
case RoomTreeModel::DisplayNameRole:
return room->displayName();
case RoomTreeModel::AvatarRole:
return room->avatarMediaId();
case RoomTreeModel::CanonicalAliasRole:
return room->canonicalAlias();
case RoomTreeModel::TopicRole:
return room->topic();
case RoomTreeModel::CategoryRole:
return NeoChatRoomType::typeForRoom(room);
case RoomTreeModel::ContextNotificationCountRole:
return room->contextAwareNotificationCount();
case RoomTreeModel::HasHighlightNotificationsRole:
return room->highlightCount() > 0 && room->contextAwareNotificationCount() > 0;
case RoomTreeModel::LastActiveTimeRole:
return room->lastActiveTime();
case RoomTreeModel::JoinStateRole:
if (!room->successorId().isEmpty()) {
return QStringLiteral("upgraded");
}
return QVariant::fromValue(room->joinState());
case RoomTreeModel::CurrentRoomRole:
return QVariant::fromValue(room);
case RoomTreeModel::SubtitleTextRole: {
if (room->lastEvent() == nullptr || room->lastEventIsSpoiler()) {
return QString();
}
EventHandler eventHandler(room, room->lastEvent());
return eventHandler.subtitleText();
}
case RoomTreeModel::AvatarImageRole:
return room->avatar(128);
case RoomTreeModel::RoomIdRole:
return room->id();
case RoomTreeModel::IsSpaceRole:
return room->isSpace();
case RoomTreeModel::IsChildSpaceRole:
return SpaceHierarchyCache::instance().isChild(room->id());
case RoomTreeModel::ReplacementIdRole:
return room->successorId();
case RoomTreeModel::IsDirectChat:
return room->isDirectChat();
case RoomTreeModel::DelegateTypeRole:
return QStringLiteral("normal");
}
return {};
}
TreeItem *TreeItem::parentItem() const
{
return m_parentItem;
}
std::optional<int> TreeItem::position(Quotient::Room *room) const
{
Q_ASSERT_X(std::holds_alternative<NeoChatRoomType::Types>(m_treeData), __FUNCTION__, "containsRoom only works in category items");
int i = 0;
for (const auto &child : m_childItems) {
if (std::get<NeoChatRoom *>(child->treeData()) == room) {
return i;
}
i++;
}
return std::nullopt;
}
TreeItem::TreeData TreeItem::treeData() const
{
return m_treeData;
}
RoomTreeModel::RoomTreeModel(QObject *parent)
: QAbstractItemModel(parent)
{
@@ -21,15 +192,18 @@ RoomTreeModel::RoomTreeModel(QObject *parent)
void RoomTreeModel::initializeCategories()
{
for (const auto &key : m_rooms.keys()) {
for (const auto &room : m_rooms[key]) {
room->disconnect(this);
}
m_rootItem.reset(new TreeItem(nullptr, nullptr));
for (int i = 0; i < NeoChatRoomType::TypesCount; i++) {
m_rootItem->appendChild(std::make_unique<TreeItem>(NeoChatRoomType::Types(i), m_rootItem.get()));
}
m_rooms.clear();
for (int i = 0; i < 8; i++) {
m_rooms[NeoChatRoomType::Types(i)] = {};
}
TreeItem *RoomTreeModel::getItem(const QModelIndex &index) const
{
if (index.isValid()) {
return static_cast<TreeItem *>(index.internalPointer());
}
return m_rootItem.get();
}
void RoomTreeModel::setConnection(NeoChatConnection *connection)
@@ -68,24 +242,34 @@ void RoomTreeModel::newRoom(Room *r)
return;
}
beginInsertRows(index(type, 0), m_rooms[type].size(), m_rooms[type].size());
m_rooms[type].append(room);
auto categoryItem = m_rootItem->child(type);
beginInsertRows(index(type, 0), categoryItem->childCount(), categoryItem->childCount());
categoryItem->appendChild(std::make_unique<TreeItem>(room, categoryItem));
connectRoomSignals(room);
endInsertRows();
qWarning() << "adding room" << type << "new count" << categoryItem->childCount();
}
void RoomTreeModel::leftRoom(Room *r)
{
const auto room = dynamic_cast<NeoChatRoom *>(r);
const auto type = NeoChatRoomType::typeForRoom(room);
auto row = m_rooms[type].indexOf(room);
if (row == -1) {
auto idx = indexForRoom(room);
if (!idx.isValid()) {
return;
}
beginRemoveRows(index(type, 0), row, row);
m_rooms[type][row]->disconnect(this);
m_rooms[type].removeAt(row);
auto parentItem = getItem(idx.parent());
Q_ASSERT(parentItem);
beginRemoveRows(idx.parent(), idx.row(), idx.row());
const bool success = parentItem->removeChildren(idx.row(), 1);
room->disconnect(this);
endRemoveRows();
if (success) {
qWarning() << "Unable to remove room";
}
}
void RoomTreeModel::moveRoom(Quotient::Room *room)
@@ -94,31 +278,46 @@ void RoomTreeModel::moveRoom(Quotient::Room *room)
// NeoChatRoomType::typeForRoom doesn't match it's current location. So find the room.
NeoChatRoomType::Types oldType;
int oldRow = -1;
for (const auto &key : m_rooms.keys()) {
if (m_rooms[key].contains(room)) {
oldType = key;
oldRow = m_rooms[key].indexOf(room);
for (int i = 0; i < NeoChatRoomType::TypesCount; i++) {
auto categoryItem = m_rootItem->child(i);
auto position = categoryItem->position(room);
if (position) {
oldType = static_cast<NeoChatRoomType::Types>(i);
oldRow = *position;
}
}
if (oldRow == -1) {
return;
}
const auto newType = NeoChatRoomType::typeForRoom(dynamic_cast<NeoChatRoom *>(room));
auto neochatRoom = dynamic_cast<NeoChatRoom *>(room);
const auto newType = NeoChatRoomType::typeForRoom(neochatRoom);
if (newType == oldType) {
return;
}
const auto oldParent = index(oldType, 0, {});
auto oldParentItem = getItem(oldParent);
Q_ASSERT(oldParentItem);
const auto newParent = index(newType, 0, {});
auto newParentItem = getItem(newParent);
Q_ASSERT(newParentItem);
// HACK: We're doing this as a remove then insert because moving doesn't work
// properly with DelegateChooser for whatever reason.
Q_ASSERT(checkIndex(index(oldRow, 0, oldParent), QAbstractItemModel::CheckIndexOption::IndexIsValid));
beginRemoveRows(oldParent, oldRow, oldRow);
m_rooms[oldType].removeAt(oldRow);
const bool success = oldParentItem->removeChildren(oldRow, 1);
Q_ASSERT(success);
endRemoveRows();
beginInsertRows(newParent, m_rooms[newType].size(), m_rooms[newType].size());
m_rooms[newType].append(dynamic_cast<NeoChatRoom *>(room));
beginInsertRows(newParent, newParentItem->childCount(), newParentItem->childCount());
newParentItem->appendChild(std::make_unique<TreeItem>(neochatRoom, newParentItem));
endInsertRows();
// Q_ASSERT(checkIndex(index(newParentItem->childCount() - 1, 0, newParent), QAbstractItemModel::CheckIndexOption::IndexIsValid));
}
void RoomTreeModel::connectRoomSignals(NeoChatRoom *room)
@@ -151,14 +350,12 @@ void RoomTreeModel::connectRoomSignals(NeoChatRoom *room)
void RoomTreeModel::refreshRoomRoles(NeoChatRoom *room, const QList<int> &roles)
{
const auto roomType = NeoChatRoomType::typeForRoom(room);
const auto it = std::find(m_rooms[roomType].begin(), m_rooms[roomType].end(), room);
if (it == m_rooms[roomType].end()) {
const auto idx = indexForRoom(room);
if (!idx.isValid()) {
qCritical() << "Room" << room->id() << "not found in the room list";
return;
}
const auto parentIndex = index(roomType, 0, {});
const auto idx = index(it - m_rooms[roomType].begin(), 0, parentIndex);
Q_EMIT dataChanged(idx, idx, roles);
}
@@ -169,38 +366,42 @@ NeoChatConnection *RoomTreeModel::connection() const
int RoomTreeModel::columnCount(const QModelIndex &parent) const
{
Q_UNUSED(parent)
return 1;
const TreeItem *parentItem = getItem(parent);
return parentItem ? 1 : 0;
}
int RoomTreeModel::rowCount(const QModelIndex &parent) const
{
if (!parent.isValid()) {
return m_rooms.keys().size();
}
if (!parent.parent().isValid()) {
return m_rooms.values()[parent.row()].size();
}
return 0;
const TreeItem *parentItem = getItem(parent);
return parentItem ? parentItem->childCount() : 0;
}
QModelIndex RoomTreeModel::parent(const QModelIndex &index) const
{
if (!index.internalPointer()) {
if (!index.isValid()) {
return {};
}
return this->index(NeoChatRoomType::typeForRoom(static_cast<NeoChatRoom *>(index.internalPointer())), 0, QModelIndex());
TreeItem *childItem = getItem(index);
Q_ASSERT(childItem);
TreeItem *parentItem = childItem->parentItem();
return parentItem != m_rootItem.get() ? createIndex(parentItem->row(), 0, parentItem) : QModelIndex{};
}
QModelIndex RoomTreeModel::index(int row, int column, const QModelIndex &parent) const
{
if (!parent.isValid()) {
return createIndex(row, column, nullptr);
}
if (row >= rowCount(parent)) {
if (parent.isValid() && parent.column() != 0) {
return {};
}
return createIndex(row, column, m_rooms[NeoChatRoomType::Types(parent.row())][row]);
TreeItem *parentItem = getItem(parent);
Q_ASSERT(parentItem);
if (auto *childItem = parentItem->child(row)) {
return createIndex(row, column, childItem);
}
return {};
}
QHash<int, QByteArray> RoomTreeModel::roleNames() const
@@ -232,102 +433,12 @@ QHash<int, QByteArray> RoomTreeModel::roleNames() const
QVariant RoomTreeModel::data(const QModelIndex &index, int role) const
{
if (!checkIndex(index, QAbstractItemModel::CheckIndexOption::IndexIsValid)) {
return QVariant();
}
if (!index.parent().isValid()) {
if (role == DisplayNameRole) {
return NeoChatRoomType::typeName(index.row());
}
if (role == DelegateTypeRole) {
if (index.row() == NeoChatRoomType::Search) {
return QStringLiteral("search");
}
if (index.row() == NeoChatRoomType::AddDirect) {
return QStringLiteral("addDirect");
}
return QStringLiteral("section");
}
if (role == IconRole) {
return NeoChatRoomType::typeIconName(index.row());
}
if (role == CategoryRole) {
return index.row();
}
qWarning() << index.row() << rowCount(index.parent());
Q_ASSERT(false);
return {};
}
const auto room = m_rooms.values()[index.parent().row()][index.row()].get();
Q_ASSERT(room);
if (role == DisplayNameRole) {
return room->displayName();
}
if (role == AvatarRole) {
return room->avatarMediaId();
}
if (role == CanonicalAliasRole) {
return room->canonicalAlias();
}
if (role == TopicRole) {
return room->topic();
}
if (role == CategoryRole) {
return NeoChatRoomType::typeForRoom(room);
}
if (role == ContextNotificationCountRole) {
return int(room->contextAwareNotificationCount());
}
if (role == HasHighlightNotificationsRole) {
return room->highlightCount() > 0 && room->contextAwareNotificationCount() > 0;
}
if (role == LastActiveTimeRole) {
return room->lastActiveTime();
}
if (role == JoinStateRole) {
if (!room->successorId().isEmpty()) {
return QStringLiteral("upgraded");
}
return QVariant::fromValue(room->joinState());
}
if (role == CurrentRoomRole) {
return QVariant::fromValue(room);
}
if (role == SubtitleTextRole) {
if (room->lastEvent() == nullptr || room->lastEventIsSpoiler()) {
return QString();
}
EventHandler eventHandler(room, room->lastEvent());
return eventHandler.subtitleText();
}
if (role == AvatarImageRole) {
return room->avatar(128);
}
if (role == RoomIdRole) {
return room->id();
}
if (role == IsSpaceRole) {
return room->isSpace();
}
if (role == IsChildSpaceRole) {
return SpaceHierarchyCache::instance().isChild(room->id());
}
if (role == ReplacementIdRole) {
return room->successorId();
}
if (role == IsDirectChat) {
return room->isDirectChat();
}
if (role == DelegateTypeRole) {
return QStringLiteral("normal");
}
if (role == AttentionRole) {
return room->notificationCount() + room->highlightCount() > 0;
}
if (role == FavouriteRole) {
return room->isFavourite();
}
return {};
return getItem(index)->data(role);
}
QModelIndex RoomTreeModel::indexForRoom(NeoChatRoom *room) const
@@ -336,18 +447,18 @@ QModelIndex RoomTreeModel::indexForRoom(NeoChatRoom *room) const
return {};
}
// Try and find by checking type.
const auto type = NeoChatRoomType::typeForRoom(room);
auto row = m_rooms[type].indexOf(room);
if (row >= 0) {
return index(row, 0, index(type, 0));
}
// Double check that the room isn't in the wrong category.
for (const auto &key : m_rooms.keys()) {
if (m_rooms[key].contains(room)) {
return index(m_rooms[key].indexOf(room), 0, index(key, 0));
const auto roomType = NeoChatRoomType::typeForRoom(room);
const auto roomTypeItem = m_rootItem->child(roomType);
for (int i = 0, count = roomTypeItem->childCount(); i < count; i++) {
auto roomItem = roomTypeItem->child(i);
if (std::get<NeoChatRoom *>(roomItem->treeData()) == room) {
const auto parentIndex = index(roomType, 0, {});
const auto idx = index(i, 0, parentIndex);
return idx;
}
}
return {};
}

View File

@@ -15,6 +15,32 @@ class Room;
class NeoChatConnection;
class NeoChatRoom;
class RoomTreeModelTest;
class TreeItem
{
public:
using TreeData = std::variant<NeoChatRoom *, NeoChatRoomType::Types>;
explicit TreeItem(TreeData data, TreeItem *parentItem);
TreeItem *child(int row);
int childCount() const;
QVariant data(int role) const;
void appendChild(std::unique_ptr<TreeItem> &&child);
bool insertChildren(int position, int count, TreeData treeData);
TreeItem *parentItem() const;
bool removeChildren(int position, int count);
bool removeColumns(int position, int columns);
std::optional<int> position(Quotient::Room *room) const;
int row() const;
TreeData treeData() const;
private:
std::vector<std::unique_ptr<TreeItem>> m_childItems;
TreeData m_treeData;
TreeItem *m_parentItem;
};
class RoomTreeModel : public QAbstractItemModel
{
@@ -49,6 +75,7 @@ public:
IconRole,
AttentionRole, /**< Whether there are any notifications. */
FavouriteRole, /**< Whether the room is favourited. */
IsCategoryRole, /**< Whether the item in the model is a category */
};
Q_ENUM(EventRoles)
explicit RoomTreeModel(QObject *parent = nullptr);
@@ -76,6 +103,8 @@ public:
int columnCount(const QModelIndex &parent = QModelIndex()) const override;
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
TreeItem *getItem(const QModelIndex &index) const;
Q_INVOKABLE QModelIndex indexForRoom(NeoChatRoom *room) const;
Q_SIGNALS:
@@ -83,7 +112,6 @@ Q_SIGNALS:
private:
QPointer<NeoChatConnection> m_connection = nullptr;
QMap<NeoChatRoomType::Types, QList<QPointer<NeoChatRoom>>> m_rooms;
void initializeCategories();
void connectRoomSignals(NeoChatRoom *room);
@@ -93,4 +121,8 @@ private:
void moveRoom(Quotient::Room *room);
void refreshRoomRoles(NeoChatRoom *room, const QList<int> &roles = {});
std::unique_ptr<TreeItem> m_rootItem;
friend RoomTreeModelTest;
};

View File

@@ -119,20 +119,28 @@ QString SortFilterRoomTreeModel::filterText() const
bool SortFilterRoomTreeModel::filterAcceptsRow(int source_row, const QModelIndex &source_parent) const
{
// root node
if (!source_parent.isValid()) {
if (sourceModel()->data(sourceModel()->index(source_row, 0), RoomTreeModel::CategoryRole).toInt() == NeoChatRoomType::Search
&& NeoChatConfig::collapsed()) {
return true;
}
const QModelIndex index = sourceModel()->index(source_row, 0, source_parent);
if (!index.isValid()) {
qWarning() << source_row << source_parent << sourceModel()->rowCount(source_parent);
Q_ASSERT(false);
return true;
}
if (sourceModel()->data(index, RoomTreeModel::IsCategoryRole).toBool()) {
if (sourceModel()->data(index, RoomTreeModel::CategoryRole).toInt() == NeoChatRoomType::Search && NeoChatConfig::collapsed()) {
return true;
}
if (sourceModel()->data(sourceModel()->index(source_row, 0), RoomTreeModel::CategoryRole).toInt() == NeoChatRoomType::AddDirect
&& m_mode == DirectChats) {
if (sourceModel()->data(index, RoomTreeModel::CategoryRole).toInt() == NeoChatRoomType::AddDirect && m_mode == DirectChats) {
return true;
}
return false;
}
QModelIndex index = sourceModel()->index(source_row, 0, source_parent);
bool acceptRoom = sourceModel()->data(index, RoomTreeModel::DisplayNameRole).toString().contains(m_filterText, Qt::CaseInsensitive)
&& sourceModel()->data(index, RoomTreeModel::IsSpaceRole).toBool() == false;

View File

@@ -8,7 +8,6 @@
#include <Quotient/room.h>
#include "neochatconnection.h"
#include "neochatroom.h"
SpaceChildrenModel::SpaceChildrenModel(QObject *parent)
: QAbstractItemModel(parent)
@@ -33,13 +32,19 @@ void SpaceChildrenModel::setSpace(NeoChatRoom *space)
}
// disconnect the new room signal from the old connection in case it is different.
if (m_space != nullptr) {
m_space->connection()->disconnect(this);
m_space->disconnect(this);
disconnect(m_space->connection(), &Quotient::Connection::loadedRoomState, this, nullptr);
}
m_space = space;
Q_EMIT spaceChanged();
for (auto job : m_currentJobs) {
if (job) {
job->abandon();
}
}
m_currentJobs.clear();
auto connection = m_space->connection();
connect(connection, &Quotient::Connection::loadedRoomState, this, [this](Quotient::Room *room) {
if (m_pendingChildren.contains(room->name())) {
@@ -61,17 +66,6 @@ bool SpaceChildrenModel::loading() const
void SpaceChildrenModel::refreshModel()
{
for (auto job : m_currentJobs) {
if (job) {
job->abandon();
}
}
m_currentJobs.clear();
if (m_space == nullptr) {
return;
}
beginResetModel();
m_replacedRooms.clear();
delete m_rootItem;
@@ -118,11 +112,6 @@ void SpaceChildrenModel::insertChildren(std::vector<Quotient::GetSpaceHierarchyJ
if (!successorId.isEmpty()) {
m_replacedRooms += successorId;
}
if (dynamic_cast<NeoChatRoom *>(room)->isSpace()) {
connect(room, &Quotient::Room::changed, this, [this]() {
refreshModel();
});
}
}
if (children[i].childrenState.size() > 0) {
auto job = m_space->connection()->callApi<Quotient::GetSpaceHierarchyJob>(children[i].roomId, Quotient::none, Quotient::none, 1);
@@ -131,7 +120,8 @@ void SpaceChildrenModel::insertChildren(std::vector<Quotient::GetSpaceHierarchyJ
insertChildren(job->rooms(), index(insertRow, 0, parent));
});
}
parentItem->insertChild(new SpaceTreeItem(dynamic_cast<NeoChatConnection *>(m_space->connection()),
parentItem->insertChild(insertRow,
new SpaceTreeItem(dynamic_cast<NeoChatConnection *>(m_space->connection()),
parentItem,
children[i].roomId,
children[i].name,

View File

@@ -60,65 +60,4 @@ bool SpaceChildSortFilterModel::filterAcceptsRow(int sourceRow, const QModelInde
return true;
}
void SpaceChildSortFilterModel::move(const QModelIndex &currentIndex, const QModelIndex &targetIndex)
{
const auto rootSpace = dynamic_cast<SpaceChildrenModel *>(sourceModel())->space();
if (rootSpace == nullptr) {
return;
}
const auto connection = rootSpace->connection();
const auto currentParent = currentIndex.parent();
auto targetParent = targetIndex.parent();
NeoChatRoom *currentParentSpace = nullptr;
if (!currentParent.isValid()) {
currentParentSpace = rootSpace;
} else {
currentParentSpace = static_cast<NeoChatRoom *>(connection->room(currentParent.data(SpaceChildrenModel::RoomIDRole).toString()));
}
NeoChatRoom *targetParentSpace = nullptr;
if (!targetParent.isValid()) {
targetParentSpace = rootSpace;
} else {
targetParentSpace = static_cast<NeoChatRoom *>(connection->room(targetParent.data(SpaceChildrenModel::RoomIDRole).toString()));
}
// If both parents are not resolvable to a room object we don't have the permissions
// required for this action.
if (currentParentSpace == nullptr || targetParentSpace == nullptr) {
return;
}
const auto currentRow = currentIndex.row();
auto targetRow = targetIndex.row();
const auto moveRoomId = currentIndex.data(SpaceChildrenModel::RoomIDRole).toString();
auto targetRoom = static_cast<NeoChatRoom *>(connection->room(targetIndex.data(SpaceChildrenModel::RoomIDRole).toString()));
// If the target room is a space, assume we want to drop the room into it.
if (targetRoom != nullptr && targetRoom->isSpace()) {
targetParent = targetIndex;
targetParentSpace = targetRoom;
targetRow = rowCount(targetParent);
}
const auto newRowCount = rowCount(targetParent) + (currentParentSpace != targetParentSpace ? 1 : 0);
for (int i = 0; i < newRowCount; i++) {
if (currentParentSpace == targetParentSpace && i == currentRow) {
continue;
}
targetParentSpace->setChildOrder(index(i, 0, targetParent).data(SpaceChildrenModel::RoomIDRole).toString(),
QString::number(i > targetRow ? i + 1 : i, 36));
if (i == targetRow) {
if (currentParentSpace != targetParentSpace) {
currentParentSpace->removeChild(moveRoomId, true);
targetParentSpace->addChild(moveRoomId, true, false, false, QString::number(i + 1, 36));
} else {
targetParentSpace->setChildOrder(currentIndex.data(SpaceChildrenModel::RoomIDRole).toString(), QString::number(i + 1, 36));
}
}
}
}
#include "moc_spacechildsortfiltermodel.cpp"

View File

@@ -46,8 +46,6 @@ protected:
*/
bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const override;
Q_INVOKABLE void move(const QModelIndex &currentIndex, const QModelIndex &targetIndex);
Q_SIGNALS:
void filterTextChanged();

View File

@@ -37,11 +37,6 @@ SpaceTreeItem::~SpaceTreeItem()
qDeleteAll(m_children);
}
bool SpaceTreeItem::operator==(const SpaceTreeItem &other) const
{
return m_id == other.id();
}
SpaceTreeItem *SpaceTreeItem::child(int number)
{
if (number < 0 || number >= m_children.size()) {
@@ -55,20 +50,12 @@ int SpaceTreeItem::childCount() const
return m_children.count();
}
bool SpaceTreeItem::insertChild(SpaceTreeItem *newChild)
bool SpaceTreeItem::insertChild(int row, SpaceTreeItem *newChild)
{
if (newChild == nullptr) {
if (row < 0 || row > m_children.size()) {
return false;
}
for (auto it = m_children.begin(), end = m_children.end(); it != end; ++it) {
if (*it == newChild) {
*it = newChild;
return true;
}
}
m_children.append(newChild);
m_children.insert(row, newChild);
return true;
}

View File

@@ -35,8 +35,6 @@ public:
Quotient::StateEvents childStates = {});
~SpaceTreeItem();
bool operator==(const SpaceTreeItem &other) const;
/**
* @brief Return the child at the given row number.
*
@@ -50,9 +48,9 @@ public:
int childCount() const;
/**
* @brief Insert the given child.
* @brief Insert the given child at the given row number.
*/
bool insertChild(SpaceTreeItem *newChild);
bool insertChild(int row, SpaceTreeItem *newChild);
/**
* @brief Remove the child at the given row number.

View File

@@ -76,7 +76,6 @@ Comment[ru]=Клиент для Matrix — децентрализованног
Comment[sk]=Klient pre matrix, decentralizovaný komunikačný protokol
Comment[sl]=Odjemalec za decentralizirani komunikacijski protokol matrix
Comment[sv]=En klient för matrix, det decentraliserade kommunikationsprotokollet
Comment[ta]=மையமில்லா தகவல் பரிமாற்ற நெறிமுறையான மேட்ரிக்ஸுக்கான செயலி
Comment[tr]=Merkezi olmayan iletişim protokolü Matrix için bir istemci
Comment[uk]=Клієнт matrix, децентралізованого протоколу обміну даними
Comment[x-test]=xxA client for matrix, the decentralized communication protocolxx
@@ -254,14 +253,12 @@ Name[eo]=Kundividi
Name[es]=Compartir
Name[eu]=Partekatu
Name[fr]=Partager
Name[hu]=Megosztás
Name[ia]=Comparti
Name[it]=Condivisione
Name[ka]=გაზიარება
Name[nl]=Gedeelde
Name[pl]=Udostępnij
Name[sl]=Deli
Name[ta]=பகிர்
Name[tr]=Paylaş
Name[uk]=Оприлюднення
Name[x-test]=xxSharexx
@@ -273,14 +270,12 @@ Comment[eo]=La rezulto el kundividado de enhavero
Comment[es]=El resultado de compartir una parte de contenido
Comment[eu]=Eduki pieza bat partekatzearen emaitza
Comment[fr]=Le résultat du partage d'une partie de contenu.
Comment[hu]=Tartalom megosztásának eredménye
Comment[ia]=Le exito de compartir un pecietta de contento
Comment[it]=Il risultato della condivisione di un contenuto
Comment[ka]=შემცველობის ნაწილის გაზიარების შედეგი
Comment[nl]=Het resultaat van het delen van een stukje inhoud
Comment[pl]=Wynik udostępniania kawałka treści
Comment[sl]=Rezultat deljenega kosa vsebine
Comment[ta]=எதையோ பகிர்ந்த‍தன் விளைவு
Comment[tr]=Bir parça içerik paylaşımının sonucu
Comment[uk]=Результат оприлюднення даних
Comment[x-test]=xxThe result of sharing a piece of contentxx

View File

@@ -482,4 +482,9 @@ QString NeoChatConnection::accountDataJsonString(const QString &type) const
return QString::fromUtf8(QJsonDocument(accountDataJson(type)).toJson());
}
void NeoChatConnection::addRoom(Quotient::Room *room)
{
Connection::addRoom(room, false);
}
#include "moc_neochatconnection.cpp"

View File

@@ -147,6 +147,12 @@ public:
bool isOnline() const;
/**
* Add room directly in the connection.
* @internal for tests
*/
void addRoom(Quotient::Room *room);
Q_SIGNALS:
void labelChanged();
void directChatNotificationsChanged();

View File

@@ -1329,7 +1329,7 @@ bool NeoChatRoom::childrenHaveHighlightNotifications() const
return SpaceHierarchyCache::instance().spaceHasHighlightNotifications(id());
}
void NeoChatRoom::addChild(const QString &childId, bool setChildParent, bool canonical, bool suggested, const QString &order)
void NeoChatRoom::addChild(const QString &childId, bool setChildParent, bool canonical, bool suggested)
{
if (!isSpace()) {
return;
@@ -1337,9 +1337,7 @@ void NeoChatRoom::addChild(const QString &childId, bool setChildParent, bool can
if (!canSendEvent("m.space.child"_ls)) {
return;
}
setState("m.space.child"_ls,
childId,
QJsonObject{{QLatin1String("via"), QJsonArray{connection()->domain()}}, {"suggested"_ls, suggested}, {"order"_ls, order}});
setState("m.space.child"_ls, childId, QJsonObject{{QLatin1String("via"), QJsonArray{connection()->domain()}}, {"suggested"_ls, suggested}});
if (setChildParent) {
if (auto child = static_cast<NeoChatRoom *>(connection()->room(childId))) {
@@ -1405,28 +1403,6 @@ void NeoChatRoom::toggleChildSuggested(const QString &childId)
}
}
void NeoChatRoom::setChildOrder(const QString &childId, const QString &order)
{
if (!isSpace()) {
return;
}
if (!canSendEvent("m.space.child"_ls)) {
return;
}
if (const auto childEvent = currentState().get("m.space.child"_ls, childId)) {
auto content = childEvent->contentJson();
if (!content.contains("via"_ls)) {
return;
}
if (content.value("order"_ls).toString() == order) {
return;
}
content.insert("order"_ls, order);
setState("m.space.child"_ls, childId, content);
}
}
PushNotificationState::State NeoChatRoom::pushNotificationState() const
{
return m_currentPushNotificationState;

View File

@@ -560,7 +560,7 @@ public:
* Will fail if the user doesn't have the required privileges or this room is
* not a space.
*/
Q_INVOKABLE void addChild(const QString &childId, bool setChildParent = false, bool canonical = false, bool suggested = false, const QString &order = {});
Q_INVOKABLE void addChild(const QString &childId, bool setChildParent = false, bool canonical = false, bool suggested = false);
/**
* @brief Remove the given room as a child.
@@ -583,8 +583,6 @@ public:
*/
Q_INVOKABLE void toggleChildSuggested(const QString &childId);
void setChildOrder(const QString &childId, const QString &order = {});
bool isInvite() const;
bool readOnly() const;

View File

@@ -1,9 +0,0 @@
# SPDX-FileCopyrightText: 2024 Tobias Fella <tobias.fella@kde.org>
# SPDX-License-Identifier: BSD-2-Clause
kcoreaddons_add_plugin(neochatplugin SOURCES purposeplugin.cpp INSTALL_NAMESPACE "kf6/purpose")
target_link_libraries(neochatplugin
Qt::DBus
KF6::Purpose
KF6::KIOGui
)

View File

@@ -1,55 +0,0 @@
// SPDX-FileCopyrightText: 2024 Tobias Fella <tobias.fella@kde.org>
// SPDX-License-Identifier: LGPL-2.1-or-later
#include <KIO/CommandLauncherJob>
#include <KPluginFactory>
#include <Purpose/PluginBase>
class NeoChatJob : public Purpose::Job
{
Q_OBJECT
public:
explicit NeoChatJob(QObject *parent)
: Purpose::Job(parent)
{
}
QStringList arrayToList(const QJsonArray &array)
{
QStringList ret;
for (const auto &val : array) {
ret += val.toString();
}
return ret;
}
void start() override
{
const QJsonArray urlsJson = data().value(QStringLiteral("urls")).toArray();
const QString title = data().value(QStringLiteral("title")).toString();
const QString message = QStringLiteral("%1 - %2").arg(title, arrayToList(urlsJson).join(QLatin1Char(' ')));
auto *job = new KIO::CommandLauncherJob(QStringLiteral("neochat"), {QStringLiteral("--share"), message});
connect(job, &KJob::finished, this, &NeoChatJob::emitResult);
job->start();
}
};
class Q_DECL_EXPORT PurposePlugin : public Purpose::PluginBase
{
Q_OBJECT
public:
PurposePlugin(QObject *p, const QVariantList &)
: Purpose::PluginBase(p)
{
}
Purpose::Job *createJob() const override
{
return new NeoChatJob(nullptr);
}
};
K_PLUGIN_CLASS_WITH_JSON(PurposePlugin, "purposeplugin.json")
#include "purposeplugin.moc"

View File

@@ -1,60 +0,0 @@
{
"KPlugin": {
"Authors": [
{
"Name": "Tobias Fella",
"Name[ca@valencia]": "Tobias Fella",
"Name[ca]": "Tobias Fella",
"Name[es]": "Tobias Fella",
"Name[fr]": "Tobias Fella",
"Name[hu]": "Tobias Fella",
"Name[ia]": "Tobias Fella",
"Name[it]": "Tobias Fella",
"Name[ka]": "Tobias Fella",
"Name[nl]": "Tobias Fella",
"Name[pl]": "Tobias Fella",
"Name[sl]": "Tobias Fella",
"Name[tr]": "Tobias Fella",
"Name[uk]": "Tobias Fella",
"Name[x-test]": "xxTobias Fellaxx"
}
],
"Category": "Utilities",
"Description": "Share via NeoChat",
"Description[ca@valencia]": "Compartix a través de NeoChat",
"Description[ca]": "Comparteix a través del NeoChat",
"Description[es]": "Compartir mediante NeoChat",
"Description[fr]": "Partager grâce à NeoChat",
"Description[hu]": "Megosztás NeoChatben",
"Description[ia]": "Comparti via NeoChat",
"Description[it]": "Condividi tramite NeoChat",
"Description[ka]": "გააზიარეთ NeoChat-ით",
"Description[nl]": "Delen via NeoChat",
"Description[pl]": "Udostępnij przez NeoChat",
"Description[sl]": "Deli prek NeoChat",
"Description[tr]": "NeoChat ile Paylaş",
"Description[uk]": "Оприлюднити за допомогою NeoChat",
"Description[x-test]": "xxShare via NeoChatxx",
"Icon": "org.kde.neochat",
"License": "GPL",
"Name": "NeoChat",
"Name[ca@valencia]": "NeoChat",
"Name[ca]": "NeoChat",
"Name[es]": "NeoChat",
"Name[fr]": "NeoChat",
"Name[hu]": "NeoChat",
"Name[ia]": "Neochat",
"Name[it]": "NeoChat",
"Name[ka]": "NeoChat",
"Name[nl]": "NeoChat",
"Name[pl]": "NeoChat",
"Name[sl]": "NeoChat",
"Name[tr]": "NeoChat",
"Name[uk]": "NeoChat",
"Name[x-test]": "xxNeoChatxx",
"X-Purpose-ActionDisplay": "NeoChat"
},
"X-Purpose-PluginTypes": [
"ShareUrl"
]
}

View File

@@ -23,7 +23,7 @@ ColumnLayout {
model: root.connection.accountDataEventTypes
delegate: FormCard.FormButtonDelegate {
text: modelData
onClicked: applicationWindow().pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat', 'MessageSourceSheet.qml'), {
onClicked: applicationWindow().pageStack.pushDialogLayer("qrc:/org/kde/neochat/qml/MessageSourceSheet.qml", {
sourceText: root.connection.accountDataJsonString(modelData)
}, {
title: i18nc("@title:window", "Event Source"),

View File

@@ -34,25 +34,7 @@ QQC2.Control {
onActiveFocusChanged: textField.forceActiveFocus()
onCurrentRoomChanged: {
_private.chatBarCache = currentRoom.mainCache
if (ShareHandler.text.length > 0 && ShareHandler.room === root.currentRoom.id) {
textField.text = ShareHandler.text;
ShareHandler.text = "";
ShareHandler.room = "";
}
}
Connections {
target: ShareHandler
function onRoomChanged(): void {
if (ShareHandler.text.length > 0 && ShareHandler.room === root.currentRoom.id) {
textField.text = ShareHandler.text;
ShareHandler.text = "";
ShareHandler.room = "";
}
}
}
onCurrentRoomChanged: _private.chatBarCache = currentRoom.mainCache
/**
* @brief The ActionsHandler object to use.
@@ -241,7 +223,7 @@ QQC2.Control {
x: textField.cursorRectangle.x
y: textField.cursorRectangle.y - height
onFormattingSelected: _private.formatText(format, selectionStart, selectionEnd)
onFormattingSelected: root.formatText(format, selectionStart, selectionEnd)
}
Keys.onDeletePressed: {

View File

@@ -108,32 +108,8 @@ QQC2.Control {
}
}
QQC2.Button {
anchors {
top: parent.top
topMargin: Kirigami.Units.smallSpacing
right: parent.right
rightMargin: Kirigami.Units.smallSpacing
}
visible: root.hovered
icon.name: "edit-copy"
text: i18n("Copy to clipboard")
display: QQC2.AbstractButton.IconOnly
onClicked: Clipboard.saveText(root.display);
QQC2.ToolTip.text: text
QQC2.ToolTip.visible: hovered
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
}
background: Rectangle {
color: Kirigami.Theme.backgroundColor
radius: Kirigami.Units.smallSpacing
border {
width: root.hovered ? 1 : 0
color: Kirigami.Theme.highlightColor
}
}
}

View File

@@ -23,27 +23,17 @@ FormCard.FormCardPage {
header: QQC2.TabBar {
id: tabBar
readonly property real tabWidth: tabBar.width / tabBar.count
QQC2.TabButton {
text: qsTr("Room Data")
implicitWidth: tabBar.tabWidth
}
QQC2.TabButton {
text: qsTr("Server Info")
implicitWidth: tabBar.tabWidth
}
QQC2.TabButton {
text: i18nc("@title:tab", "Account Data")
implicitWidth: tabBar.tabWidth
}
QQC2.TabButton {
text: i18nc("@title:tab", "Feature Flags")
implicitWidth: tabBar.tabWidth
}
}

View File

@@ -59,6 +59,7 @@ ColumnLayout {
*/
readonly property bool downloaded: root.fileTransferInfo && root.fileTransferInfo.completed
onDownloadedChanged: {
itineraryModel.path = root.fileTransferInfo.localPath;
if (autoOpenFile) {
openSavedFile();
}
@@ -144,6 +145,15 @@ ColumnLayout {
QQC2.ToolTip.text: i18nc("tooltip for a button on a message; stops downloading the message's file", "Stop Download")
onClicked: root.room.cancelFileTransfer(root.eventId)
}
},
State {
name: "raw"
when: true
PropertyChanges {
target: downloadButton
onClicked: root.saveFileAs()
}
}
]
@@ -186,7 +196,6 @@ ColumnLayout {
QQC2.Button {
id: downloadButton
icon.name: "download"
onClicked: root.saveFileAs()
QQC2.ToolTip.text: i18nc("tooltip for a button on a message; offers ability to download its file", "Download")
QQC2.ToolTip.visible: hovered
@@ -211,4 +220,83 @@ ColumnLayout {
}
}
}
Repeater {
id: itinerary
model: ItineraryModel {
id: itineraryModel
connection: root.room.connection
}
delegate: DelegateChooser {
role: "type"
DelegateChoice {
roleValue: "TrainReservation"
delegate: ColumnLayout {
Kirigami.Separator {
Layout.fillWidth: true
}
RowLayout {
QQC2.Label {
text: model.name
}
QQC2.Label {
text: model.coach ? i18n("Coach: %1, Seat: %2", model.coach, model.seat) : ""
visible: model.coach
opacity: 0.7
}
}
RowLayout {
Layout.fillWidth: true
ColumnLayout {
QQC2.Label {
text: model.departureStation + (model.departurePlatform ? (" [" + model.departurePlatform + "]") : "")
}
QQC2.Label {
text: model.departureTime
opacity: 0.7
}
}
Item {
Layout.fillWidth: true
}
ColumnLayout {
QQC2.Label {
text: model.arrivalStation + (model.arrivalPlatform ? (" [" + model.arrivalPlatform + "]") : "")
}
QQC2.Label {
text: model.arrivalTime
opacity: 0.7
Layout.alignment: Qt.AlignRight
}
}
}
}
}
DelegateChoice {
roleValue: "LodgingReservation"
delegate: ColumnLayout {
Kirigami.Separator {
Layout.fillWidth: true
}
QQC2.Label {
text: model.name
}
QQC2.Label {
text: i18nc("<start time> - <end time>", "%1 - %2", model.startTime, model.endTime)
}
QQC2.Label {
text: model.address
}
}
}
}
}
QQC2.Button {
icon.name: "map-globe"
text: i18nc("@action", "Send to KDE Itinerary")
QQC2.ToolTip.visible: hovered
QQC2.ToolTip.text: text
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
onClicked: itineraryModel.sendToItinerary()
visible: itinerary.count > 0
}
}

View File

@@ -8,6 +8,8 @@ import QtQuick.Layouts
import org.kde.kirigami as Kirigami
import org.kde.neochat
/**
* @brief A component to show a link preview from a message.
*/

View File

@@ -5,6 +5,8 @@ import QtQuick
import org.kde.kirigami as Kirigami
import org.kde.neochat
TimelineDelegate {
id: root
contentItem: Kirigami.PlaceholderMessage {

View File

@@ -117,13 +117,6 @@ DelegateChooser {
}
}
DelegateChoice {
roleValue: MessageComponentType.Itinerary
delegate: ItineraryComponent {
maxContentWidth: root.maxContentWidth
}
}
DelegateChoice {
roleValue: MessageComponentType.Poll
delegate: PollComponent {

View File

@@ -170,11 +170,8 @@ QQC2.TextArea {
onChatBarCacheChanged: documentHandler.chatBarCache = chatBarCache
function updateEditText() {
// This could possibly be undefined due to some esoteric QtQuick issue. Referencing it somewhere in JS is enough.
documentHandler.document;
if (chatBarCache?.isEditing && chatBarCache.relationMessage.length > 0) {
root.text = chatBarCache.relationMessage;
chatBarCache.updateMentions(root.textDocument, documentHandler);
root.forceActiveFocus();
root.cursorPosition = root.length;
}

Some files were not shown because too many files have changed in this diff Show More