Compare commits

..

36 Commits

Author SHA1 Message Date
James Graham
e8b269869c Some people don't want friends so fix 2024-01-24 16:41:13 +00:00
l10n daemon script
7fd8394253 GIT_SILENT Sync po/docbooks with svn 2024-01-24 01:17:22 +00:00
Tobias Fella
c54a447caf Add appstream developer tag and remove developer_name tag 2024-01-23 22:42:36 +01:00
l10n daemon script
61b009422d GIT_SILENT Sync po/docbooks with svn 2024-01-23 01:18:48 +00:00
l10n daemon script
5a8b0184ea GIT_SILENT Sync po/docbooks with svn 2024-01-22 01:29:04 +00:00
James Graham
f48c2a21d9 Autosearch
Make the user search automatically. This includes a timer to ensure that we aren't constantly pinging the server as the user types, the search is started 0.5s after the user stops typing. The `PublicRoomListModel` is upgraded to work in the same manner as it was architected slightly differently.
2024-01-21 11:24:40 +00:00
l10n daemon script
538cfbee8d GIT_SILENT Sync po/docbooks with svn 2024-01-21 01:17:19 +00:00
James Graham
7666f1c362 Fix the vertical alignment of the notification bubble text 2024-01-20 19:07:27 +00:00
James Graham
4b5d828bf8 The search for friendship
Add the ability to search in the user directory for friends.

This adds an option in roomlist when on the friends tab and opens a search dialog when clicked. The new search model searches the user directory for the given filter term.
2024-01-20 16:13:49 +00:00
Tobias Fella
4bd160cceb Remove workaround for QTBUG 93281
Seems to no longer be required
2024-01-20 16:13:13 +00:00
Joshua Goins
5f56fc1156 Add icon for notification state menu
In Qt6 we can (finally) add icons to QQC Menus!
2024-01-20 13:47:17 +00:00
l10n daemon script
72a2a74395 GIT_SILENT Sync po/docbooks with svn 2024-01-20 01:17:30 +00:00
James Graham
f6a5cc7c25 Generic Search Page
Pull the generic aspects from Room search and join room pages into it's own component. This is done in anticipation of using the new generic search page for a user search functionality.

- `SearchPage` is now used for the generic version with the old one being renamed `RoomSearchPage`
- `JoinRoomPage` is renamed to `ExploreRoomsPage` inline with everywhere else in NeoChat

There is also some cleanup of the code for both search pages in here.
2024-01-19 17:59:45 +00:00
l10n daemon script
80f3bd64b6 GIT_SILENT Sync po/docbooks with svn 2024-01-18 01:18:17 +00:00
Joshua Goins
1f69a96766 Hide the subtitle text for room delegates if there is none
This centers the room name label for room list items, which looks a bit
cleaner than nothing being there at all.
2024-01-17 17:29:57 +00:00
James Graham
f963e06983 Remove the option to merge the room list 2024-01-17 16:58:51 +00:00
l10n daemon script
e6980e2370 GIT_SILENT Sync po/docbooks with svn 2024-01-17 01:19:30 +00:00
James Graham
8e8105d04d Clip QuickSwitcher
Clip QuickSwitcher to stop the delegates overlapping the dialog
2024-01-16 20:08:53 +00:00
Ingo Klöcker
21d9e69712 Require master of ECM
We need the fix for APK packaging with Android NDK r25
2024-01-16 13:57:33 +01:00
l10n daemon script
e0783a3c6e GIT_SILENT Sync po/docbooks with svn 2024-01-16 01:19:20 +00:00
l10n daemon script
3b9337d2a8 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"
2024-01-16 01:12:42 +00:00
James Graham
85cda8ffa7 Current Room Messages
Make sure that message delegates are getting the room object directly rather than requiring the assumption that currentRoom is declared somewhere higher up.
2024-01-15 19:47:50 +00:00
l10n daemon script
f1efc1f17d GIT_SILENT Sync po/docbooks with svn 2024-01-15 01:19:09 +00:00
James Graham
0486fa61cd NeoChatConnection signals
Move the signal connects to a function and call from both constructors
2024-01-14 12:25:53 +00:00
Joshua Goins
2247a2a7af Make the search message dialog header way prettier, like it is in KCMs
I think I've heard of this before...
2024-01-14 01:36:59 +00:00
Joshua Goins
08a0fbfd6b Add missing thread roles in SearchModel
This fixes the message search so it works again!
2024-01-14 01:34:43 +00:00
l10n daemon script
898b993b94 GIT_SILENT Sync po/docbooks with svn 2024-01-14 01:30:05 +00:00
James Graham
77e366b179 Why can't we be friends
Update the UX to refer to structure direct chats as friends. The direct chats are pulled into their own tab in the space drawer.

The `UserDetailDialog` is also updated to check whether a direct chat already exists and if not ask to invite as friend.

![image](/uploads/67f13fa8558e704e0acaf7c60e135bbc/image.png)
2024-01-13 21:38:43 +00:00
Tobias Fella
981edc9cf7 Refactor proxy configuration and move to separate file 2024-01-13 17:39:56 +01:00
Tobias Fella
d45aa14348 Refactor some code around connection handling 2024-01-13 13:00:29 +00:00
Tobias Fella
4926488d49 Move notifications button to space drawer.
Since this means that the space drawer can no longer be hidden when there are no spaces,
also make it less empty by adding a button for creating new spaces.
More things will come in the future.

BUG: 479051
2024-01-13 11:28:25 +01:00
l10n daemon script
dcc1935150 GIT_SILENT Sync po/docbooks with svn 2024-01-13 01:24:17 +00:00
Tobias Fella
55364a8eb8 Add basic Itinerary integration
After downloading a file, the model calls the extractor and uses the
JSON to show some basic information about the content and allows to import
the data to Itinerary. This is entirely runtime-optional; no build-time dependencies
are required and nothing changes if the extractor isn't available.
2024-01-12 21:00:14 +00:00
Tobias Fella
70bb06715f Don't crash when calling directChatRemoteUser in something that isn't a direct chat
Can happen e.g. in gammaray
2024-01-12 16:32:49 +01:00
James Graham
ec4aa73e37 Readonly Room
Add readonly property to a room and use it to decide whether to show chatbar, replies and edits

BUG: 479590
2024-01-12 01:59:09 +00:00
Albert Astals Cid
c1d122a717 GIT_SILENT Upgrade release service version to 24.04.70. 2024-01-11 21:36:24 +01:00
127 changed files with 20086 additions and 16335 deletions

View File

@@ -2,4 +2,6 @@
; SPDX-License-Identifier: CC0-1.0 ; SPDX-License-Identifier: CC0-1.0
[BlueprintSettings] [BlueprintSettings]
kde/applications/neochat.packageAppx = True kde/frameworks/extra-cmake-modules.version=master
kde/unreleased/kirigami-addons.version=master
libs/qt.qtMajorVersion=6

View File

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

View File

@@ -8,8 +8,8 @@ cmake_minimum_required(VERSION 3.16)
# KDE Applications version, managed by release script. # KDE Applications version, managed by release script.
set(RELEASE_SERVICE_VERSION_MAJOR "24") set(RELEASE_SERVICE_VERSION_MAJOR "24")
set(RELEASE_SERVICE_VERSION_MINOR "02") set(RELEASE_SERVICE_VERSION_MINOR "04")
set(RELEASE_SERVICE_VERSION_MICRO "2") set(RELEASE_SERVICE_VERSION_MICRO "70")
set(RELEASE_SERVICE_VERSION "${RELEASE_SERVICE_VERSION_MAJOR}.${RELEASE_SERVICE_VERSION_MINOR}.${RELEASE_SERVICE_VERSION_MICRO}") set(RELEASE_SERVICE_VERSION "${RELEASE_SERVICE_VERSION_MAJOR}.${RELEASE_SERVICE_VERSION_MINOR}.${RELEASE_SERVICE_VERSION_MICRO}")
project(NeoChat VERSION ${RELEASE_SERVICE_VERSION}) project(NeoChat VERSION ${RELEASE_SERVICE_VERSION})

View File

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

View File

@@ -59,8 +59,7 @@
<summary xml:lang="fi">Keskustelu ystäviesi kanssa Matrixissa</summary> <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="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="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 conntu amicos sur matrix</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="it">Conversa con i tuoi contatti su matrix</summary>
<summary xml:lang="ka">ესაუბრეთ მეგობრებს Matrix-ზე</summary> <summary xml:lang="ka">ესაუბრეთ მეგობრებს Matrix-ზე</summary>
<summary xml:lang="ko">Matrix를 사용하여 친구들과 대화하기</summary> <summary xml:lang="ko">Matrix를 사용하여 친구들과 대화하기</summary>
@@ -70,7 +69,7 @@
<summary xml:lang="sl">Klepet z vašimi prijatelji na matrixu</summary> <summary xml:lang="sl">Klepet z vašimi prijatelji na matrixu</summary>
<summary xml:lang="sv">Chatta med dina vänner på Matrix</summary> <summary xml:lang="sv">Chatta med dina vänner på Matrix</summary>
<summary xml:lang="ta">மேட்ரிக்ஸு மூலம் உங்கள் நண்பர்களிடம் பேசலாம்</summary> <summary xml:lang="ta">மேட்ரிக்ஸு மூலம் உங்கள் நண்பர்களிடம் பேசலாம்</summary>
<summary xml:lang="tr">Matrixte arkadaşlarınızla sohbet edin</summary> <summary xml:lang="tr">Matrix'te arkadaşlarınızla sohbet edin</summary>
<summary xml:lang="uk">Спілкуйтеся з вашими друзями у matrix</summary> <summary xml:lang="uk">Спілкуйтеся з вашими друзями у matrix</summary>
<summary xml:lang="x-test">xxChat with your friends on matrixxx</summary> <summary xml:lang="x-test">xxChat with your friends on matrixxx</summary>
<summary xml:lang="zh-CN">在 Matrix 上与朋友聊天</summary> <summary xml:lang="zh-CN">在 Matrix 上与朋友聊天</summary>
@@ -88,7 +87,6 @@ to provide a convergent experience across multiple platforms.</p>
<p xml:lang="fi">NeoChat on asiakassovellus Matrixille, hajautetulle pikaviestinyhteyskäytännölle. Sillä voi lähettää teksti-, video- ja ääniviestejä perheelle, tutuille ja ystäville. Se käyttää KDE-kehystä ja erityisesti Kirigamia tuottaakseen mukautuvan monialustaisen käyttökokemuksen.</p> <p xml:lang="fi">NeoChat on asiakassovellus Matrixille, hajautetulle pikaviestinyhteyskäytännölle. Sillä voi lähettää teksti-, video- ja ääniviestejä perheelle, tutuille ja ystäville. Se käyttää KDE-kehystä ja erityisesti Kirigamia tuottaakseen mukautuvan monialustaisen käyttökokemuksen.</p>
<p xml:lang="fr">NeoChat est un client pour le protocole Matrix, un protocole décentralisé de communications pour messagerie instantané. Il vous permet d'envoyer des messages de texte, des vidéos et des fichiers audio à votre famille, vos collègues et vos amis. Il utilise les environnements de développement et plus précisément Kirigami pour fournir une expérience convergente sur plusieurs plate-formes. </p> <p xml:lang="fr">NeoChat est un client pour le protocole Matrix, un protocole décentralisé de communications pour messagerie instantané. Il vous permet d'envoyer des messages de texte, des vidéos et des fichiers audio à votre famille, vos collègues et vos amis. Il utilise les environnements de développement et plus précisément Kirigami pour fournir une expérience convergente sur plusieurs plate-formes. </p>
<p xml:lang="gl">NeoChat é un cliente para Matrix, o protocolo de comunicación descentralizada para mensaxaría instantánea. Podes enviar mensaxes de texto, vídeos e ficheiros de son á túa familia, colegas e amizades. Usas infraestruturas de KDE e principalmente Kirigami para proporcionar unha experiencia de uso converxente para varias plataformas.</p> <p xml:lang="gl">NeoChat é un cliente para Matrix, o protocolo de comunicación descentralizada para mensaxaría instantánea. Podes enviar mensaxes de texto, vídeos e ficheiros de son á túa familia, colegas e amizades. Usas infraestruturas de KDE e principalmente Kirigami para proporcionar unha experiencia de uso converxente para varias plataformas.</p>
<p xml:lang="hu">A NeoChat egy kliens a Matrixhoz, az azonnali üzenetküldés decentralizált komunikációs protokolljához. Szöveges üzeneteket, videókat és hangfájlokat küldhet családjának, kollégáinak és barátainak. A KDE keretrendszert használja, a Kirigaminak köszönhetően konvergens élményt nyújt több platformon is.</p>
<p xml:lang="ia">NeoChat es un cliente per Matrix, le protocollo de communication decentralisate per messager instantanee. Illo te permitte inviar messager de texto, files de video e audio a tu familia, collegas e amicos usante. Illo usa KDE frameworks e super toto Kirigamii forni un experientia convergente trans platteforme multiple.</p> <p xml:lang="ia">NeoChat es un cliente per Matrix, le protocollo de communication decentralisate per messager instantanee. Illo te permitte inviar messager de texto, files de video e audio a tu familia, collegas e amicos usante. Illo usa KDE frameworks e super toto Kirigamii forni un experientia convergente trans platteforme multiple.</p>
<p xml:lang="it">NeoChat è un client per Matrix, il protocollo di comunicazione decentralizzato per la messaggistica istantanea. Ti consente di inviare messaggi di testo, video e file audio a familiari, colleghi e amici. Utilizza i framework KDE e in particolare Kirigami per fornire un'esperienza convergente su più piattaforme.</p> <p xml:lang="it">NeoChat è un client per Matrix, il protocollo di comunicazione decentralizzato per la messaggistica istantanea. Ti consente di inviare messaggi di testo, video e file audio a familiari, colleghi e amici. Utilizza i framework KDE e in particolare Kirigami per fornire un'esperienza convergente su più piattaforme.</p>
<p xml:lang="ka">NeoChat არის Matrix კლიენტი. ის საშუალებას გაძლევთ გაგზავნოთ ტექსტური შეტყობინებები, ვიდეოები და აუდიო ფაილები თქვენს ოჯახს, კოლეგებსა და მეგობრებს მატრიქსის პროტოკოლის გამოყენებით.</p> <p xml:lang="ka">NeoChat არის Matrix კლიენტი. ის საშუალებას გაძლევთ გაგზავნოთ ტექსტური შეტყობინებები, ვიდეოები და აუდიო ფაილები თქვენს ოჯახს, კოლეგებსა და მეგობრებს მატრიქსის პროტოკოლის გამოყენებით.</p>
@@ -114,7 +112,6 @@ to provide a convergent experience across multiple platforms.</p>
<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="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="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="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="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="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> <p xml:lang="ka">NeoChat-ი მიზნად ისახავს Matrix სპეციფიკაციის სრული განხორციელება ჰქონდეს. როგორც ასეთი, ყველაფერი მიმდინარე სპეციფიკაციიდან, VoIP-ის, ძაფებისა და გამჭოლი დაშიფვრის ზოგიერთი ასპექტის გარდა, მხარდაჭერილია. შეძლება ასევე იყოს მცირე ლაფსუსებიც იმის გამო, რომ Matrix-ის სპეციფიკაცია მუდმივად ვითარგდება, მაგრამ ჩვენი მიზანი მისი სრული მხარდაჭერაა.</p>
@@ -125,7 +122,7 @@ to provide a convergent experience across multiple platforms.</p>
<p xml:lang="pt">O NeoChat pretende ser uma aplicação completa para a especificação do Matrix. Como tal, tudo o que existe na especificação estável actual, com as notáveis excepções do VoIP, tópicos e alguns aspectos da Encriptação Ponto-a-Ponto, são suportados. Existem mais algumas omissões, devido ao facto que a norma do Matrix está em constante evolução, mas o objectivo continua a ser oferecer o suporte eventual para a norma por inteiro.</p> <p xml:lang="pt">O NeoChat pretende ser uma aplicação completa para a especificação do Matrix. Como tal, tudo o que existe na especificação estável actual, com as notáveis excepções do VoIP, tópicos e alguns aspectos da Encriptação Ponto-a-Ponto, são suportados. Existem mais algumas omissões, devido ao facto que a norma do Matrix está em constante evolução, mas o objectivo continua a ser oferecer o suporte eventual para a norma por inteiro.</p>
<p xml:lang="sl">Neochat cilja, da bi bila popolna aplikacija po specifikaciji Matrixa. Kot takšna vsebuje vse v trenutni stabilni specifikaciji z pomembnimi izjemami pri VoIP, nitih in nekaterih vidikov šifriranja od konca do konca. Obstaja nekaj drugih manjših opustitev zaradi dejstva, da se specifikacija Matrix nenehno razvija, vendar cilj ostaja zagotoviti morebitno podporo celotni specifikaciji.</p> <p xml:lang="sl">Neochat cilja, da bi bila popolna aplikacija po specifikaciji Matrixa. Kot takšna vsebuje vse v trenutni stabilni specifikaciji z pomembnimi izjemami pri VoIP, nitih in nekaterih vidikov šifriranja od konca do konca. Obstaja nekaj drugih manjših opustitev zaradi dejstva, da se specifikacija Matrix nenehno razvija, vendar cilj ostaja zagotoviti morebitno podporo celotni specifikaciji.</p>
<p xml:lang="sv">NeoChat har som mål att vara ett fullständigt program enligt Matrix-specifikationen. Som sådant stöds allt i den nuvarande stabila specifikationen, med de nämnvärda undantagen VoIP, trådar och några aspekter av kryptering hela vägen. Det finns några ytterligare utelämnanden på grund av att Matrix-specifikationen hela tiden utvecklas, men målet förblir att till slut erbjuda stöd för hela specifikationen.</p> <p xml:lang="sv">NeoChat har som mål att vara ett fullständigt program enligt Matrix-specifikationen. Som sådant stöds allt i den nuvarande stabila specifikationen, med de nämnvärda undantagen VoIP, trådar och några aspekter av kryptering hela vägen. Det finns några ytterligare utelämnanden på grund av att Matrix-specifikationen hela tiden utvecklas, men målet förblir att till slut erbjuda stöd för hela specifikationen.</p>
<p xml:lang="tr">NeoChat, Matrix belirtimi için tam özellikli bir uygulama olmayı hedefler. Bu nedenle; VoIP, ileti zincirleri ve Uçtan Uca Şifrelemenin bazı yönleri gibi dikkate değer istisnalar dışında var olan kararlı belirtimdeki her şey desteklenir. Matrix belirtiminin sürekli gelişmesi nedeniyle birkaç küçük eksiklik daha var; ancak amaç tüm belirtim için nihai destek sağlamak olmayı sürdürüyor.</p> <p xml:lang="tr">NeoChat, Matrix belirtimi için tam özellikli bir uygulama olmayı hedefler. Bu nedenle; VoIP, ileti zincirleri ve Uçtan Uca Şifreleme'nin bazı yönleri gibi dikkate değer istisnalar dışında var olan kararlı belirtimdeki her şey desteklenir. Matrix belirtiminin sürekli gelişmesi nedeniyle birkaç küçük eksiklik daha var; ancak amaç tüm belirtim için nihai destek sağlamak olmayı sürdürüyor.</p>
<p xml:lang="uk">Метою створення NeoChat є повноцінна реалізація програми для специфікації Matrix. Як наслідок, реалізовано усе у поточній стабільній специфікації, окрім голосового інтернет-зв'язку, потоків та деяких аспектів міжвузлового шифрування. Є також декілька інших незначних прогалин через те, що специфікація Matrix постійно змінюється, але метою лишається повна підтримка специфікації.</p> <p xml:lang="uk">Метою створення NeoChat є повноцінна реалізація програми для специфікації Matrix. Як наслідок, реалізовано усе у поточній стабільній специфікації, окрім голосового інтернет-зв'язку, потоків та деяких аспектів міжвузлового шифрування. Є також декілька інших незначних прогалин через те, що специфікація Matrix постійно змінюється, але метою лишається повна підтримка специфікації.</p>
<p xml:lang="x-test">xxNeoChat 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.xx</p> <p xml:lang="x-test">xxNeoChat 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.xx</p>
<p xml:lang="zh-TW">NeoChat 以完整支援 Matrix 標準為目標,因此目前穩定版標準除了 VoIP、對話串與端對端加密的某些部分以外的所有部分都有支援。其他部分還有一些較小的不支援的部分這是因為 Matrix 標準隨時都在改進,但目標仍然時最終提供整個標準的完整支援。</p> <p xml:lang="zh-TW">NeoChat 以完整支援 Matrix 標準為目標,因此目前穩定版標準除了 VoIP、對話串與端對端加密的某些部分以外的所有部分都有支援。其他部分還有一些較小的不支援的部分這是因為 Matrix 標準隨時都在改進,但目標仍然時最終提供整個標準的完整支援。</p>
@@ -140,7 +137,6 @@ to provide a convergent experience across multiple platforms.</p>
<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="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="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="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="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="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> <p xml:lang="ka">Matrix-ის სპეციფიკაციის განვითარების ბუნების გამო NeoChat-ს ასევე აქვს უამრავი არასტაბილური ფუნქციაც. ახლა ისინია:</p>
@@ -168,7 +164,6 @@ to provide a convergent experience across multiple platforms.</p>
<li xml:lang="fi">Kyselyt MSC3381</li> <li xml:lang="fi">Kyselyt MSC3381</li>
<li xml:lang="fr">Sondages - MSC3381</li> <li xml:lang="fr">Sondages - MSC3381</li>
<li xml:lang="gl">Enquisas — 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="ia">Inquestas - MSC3381</li>
<li xml:lang="it">Sondaggi - MSC3381</li> <li xml:lang="it">Sondaggi - MSC3381</li>
<li xml:lang="ka">Polls - MSC3381</li> <li xml:lang="ka">Polls - MSC3381</li>
@@ -180,7 +175,7 @@ to provide a convergent experience across multiple platforms.</p>
<li xml:lang="sl">Polls - MSC3381</li> <li xml:lang="sl">Polls - MSC3381</li>
<li xml:lang="sv">Polls - MSC3381</li> <li xml:lang="sv">Polls - MSC3381</li>
<li xml:lang="ta">வாக்கெடுப்புகள் - MSC3381</li> <li xml:lang="ta">வாக்கெடுப்புகள் - MSC3381</li>
<li xml:lang="tr">Anketler MSC3381</li> <li xml:lang="tr">Anketler - MSC3381</li>
<li xml:lang="uk">Опитування - MSC3381</li> <li xml:lang="uk">Опитування - MSC3381</li>
<li xml:lang="x-test">xxPolls - MSC3381xx</li> <li xml:lang="x-test">xxPolls - MSC3381xx</li>
<li xml:lang="zh-TW">投票 - MSC3381</li> <li xml:lang="zh-TW">投票 - MSC3381</li>
@@ -195,7 +190,6 @@ to provide a convergent experience across multiple platforms.</p>
<li xml:lang="fi">Tarrapakkaukset MSC2545</li> <li xml:lang="fi">Tarrapakkaukset MSC2545</li>
<li xml:lang="fr">Paquets d'auto-collants - MSC2545</li> <li xml:lang="fr">Paquets d'auto-collants - MSC2545</li>
<li xml:lang="gl">Paquetes de adhesivos — 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="ia">Etiquetta gummate (sticker) -MSC2545</li>
<li xml:lang="it">Pacchetti di adesivi - MSC2545</li> <li xml:lang="it">Pacchetti di adesivi - MSC2545</li>
<li xml:lang="ka">სტიკერების პაკეტები - MSC2545</li> <li xml:lang="ka">სტიკერების პაკეტები - MSC2545</li>
@@ -207,7 +201,7 @@ to provide a convergent experience across multiple platforms.</p>
<li xml:lang="sl">Sticker Packs - MSC2545</li> <li xml:lang="sl">Sticker Packs - MSC2545</li>
<li xml:lang="sv">Sticker Packs - MSC2545</li> <li xml:lang="sv">Sticker Packs - MSC2545</li>
<li xml:lang="ta">ஒட்டி தொகுப்புகள் - MSC2545</li> <li xml:lang="ta">ஒட்டி தொகுப்புகள் - MSC2545</li>
<li xml:lang="tr">Yapışkan Paketleri MSC2545</li> <li xml:lang="tr">Yapışkan Paketleri - MSC2545</li>
<li xml:lang="uk">Пакунки наліпок - MSC2545</li> <li xml:lang="uk">Пакунки наліпок - MSC2545</li>
<li xml:lang="x-test">xxSticker Packs - MSC2545xx</li> <li xml:lang="x-test">xxSticker Packs - MSC2545xx</li>
<li xml:lang="zh-TW">貼圖包 - MSC2545</li> <li xml:lang="zh-TW">貼圖包 - MSC2545</li>
@@ -222,7 +216,6 @@ to provide a convergent experience across multiple platforms.</p>
<li xml:lang="fi">Sijaintitapahtumat MSC3488</li> <li xml:lang="fi">Sijaintitapahtumat MSC3488</li>
<li xml:lang="fr">Événements de lieu - 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="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="ia">Eventos de Location - MSC3488</li>
<li xml:lang="it">Località eventi - MSC3488</li> <li xml:lang="it">Località eventi - MSC3488</li>
<li xml:lang="ka">მდებარეობის მოვლენები - MSC3488</li> <li xml:lang="ka">მდებარეობის მოვლენები - MSC3488</li>
@@ -234,7 +227,7 @@ to provide a convergent experience across multiple platforms.</p>
<li xml:lang="sl">Location Events - MSC3488</li> <li xml:lang="sl">Location Events - MSC3488</li>
<li xml:lang="sv">Location Events - MSC3488</li> <li xml:lang="sv">Location Events - MSC3488</li>
<li xml:lang="ta">இட நிகழ்வுகள் - MSC3488</li> <li xml:lang="ta">இட நிகழ்வுகள் - MSC3488</li>
<li xml:lang="tr">Konum Etkinlikleri MSC3488</li> <li xml:lang="tr">Konum Etkinlikleri - MSC3488</li>
<li xml:lang="uk">Місцеві зустрічі - MSC3488</li> <li xml:lang="uk">Місцеві зустрічі - MSC3488</li>
<li xml:lang="x-test">xxLocation Events - MSC3488xx</li> <li xml:lang="x-test">xxLocation Events - MSC3488xx</li>
<li xml:lang="zh-TW">位置事件 - MSC3488</li> <li xml:lang="zh-TW">位置事件 - MSC3488</li>
@@ -245,44 +238,11 @@ to provide a convergent experience across multiple platforms.</p>
<categories> <categories>
<category>Network</category> <category>Network</category>
</categories> </categories>
<developer_name>The KDE Community</developer_name> <developer>
<developer_name xml:lang="ar">مجتمع كِيدِي</developer_name> <id>kde.org</id>
<developer_name xml:lang="az">KDE Cəmiyyəti</developer_name> <name>The KDE Community</name>
<developer_name xml:lang="ca">La comunitat KDE</developer_name> <url>https://kde.org</url>
<developer_name xml:lang="ca-valencia">La comunitat KDE</developer_name> </developer>
<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="eo">La KDE-Komunumo</developer_name>
<developer_name xml:lang="es">La comunidad KDE</developer_name>
<developer_name xml:lang="eu">KDE komunitatea</developer_name>
<developer_name xml:lang="fi">KDE-yhteisö</developer_name>
<developer_name xml:lang="fr">La communauté de KDE</developer_name>
<developer_name xml:lang="gl">A comunidade KDE</developer_name>
<developer_name xml:lang="hu">A KDE Közösség</developer_name>
<developer_name xml:lang="ia">Le communitate de KDE</developer_name>
<developer_name xml:lang="id">Komunitas KDE</developer_name>
<developer_name xml:lang="ie">Li comunité de KDE</developer_name>
<developer_name xml:lang="it">La comunità KDE</developer_name>
<developer_name xml:lang="ka">KDE-ის საზოგადოება</developer_name>
<developer_name xml:lang="ko">KDE 커뮤니티</developer_name>
<developer_name xml:lang="nl">De KDE gemeenschap</developer_name>
<developer_name xml:lang="nn">KDE-fellesskapet</developer_name>
<developer_name xml:lang="pa">ਕੇਡੀਈ ਕਮਿਊਨਟੀ</developer_name>
<developer_name xml:lang="pl">Społeczność KDE</developer_name>
<developer_name xml:lang="pt">A Comunidade do KDE</developer_name>
<developer_name xml:lang="pt-BR">A comunidade KDE</developer_name>
<developer_name xml:lang="ru">Сообщество KDE</developer_name>
<developer_name xml:lang="sk">KDE Komunita</developer_name>
<developer_name xml:lang="sl">Skupnost KDE</developer_name>
<developer_name xml:lang="sv">KDE-gemenskapen</developer_name>
<developer_name xml:lang="ta">கே.டீ.யீ. சமூகம்</developer_name>
<developer_name xml:lang="tr">KDE Topluluğu</developer_name>
<developer_name xml:lang="uk">Спільнота KDE</developer_name>
<developer_name xml:lang="x-test">xxThe KDE Communityxx</developer_name>
<developer_name xml:lang="zh-CN">KDE 社区</developer_name>
<developer_name xml:lang="zh-TW">KDE 社群</developer_name>
<metadata_license>CC0-1.0</metadata_license> <metadata_license>CC0-1.0</metadata_license>
<project_license>GPL-3.0</project_license> <project_license>GPL-3.0</project_license>
<custom> <custom>
@@ -314,7 +274,6 @@ to provide a convergent experience across multiple platforms.</p>
<caption xml:lang="fi">Päänäkymä, jossa huoneluettelo, keskustelu ja huoneen tiedot</caption> <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="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="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="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="it">Vista principale con elenco delle stanze, chat e informazioni sulla stanza</caption>
<caption xml:lang="ka">მთავარი ხედი სურათების სიით, ჩატით და ოთახის ინფორმაციით</caption> <caption xml:lang="ka">მთავარი ხედი სურათების სიით, ჩატით და ოთახის ინფორმაციით</caption>
@@ -344,7 +303,6 @@ to provide a convergent experience across multiple platforms.</p>
<caption xml:lang="fi">Kirjautumisnäkymä</caption> <caption xml:lang="fi">Kirjautumisnäkymä</caption>
<caption xml:lang="fr">Écran de connexion</caption> <caption xml:lang="fr">Écran de connexion</caption>
<caption xml:lang="gl">Pantalla de identificación.</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="ia">Schermo de accesso</caption>
<caption xml:lang="it">Schermata di accesso</caption> <caption xml:lang="it">Schermata di accesso</caption>
<caption xml:lang="ka">შესვლის ეკრანი</caption> <caption xml:lang="ka">შესვლის ეკრანი</caption>
@@ -366,26 +324,6 @@ to provide a convergent experience across multiple platforms.</p>
<content_attribute id="social-chat">intense</content_attribute> <content_attribute id="social-chat">intense</content_attribute>
</content_rating> </content_rating>
<releases> <releases>
<release version="24.02.2" date="2024-04-11"/>
<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>
<p>In the newest version, when launching the app, you will get a welcome page that lets you choose which account you want to use and lets you log in to other accounts. The welcome screen will also warn you when NeoChat cannot load an account.</p>
<p>NeoChat will also let you register a new account directly from the app itself. Deactivating your Matrix account is also possible from within NeoChat.</p>
<p>Spaces are a relatively new feature of Matrix that let you group chat channels together. This is used to improve the discoverability of rooms, manage large communities, or just tidy all the channels you are in. You can now do all this without leaving NeoChat.</p>
<p>NeoChat won't let you miss any new notifications anymore. We added a new page that includes all your recent notifications and when NeoChat is closed, you will still be able to receive push notifications. The main timeline will let you know when more messages are loading and when you reach the end of it.</p>
<p>More NeoChat Goodies</p>
<ul>
<li>QR Codes to share contacts</li>
<li>Improved room upgrades</li>
<li>Added button to reject invitation and ignore user</li>
<li>Display device security details</li>
<li>Added room security settings</li>
</ul>
</description>
</release>
<release version="23.08.5" date="2024-02-15"/>
<release version="23.08.4" date="2023-12-07"/> <release version="23.08.4" date="2023-12-07"/>
<release version="23.08.3" date="2023-11-09"/> <release version="23.08.3" date="2023-11-09"/>
<release version="23.08.2" date="2023-10-12"/> <release version="23.08.2" date="2023-10-12"/>

View File

@@ -65,7 +65,7 @@ GenericName[ie]=Cliente de Matrix
GenericName[it]=Client Matrix GenericName[it]=Client Matrix
GenericName[ka]=Matrix -ის კლიენტი GenericName[ka]=Matrix -ის კლიენტი
GenericName[ko]=Matrix 클라이언트 GenericName[ko]=Matrix 클라이언트
GenericName[lt]=Matrix kliento programą GenericName[lt]=Matrix kliento programa
GenericName[nl]=Matrix-client GenericName[nl]=Matrix-client
GenericName[nn]=Matrix-klient GenericName[nn]=Matrix-klient
GenericName[pa]=ਮੈਟਰਿਕਸ ਕਲਾਈਂਟ GenericName[pa]=ਮੈਟਰਿਕਸ ਕਲਾਈਂਟ

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

@@ -144,6 +144,10 @@ add_library(neochat STATIC
models/timelinemodel.cpp models/timelinemodel.cpp
models/timelinemodel.h models/timelinemodel.h
enums/pushrule.h enums/pushrule.h
models/itinerarymodel.cpp
models/itinerarymodel.h
proxycontroller.cpp
proxycontroller.h
) )
qt_add_qml_module(neochat URI org.kde.neochat NO_PLUGIN qt_add_qml_module(neochat URI org.kde.neochat NO_PLUGIN
@@ -161,11 +165,10 @@ qt_add_qml_module(neochat URI org.kde.neochat NO_PLUGIN
qml/UserInfoDesktop.qml qml/UserInfoDesktop.qml
qml/RoomPage.qml qml/RoomPage.qml
qml/RoomWindow.qml qml/RoomWindow.qml
qml/JoinRoomPage.qml qml/ExploreRoomsPage.qml
qml/ManualRoomDialog.qml qml/ManualRoomDialog.qml
qml/ExplorerDelegate.qml qml/ExplorerDelegate.qml
qml/InviteUserPage.qml qml/InviteUserPage.qml
qml/StartChatPage.qml
qml/ImageEditorPage.qml qml/ImageEditorPage.qml
qml/WelcomePage.qml qml/WelcomePage.qml
qml/General.qml qml/General.qml
@@ -267,7 +270,7 @@ qt_add_qml_module(neochat URI org.kde.neochat NO_PLUGIN
qml/EmojiTonesPicker.qml qml/EmojiTonesPicker.qml
qml/EmojiDelegate.qml qml/EmojiDelegate.qml
qml/EmojiGrid.qml qml/EmojiGrid.qml
qml/SearchPage.qml qml/RoomSearchPage.qml
qml/LocationDelegate.qml qml/LocationDelegate.qml
qml/LocationChooser.qml qml/LocationChooser.qml
qml/TimelineView.qml qml/TimelineView.qml
@@ -298,11 +301,16 @@ qt_add_qml_module(neochat URI org.kde.neochat NO_PLUGIN
qml/NotificationsView.qml qml/NotificationsView.qml
qml/LoadingDelegate.qml qml/LoadingDelegate.qml
qml/TimelineEndDelegate.qml qml/TimelineEndDelegate.qml
qml/SearchPage.qml
qml/ServerComboBox.qml
qml/UserSearchPage.qml
RESOURCES RESOURCES
qml/confetti.png qml/confetti.png
qml/glowdot.png qml/glowdot.png
) )
configure_file(config-neochat.h.in ${CMAKE_CURRENT_BINARY_DIR}/config-neochat.h)
if(WIN32) if(WIN32)
set_target_properties(neochat PROPERTIES OUTPUT_NAME "neochatlib") set_target_properties(neochat PROPERTIES OUTPUT_NAME "neochatlib")
endif() endif()
@@ -316,6 +324,15 @@ ecm_qt_declare_logging_category(neochat
EXPORT NEOCHAT EXPORT NEOCHAT
) )
ecm_qt_declare_logging_category(neochat
HEADER "publicroomlist_logging.h"
IDENTIFIER "PublicRoomList"
CATEGORY_NAME "org.kde.neochat.publicroomlistmodel"
DESCRIPTION "Neochat: publicroomlistmodel"
DEFAULT_SEVERITY Info
EXPORT NEOCHAT
)
ecm_qt_declare_logging_category(neochat ecm_qt_declare_logging_category(neochat
HEADER "eventhandler_logging.h" HEADER "eventhandler_logging.h"
IDENTIFIER "EventHandling" IDENTIFIER "EventHandling"
@@ -365,10 +382,6 @@ if (NOT ANDROID AND NOT WIN32 AND NOT APPLE)
target_compile_definitions(neochat PUBLIC -DHAVE_RUNNER) target_compile_definitions(neochat PUBLIC -DHAVE_RUNNER)
target_compile_definitions(neochat PUBLIC -DHAVE_X11) target_compile_definitions(neochat PUBLIC -DHAVE_X11)
target_sources(neochat PRIVATE runner.cpp) target_sources(neochat PRIVATE runner.cpp)
if (TARGET KUnifiedPush)
target_sources(neochat PRIVATE fakerunner.cpp)
endif()
endif() endif()
target_include_directories(neochat PRIVATE ${CMAKE_BINARY_DIR} ${CMAKE_CURRENT_SOURCE_DIR}/models ${CMAKE_CURRENT_SOURCE_DIR}/enums) target_include_directories(neochat PRIVATE ${CMAKE_BINARY_DIR} ${CMAKE_CURRENT_SOURCE_DIR}/models ${CMAKE_CURRENT_SOURCE_DIR}/enums)
@@ -505,7 +518,7 @@ if(NOT ANDROID)
set_target_properties(neochat-app PROPERTIES OUTPUT_NAME "neochat") set_target_properties(neochat-app PROPERTIES OUTPUT_NAME "neochat")
endif() endif()
if(TARGET KF6::DBusAddons AND NOT WIN32) if(TARGET KF6::DBusAddons)
target_link_libraries(neochat PUBLIC KF6::DBusAddons) target_link_libraries(neochat PUBLIC KF6::DBusAddons)
target_compile_definitions(neochat PUBLIC -DHAVE_KDBUSADDONS) target_compile_definitions(neochat PUBLIC -DHAVE_KDBUSADDONS)
endif() endif()

View File

@@ -14,7 +14,6 @@
#include "models/actionsmodel.h" #include "models/actionsmodel.h"
#include "neochatconfig.h" #include "neochatconfig.h"
#include "texthandler.h" #include "texthandler.h"
#include "utils.h"
using namespace Quotient; using namespace Quotient;
@@ -145,26 +144,6 @@ void ActionsHandler::handleMessage(const QString &text, QString handledText, Cha
return; return;
} }
// We want to add back the <mx-reply> if it's in the original message but not in the edit, to preserve the reply.
for (auto it = m_room->messageEvents().crbegin(); it != m_room->messageEvents().crend(); it++) {
if (const auto event = eventCast<const RoomMessageEvent>(&**it)) {
if (event->senderId() == m_room->localUser()->id() && event->hasTextContent()) {
QString originalString;
if (event->content()) {
originalString = static_cast<const Quotient::EventContent::TextContent *>(event->content())->body;
} else {
originalString = event->plainBody();
}
const QRegularExpression exp(TextRegex::removeRichReply);
const auto match = exp.match(originalString);
if (match.hasCaptured(0) && !handledText.contains(TextRegex::removeRichReply)) {
handledText.prepend(match.captured(0));
}
}
}
}
m_room->postMessage(text, handledText, messageType, chatBarCache->replyId(), chatBarCache->editId(), chatBarCache->threadId()); m_room->postMessage(text, handledText, messageType, chatBarCache->replyId(), chatBarCache->editId(), chatBarCache->threadId());
} }

View File

@@ -3,7 +3,6 @@
#include "chatbarcache.h" #include "chatbarcache.h"
#include "chatdocumenthandler.h"
#include "eventhandler.h" #include "eventhandler.h"
#include "neochatroom.h" #include "neochatroom.h"
@@ -119,7 +118,7 @@ QString ChatBarCache::relationMessage() const
eventhandler.setRoom(room); eventhandler.setRoom(room);
if (auto event = room->findInTimeline(m_relationId); event != room->historyEdge()) { if (auto event = room->findInTimeline(m_relationId); event != room->historyEdge()) {
eventhandler.setEvent(&**event); eventhandler.setEvent(&**event);
return eventhandler.getMarkdownBody(); return eventhandler.getPlainBody();
} }
return {}; return {};
} }
@@ -165,54 +164,6 @@ QList<Mention> *ChatBarCache::mentions()
return &m_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 QString ChatBarCache::savedText() const
{ {
return m_savedText; return m_savedText;

View File

@@ -5,11 +5,8 @@
#include <QObject> #include <QObject>
#include <QQmlEngine> #include <QQmlEngine>
#include <QQuickTextDocument>
#include <QTextCursor> #include <QTextCursor>
class ChatDocumentHandler;
/** /**
* @brief Defines a user mention in the current chat or edit text. * @brief Defines a user mention in the current chat or edit text.
*/ */
@@ -177,11 +174,6 @@ public:
*/ */
QList<Mention> *mentions(); 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. * @brief Get the saved chat bar text.
*/ */

8
src/config-neochat.h.in Normal file
View File

@@ -0,0 +1,8 @@
/*
SPDX-FileCopyrightText: 2024 Tobias Fella <tobias.fella@kde.org>
SPDX-License-Identifier: LGPL-2.0-or-later
*/
#pragma once
#define CMAKE_INSTALL_FULL_LIBEXECDIR_KF6 "${KDE_INSTALL_FULL_LIBEXECDIR_KF}"

View File

@@ -23,12 +23,12 @@
#include <Quotient/csapi/logout.h> #include <Quotient/csapi/logout.h>
#include <Quotient/csapi/notifications.h> #include <Quotient/csapi/notifications.h>
#include <Quotient/eventstats.h> #include <Quotient/eventstats.h>
#include <Quotient/jobs/downloadfilejob.h>
#include <Quotient/qt_connection_util.h> #include <Quotient/qt_connection_util.h>
#include "neochatconfig.h" #include "neochatconfig.h"
#include "neochatroom.h" #include "neochatroom.h"
#include "notificationsmanager.h" #include "notificationsmanager.h"
#include "proxycontroller.h"
#include "roommanager.h" #include "roommanager.h"
#if defined(Q_OS_WIN) || defined(Q_OS_MAC) #if defined(Q_OS_WIN) || defined(Q_OS_MAC)
@@ -46,7 +46,7 @@ Controller::Controller(QObject *parent)
{ {
Connection::setRoomType<NeoChatRoom>(); Connection::setRoomType<NeoChatRoom>();
setApplicationProxy(); ProxyController::instance().setApplicationProxy();
#ifndef Q_OS_ANDROID #ifndef Q_OS_ANDROID
setQuitOnLastWindowClosed(); setQuitOnLastWindowClosed();
@@ -178,12 +178,10 @@ void Controller::invokeLogin()
} }
auto connection = new NeoChatConnection(account.homeserver()); auto connection = new NeoChatConnection(account.homeserver());
m_connectionsLoading[accountId] = connection; connect(connection, &NeoChatConnection::connected, this, [this, connection] {
connect(connection, &NeoChatConnection::connected, this, [this, connection, accountId] {
connection->loadState(); connection->loadState();
addConnection(connection); addConnection(connection);
m_accountsLoading.removeAll(connection->userId()); m_accountsLoading.removeAll(connection->userId());
m_connectionsLoading.remove(accountId);
Q_EMIT accountsLoadingChanged(); Q_EMIT accountsLoadingChanged();
}); });
connect(connection, &NeoChatConnection::networkError, this, [this](const QString &error, const QString &, int, int) { connect(connection, &NeoChatConnection::networkError, this, [this](const QString &error, const QString &, int, int) {
@@ -287,28 +285,10 @@ void Controller::setActiveConnection(NeoChatConnection *connection)
if (connection == m_connection) { if (connection == m_connection) {
return; return;
} }
if (m_connection != nullptr) {
disconnect(m_connection, &NeoChatConnection::syncError, this, nullptr);
disconnect(m_connection, &NeoChatConnection::accountDataChanged, this, nullptr);
}
m_connection = connection; m_connection = connection;
if (connection != nullptr) {
connect(connection, &NeoChatConnection::requestFailed, this, [](BaseJob *job) {
if (dynamic_cast<DownloadFileJob *>(job) && job->jsonData()["errcode"_ls].toString() == "M_TOO_LARGE"_ls) {
RoomManager::instance().warning(i18n("File too large to download."), i18n("Contact your matrix server administrator for support."));
}
});
}
NeoChatConfig::self()->save();
Q_EMIT activeConnectionChanged(); Q_EMIT activeConnectionChanged();
} }
void Controller::forceRefreshTextDocument(QQuickTextDocument *textDocument, QQuickItem *item)
{
// HACK: Workaround bug QTBUG 93281
connect(textDocument->textDocument(), SIGNAL(imagesLoaded()), item, SLOT(updateWholeDocument()));
}
void Controller::listenForNotifications() void Controller::listenForNotifications()
{ {
#ifdef HAVE_KUNIFIEDPUSH #ifdef HAVE_KUNIFIEDPUSH
@@ -330,36 +310,6 @@ void Controller::listenForNotifications()
#endif #endif
} }
void Controller::setApplicationProxy()
{
NeoChatConfig *cfg = NeoChatConfig::self();
QNetworkProxy proxy;
// type match to ProxyType from neochatconfig.kcfg
switch (cfg->proxyType()) {
case 1: // HTTP
proxy.setType(QNetworkProxy::HttpProxy);
proxy.setHostName(cfg->proxyHost());
proxy.setPort(cfg->proxyPort());
proxy.setUser(cfg->proxyUser());
proxy.setPassword(cfg->proxyPassword());
break;
case 2: // SOCKS 5
proxy.setType(QNetworkProxy::Socks5Proxy);
proxy.setHostName(cfg->proxyHost());
proxy.setPort(cfg->proxyPort());
proxy.setUser(cfg->proxyUser());
proxy.setPassword(cfg->proxyPassword());
break;
case 0: // System Default
default:
// do nothing
break;
}
QNetworkProxy::setApplicationProxy(proxy);
}
bool Controller::isFlatpak() const bool Controller::isFlatpak() const
{ {
#ifdef NEOCHAT_FLATPAK #ifdef NEOCHAT_FLATPAK
@@ -380,13 +330,3 @@ void Controller::setTestMode(bool test)
{ {
testMode = test; testMode = test;
} }
void Controller::removeConnection(const QString &userId)
{
if (m_connectionsLoading.contains(userId) && m_connectionsLoading[userId]) {
auto connection = m_connectionsLoading[userId];
m_accountsLoading.removeAll(userId);
Q_EMIT accountsLoadingChanged();
SettingsGroup("Accounts"_ls).remove(userId);
}
}

View File

@@ -84,22 +84,8 @@ public:
[[nodiscard]] bool supportSystemTray() const; [[nodiscard]] bool supportSystemTray() const;
/**
* @brief Sets the QNetworkProxy for the application.
*
* @sa QNetworkProxy::setApplicationProxy
*/
Q_INVOKABLE void setApplicationProxy();
bool isFlatpak() const; bool isFlatpak() const;
/**
* @brief Force a QQuickTextDocument to refresh when images are loaded.
*
* HACK: This is a workaround for QTBUG 93281.
*/
Q_INVOKABLE void forceRefreshTextDocument(QQuickTextDocument *textDocument, QQuickItem *item);
/** /**
* @brief Start listening for notifications in dbus-activated mode. * @brief Start listening for notifications in dbus-activated mode.
* These notifications will quit the application when closed. * These notifications will quit the application when closed.
@@ -110,8 +96,6 @@ public:
static void setTestMode(bool testMode); static void setTestMode(bool testMode);
Q_INVOKABLE void removeConnection(const QString &userId);
private: private:
explicit Controller(QObject *parent = nullptr); explicit Controller(QObject *parent = nullptr);
@@ -125,7 +109,6 @@ private:
Quotient::AccountRegistry m_accountRegistry; Quotient::AccountRegistry m_accountRegistry;
QStringList m_accountsLoading; QStringList m_accountsLoading;
QMap<QString, QPointer<Quotient::Connection>> m_connectionsLoading;
QString m_endpoint; QString m_endpoint;
private Q_SLOTS: private Q_SLOTS:

View File

@@ -300,27 +300,6 @@ bool EventHandler::isHidden()
return false; return false;
} }
QString EventHandler::rawMessageBody(const Quotient::RoomMessageEvent &event)
{
if (event.hasFileContent()) {
auto fileCaption = event.content()->fileInfo()->originalName;
if (fileCaption.isEmpty()) {
fileCaption = event.plainBody();
} else if (event.content()->fileInfo()->originalName != event.plainBody()) {
fileCaption = event.plainBody() + " | "_ls + fileCaption;
}
return fileCaption;
}
QString body;
if (event.hasTextContent() && event.content()) {
body = static_cast<const MessageEventContent::TextContent *>(event.content())->body;
} else {
body = event.plainBody();
}
return body;
}
QString EventHandler::getRichBody(bool stripNewlines) const QString EventHandler::getRichBody(bool stripNewlines) const
{ {
if (m_event == nullptr) { if (m_event == nullptr) {
@@ -339,22 +318,6 @@ QString EventHandler::getPlainBody(bool stripNewlines) const
return getBody(m_event, Qt::PlainText, stripNewlines); 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 QString EventHandler::getBody(const Quotient::RoomEvent *event, Qt::TextFormat format, bool stripNewlines) const
{ {
if (event->isRedacted()) { if (event->isRedacted()) {

View File

@@ -159,14 +159,6 @@ public:
*/ */
bool isHidden(); bool isHidden();
/**
* @brief Output a string for the room message content without any formatting.
*
* This is the content of the formatted_body key if present or the body key if
* not.
*/
static QString rawMessageBody(const Quotient::RoomMessageEvent &event);
/** /**
* @brief Output a string for the message content ready for display in a rich text field. * @brief Output a string for the message content ready for display in a rich text field.
* *
@@ -199,13 +191,6 @@ public:
*/ */
QString getPlainBody(bool stripNewlines = false) const; 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. * @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

@@ -48,11 +48,6 @@
#ifdef HAVE_RUNNER #ifdef HAVE_RUNNER
#include "runner.h" #include "runner.h"
#include <QDBusConnection> #include <QDBusConnection>
#include <QDBusMetaType>
#endif
#if defined(HAVE_RUNNER) && defined(HAVE_KUNIFIEDPUSH)
#include "fakerunner.h"
#endif #endif
#ifdef Q_OS_WINDOWS #ifdef Q_OS_WINDOWS
@@ -201,14 +196,6 @@ int main(int argc, char *argv[])
// We want to be replaceable by the main client // We want to be replaceable by the main client
KDBusService service(KDBusService::Replace); 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(); Controller::listenForNotifications();
return QCoreApplication::exec(); return QCoreApplication::exec();
} }

View File

@@ -354,12 +354,17 @@ QList<ActionsModel::Action> actions{
Q_EMIT room->showMessage(NeoChatRoom::Error, i18nc("'<text>' does not look like a matrix id.", "'%1' does not look like a matrix id.", text)); Q_EMIT room->showMessage(NeoChatRoom::Error, i18nc("'<text>' does not look like a matrix id.", "'%1' does not look like a matrix id.", text));
return QString(); return QString();
} }
if (room->connection()->ignoredUsers().contains(text)) { auto user = room->connection()->users()[text];
if (room->connection()->ignoredUsers().contains(user->id())) {
Q_EMIT room->showMessage(NeoChatRoom::Info, i18nc("<username> is already ignored.", "%1 is already ignored.", text)); Q_EMIT room->showMessage(NeoChatRoom::Info, i18nc("<username> is already ignored.", "%1 is already ignored.", text));
return QString(); return QString();
} }
room->connection()->addToIgnoredUsers(room->connection()->user(text)); if (user) {
Q_EMIT room->showMessage(NeoChatRoom::Positive, i18nc("<username> is now ignored", "%1 is now ignored.", text)); room->connection()->addToIgnoredUsers(user);
Q_EMIT room->showMessage(NeoChatRoom::Positive, i18nc("<username> is now ignored", "%1 is now ignored.", text));
} else {
Q_EMIT room->showMessage(NeoChatRoom::Error, i18nc("<username> is not a known user", "%1 is not a known user.", text));
}
return QString(); return QString();
}, },
false, false,
@@ -377,12 +382,17 @@ QList<ActionsModel::Action> actions{
Q_EMIT room->showMessage(NeoChatRoom::Error, i18nc("'<text>' does not look like a matrix id.", "'%1' does not look like a matrix id.", text)); Q_EMIT room->showMessage(NeoChatRoom::Error, i18nc("'<text>' does not look like a matrix id.", "'%1' does not look like a matrix id.", text));
return QString(); return QString();
} }
if (!room->connection()->ignoredUsers().contains(text)) { auto user = room->connection()->users()[text];
Q_EMIT room->showMessage(NeoChatRoom::Info, i18nc("<username> is not ignored.", "%1 is not ignored.", text)); if (user) {
return QString(); if (!room->connection()->ignoredUsers().contains(user->id())) {
Q_EMIT room->showMessage(NeoChatRoom::Info, i18nc("<username> is not ignored.", "%1 is not ignored.", text));
return QString();
}
room->connection()->removeFromIgnoredUsers(user);
Q_EMIT room->showMessage(NeoChatRoom::Positive, i18nc("<username> is no longer ignored.", "%1 is no longer ignored.", text));
} else {
Q_EMIT room->showMessage(NeoChatRoom::Error, i18nc("<username> is not a known user", "%1 is not a known user.", text));
} }
room->connection()->removeFromIgnoredUsers(room->connection()->user(text));
Q_EMIT room->showMessage(NeoChatRoom::Positive, i18nc("<username> is no longer ignored.", "%1 is no longer ignored.", text));
return QString(); return QString();
}, },
false, false,

View File

@@ -0,0 +1,163 @@
// SPDX-FileCopyrightText: 2024 Tobias Fella <tobias.fella@kde.org>
// SPDX-License-Identifier: LGPL-2.0-or-later
#include "itinerarymodel.h"
#include <QProcess>
#include "config-neochat.h"
#ifndef Q_OS_ANDROID
#include <KIO/ApplicationLauncherJob>
#endif
ItineraryModel::ItineraryModel(QObject *parent)
: QAbstractListModel(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()) {
return {};
}
auto row = index.row();
auto data = m_data[row];
if (role == NameRole) {
if (data[QStringLiteral("@type")] == QStringLiteral("TrainReservation")) {
return data[QStringLiteral("reservationFor")][QStringLiteral("trainNumber")];
}
if (data[QStringLiteral("@type")] == QStringLiteral("LodgingReservation")) {
return data[QStringLiteral("reservationFor")][QStringLiteral("name")];
}
}
if (role == TypeRole) {
return data[QStringLiteral("@type")];
}
if (role == DepartureStationRole) {
return data[QStringLiteral("reservationFor")][QStringLiteral("departureStation")][QStringLiteral("name")];
}
if (role == ArrivalStationRole) {
return data[QStringLiteral("reservationFor")][QStringLiteral("arrivalStation")][QStringLiteral("name")];
}
if (role == DepartureTimeRole) {
const auto &time = data[QStringLiteral("reservationFor")][QStringLiteral("departureTime")];
auto dateTime = (time.isString() ? time : time[QStringLiteral("@value")]).toVariant().toDateTime();
if (const auto &timeZone = time[QStringLiteral("timezone")].toString(); timeZone.length() > 0) {
dateTime.setTimeZone(QTimeZone(timeZone.toLatin1().data()));
}
return dateTime.toString(QLocale::system().dateTimeFormat(QLocale::ShortFormat));
}
if (role == ArrivalTimeRole) {
const auto &time = data[QStringLiteral("reservationFor")][QStringLiteral("arrivalTime")];
auto dateTime = (time.isString() ? time : time[QStringLiteral("@value")]).toVariant().toDateTime();
if (const auto &timeZone = time[QStringLiteral("timezone")].toString(); timeZone.length() > 0) {
dateTime.setTimeZone(QTimeZone(timeZone.toLatin1().data()));
}
return dateTime.toString(QLocale::system().dateTimeFormat(QLocale::ShortFormat));
}
if (role == AddressRole) {
const auto &addressData = data[QStringLiteral("reservationFor")][QStringLiteral("address")];
return QStringLiteral("%1 - %2 %3 %4")
.arg(addressData[QStringLiteral("streetAddress")].toString(),
addressData[QStringLiteral("postalCode")].toString(),
addressData[QStringLiteral("addressLocality")].toString(),
addressData[QStringLiteral("addressCountry")].toString());
}
if (role == StartTimeRole) {
auto dateTime = data[QStringLiteral("checkinTime")][QStringLiteral("@value")].toVariant().toDateTime();
return dateTime.toString(QLocale::system().dateTimeFormat(QLocale::ShortFormat));
}
if (role == EndTimeRole) {
auto dateTime = data[QStringLiteral("checkoutTime")][QStringLiteral("@value")].toVariant().toDateTime();
return dateTime.toString(QLocale::system().dateTimeFormat(QLocale::ShortFormat));
}
if (role == DeparturePlatformRole) {
return data[QStringLiteral("reservationFor")][QStringLiteral("departurePlatform")];
}
if (role == ArrivalPlatformRole) {
return data[QStringLiteral("reservationFor")][QStringLiteral("arrivalPlatform")];
}
if (role == CoachRole) {
return data[QStringLiteral("reservedTicket")][QStringLiteral("ticketedSeat")][QStringLiteral("seatSection")];
}
if (role == SeatRole) {
return data[QStringLiteral("reservedTicket")][QStringLiteral("ticketedSeat")][QStringLiteral("seatNumber")];
}
return {};
}
int ItineraryModel::rowCount(const QModelIndex &parent) const
{
Q_UNUSED(parent);
return m_data.size();
}
QHash<int, QByteArray> ItineraryModel::roleNames() const
{
return {
{NameRole, "name"},
{TypeRole, "type"},
{DepartureStationRole, "departureStation"},
{ArrivalStationRole, "arrivalStation"},
{DepartureTimeRole, "departureTime"},
{ArrivalTimeRole, "arrivalTime"},
{AddressRole, "address"},
{StartTimeRole, "startTime"},
{EndTimeRole, "endTime"},
{DeparturePlatformRole, "departurePlatform"},
{ArrivalPlatformRole, "arrivalPlatform"},
{CoachRole, "coach"},
{SeatRole, "seat"},
};
}
QString ItineraryModel::path() const
{
return m_path;
}
void ItineraryModel::setPath(const QString &path)
{
if (path == m_path) {
return;
}
m_path = path;
Q_EMIT pathChanged();
loadData();
}
void ItineraryModel::loadData()
{
auto process = new QProcess(this);
process->start(QLatin1String(CMAKE_INSTALL_FULL_LIBEXECDIR_KF6) + QLatin1String("/kitinerary-extractor"), {m_path.mid(7)});
connect(process, &QProcess::finished, this, [this, process]() {
auto data = process->readAllStandardOutput();
beginResetModel();
m_data = QJsonDocument::fromJson(data).array();
endResetModel();
});
}
void ItineraryModel::sendToItinerary()
{
#ifndef Q_OS_ANDROID
auto job = new KIO::ApplicationLauncherJob(KService::serviceByDesktopName(QStringLiteral("org.kde.itinerary")));
job->setUrls({QUrl::fromLocalFile(m_path.mid(7))});
job->start();
#endif
}

View File

@@ -0,0 +1,62 @@
// SPDX-FileCopyrightText: 2024 Tobias Fella <tobias.fella@kde.org>
// SPDX-License-Identifier: LGPL-2.0-or-later
#pragma once
#include <QAbstractListModel>
#include <QPointer>
#include <QQmlEngine>
#include <QString>
#include "neochatconnection.h"
class ItineraryModel : public QAbstractListModel
{
Q_OBJECT
QML_ELEMENT
Q_PROPERTY(NeoChatConnection *connection READ connection WRITE setConnection NOTIFY connectionChanged)
Q_PROPERTY(QString path READ path WRITE setPath NOTIFY pathChanged)
public:
enum Roles {
NameRole = Qt::DisplayRole,
TypeRole,
DepartureStationRole,
ArrivalStationRole,
DepartureTimeRole,
ArrivalTimeRole,
AddressRole,
StartTimeRole,
EndTimeRole,
DeparturePlatformRole,
ArrivalPlatformRole,
CoachRole,
SeatRole,
};
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;
QHash<int, QByteArray> roleNames() const override;
QString path() const;
void setPath(const QString &path);
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

@@ -5,6 +5,8 @@
#include <Quotient/connection.h> #include <Quotient/connection.h>
#include "publicroomlist_logging.h"
using namespace Quotient; using namespace Quotient;
PublicRoomListModel::PublicRoomListModel(QObject *parent) PublicRoomListModel::PublicRoomListModel(QObject *parent)
@@ -41,7 +43,7 @@ void PublicRoomListModel::setConnection(Connection *conn)
if (job) { if (job) {
job->abandon(); job->abandon();
job = nullptr; job = nullptr;
Q_EMIT loadingChanged(); Q_EMIT searchingChanged();
} }
if (m_connection) { if (m_connection) {
@@ -50,7 +52,6 @@ void PublicRoomListModel::setConnection(Connection *conn)
Q_EMIT connectionChanged(); Q_EMIT connectionChanged();
Q_EMIT serverChanged(); Q_EMIT serverChanged();
Q_EMIT hasMoreChanged();
} }
QString PublicRoomListModel::server() const QString PublicRoomListModel::server() const
@@ -71,14 +72,13 @@ void PublicRoomListModel::setServer(const QString &value)
nextBatch = QString(); nextBatch = QString();
attempted = false; attempted = false;
rooms.clear(); rooms.clear();
Q_EMIT loadingChanged();
endResetModel(); endResetModel();
if (job) { if (job) {
job->abandon(); job->abandon();
job = nullptr; job = nullptr;
Q_EMIT loadingChanged(); Q_EMIT searchingChanged();
} }
if (m_connection) { if (m_connection) {
@@ -86,42 +86,30 @@ void PublicRoomListModel::setServer(const QString &value)
} }
Q_EMIT serverChanged(); Q_EMIT serverChanged();
Q_EMIT hasMoreChanged();
} }
QString PublicRoomListModel::keyword() const QString PublicRoomListModel::searchText() const
{ {
return m_keyword; return m_searchText;
} }
void PublicRoomListModel::setKeyword(const QString &value) void PublicRoomListModel::setSearchText(const QString &value)
{ {
if (m_keyword == value) { if (m_searchText == value) {
return; return;
} }
m_keyword = value; m_searchText = value;
Q_EMIT searchTextChanged();
beginResetModel();
nextBatch = QString(); nextBatch = QString();
attempted = false; attempted = false;
rooms.clear();
endResetModel();
if (job) { if (job) {
job->abandon(); job->abandon();
job = nullptr; job = nullptr;
Q_EMIT loadingChanged(); Q_EMIT searchingChanged();
} }
if (m_connection) {
next();
}
Q_EMIT keywordChanged();
Q_EMIT hasMoreChanged();
} }
bool PublicRoomListModel::showOnlySpaces() const bool PublicRoomListModel::showOnlySpaces() const
@@ -138,15 +126,28 @@ void PublicRoomListModel::setShowOnlySpaces(bool showOnlySpaces)
Q_EMIT showOnlySpacesChanged(); Q_EMIT showOnlySpacesChanged();
} }
void PublicRoomListModel::next(int count) void PublicRoomListModel::search(int limit)
{ {
if (count < 1) { if (limit < 1 || attempted) {
return; return;
} }
if (job) { if (job) {
qDebug() << "PublicRoomListModel: Other jobs running, ignore"; qCDebug(PublicRoomList) << "Other job running, ignore";
return;
}
next(limit);
}
void PublicRoomListModel::next(int limit)
{
if (m_connection == nullptr || limit < 1) {
return;
}
if (job) {
qCDebug(PublicRoomList) << "Other job running, ignore";
return; return;
} }
@@ -154,11 +155,17 @@ void PublicRoomListModel::next(int count)
if (m_showOnlySpaces) { if (m_showOnlySpaces) {
roomTypes += QLatin1String("m.space"); roomTypes += QLatin1String("m.space");
} }
job = m_connection->callApi<QueryPublicRoomsJob>(m_server, count, nextBatch, QueryPublicRoomsJob::Filter{m_keyword, roomTypes}); job = m_connection->callApi<QueryPublicRoomsJob>(m_server, limit, nextBatch, QueryPublicRoomsJob::Filter{m_searchText, roomTypes});
Q_EMIT loadingChanged(); Q_EMIT searchingChanged();
connect(job, &BaseJob::finished, this, [this] { connect(job, &BaseJob::finished, this, [this] {
attempted = true; if (!attempted) {
beginResetModel();
rooms.clear();
endResetModel();
attempted = true;
}
if (job->status() == BaseJob::Success) { if (job->status() == BaseJob::Success) {
nextBatch = job->nextBatch(); nextBatch = job->nextBatch();
@@ -166,14 +173,10 @@ void PublicRoomListModel::next(int count)
this->beginInsertRows({}, rooms.count(), rooms.count() + job->chunk().count() - 1); this->beginInsertRows({}, rooms.count(), rooms.count() + job->chunk().count() - 1);
rooms.append(job->chunk()); rooms.append(job->chunk());
this->endInsertRows(); this->endInsertRows();
if (job->nextBatch().isEmpty()) {
Q_EMIT hasMoreChanged();
}
} }
this->job = nullptr; this->job = nullptr;
Q_EMIT loadingChanged(); Q_EMIT searchingChanged();
}); });
} }
@@ -184,8 +187,7 @@ QVariant PublicRoomListModel::data(const QModelIndex &index, int role) const
} }
if (index.row() >= rooms.count()) { if (index.row() >= rooms.count()) {
qDebug() << "PublicRoomListModel, something's wrong: index.row() >= " qCDebug(PublicRoomList) << "something's wrong: index.row() >= rooms.count()";
"rooms.count()";
return {}; return {};
} }
auto room = rooms.at(index.row()); auto room = rooms.at(index.row());
@@ -271,12 +273,19 @@ int PublicRoomListModel::rowCount(const QModelIndex &parent) const
return rooms.count(); return rooms.count();
} }
bool PublicRoomListModel::hasMore() const bool PublicRoomListModel::canFetchMore(const QModelIndex &parent) const
{ {
return !(attempted && nextBatch.isEmpty()); Q_UNUSED(parent)
return !nextBatch.isEmpty();
} }
bool PublicRoomListModel::loading() const void PublicRoomListModel::fetchMore(const QModelIndex &parent)
{
Q_UNUSED(parent)
next();
}
bool PublicRoomListModel::searching() const
{ {
return job != nullptr; return job != nullptr;
} }

View File

@@ -41,9 +41,9 @@ class PublicRoomListModel : public QAbstractListModel
Q_PROPERTY(QString server READ server WRITE setServer NOTIFY serverChanged) Q_PROPERTY(QString server READ server WRITE setServer NOTIFY serverChanged)
/** /**
* @brief The filter keyword for the list of public rooms. * @brief The text to search the public room list for.
*/ */
Q_PROPERTY(QString keyword READ keyword WRITE setKeyword NOTIFY keywordChanged) Q_PROPERTY(QString searchText READ searchText WRITE setSearchText NOTIFY searchTextChanged)
/** /**
* @brief Whether only space rooms should be shown. * @brief Whether only space rooms should be shown.
@@ -51,14 +51,9 @@ class PublicRoomListModel : public QAbstractListModel
Q_PROPERTY(bool showOnlySpaces READ showOnlySpaces WRITE setShowOnlySpaces NOTIFY showOnlySpacesChanged) Q_PROPERTY(bool showOnlySpaces READ showOnlySpaces WRITE setShowOnlySpaces NOTIFY showOnlySpacesChanged)
/** /**
* @brief Whether the model has more items to load. * @brief Whether the model is searching.
*/ */
Q_PROPERTY(bool hasMore READ hasMore NOTIFY hasMoreChanged) Q_PROPERTY(bool searching READ searching NOTIFY searchingChanged)
/**
* @biref Whether the model is still loading.
*/
Q_PROPERTY(bool loading READ loading NOTIFY loadingChanged)
public: public:
/** /**
@@ -105,31 +100,38 @@ public:
[[nodiscard]] QString server() const; [[nodiscard]] QString server() const;
void setServer(const QString &value); void setServer(const QString &value);
[[nodiscard]] QString keyword() const; [[nodiscard]] QString searchText() const;
void setKeyword(const QString &value); void setSearchText(const QString &searchText);
[[nodiscard]] bool showOnlySpaces() const; [[nodiscard]] bool showOnlySpaces() const;
void setShowOnlySpaces(bool showOnlySpaces); void setShowOnlySpaces(bool showOnlySpaces);
[[nodiscard]] bool hasMore() const; [[nodiscard]] bool searching() const;
[[nodiscard]] bool loading() const; /**
* @brief Search the room directory.
*
* @param limit the maximum number of rooms to load.
*/
Q_INVOKABLE void search(int limit = 50);
private:
QPointer<Quotient::Connection> m_connection = nullptr;
QString m_server;
QString m_searchText;
bool m_showOnlySpaces = false;
/** /**
* @brief Load the next set of rooms. * @brief Load the next set of rooms.
* *
* @param count the maximum number of rooms to load. * @param limit the maximum number of rooms to load.
*/ */
Q_INVOKABLE void next(int count = 50); void next(int limit = 50);
bool canFetchMore(const QModelIndex &parent) const override;
private: void fetchMore(const QModelIndex &parent) override;
Quotient::Connection *m_connection = nullptr;
QString m_server;
QString m_keyword;
bool m_showOnlySpaces = false;
bool attempted = false; bool attempted = false;
bool m_loading = false; bool m_searching = false;
QString nextBatch; QString nextBatch;
QList<Quotient::PublicRoomsChunk> rooms; QList<Quotient::PublicRoomsChunk> rooms;
@@ -139,8 +141,7 @@ private:
Q_SIGNALS: Q_SIGNALS:
void connectionChanged(); void connectionChanged();
void serverChanged(); void serverChanged();
void keywordChanged(); void searchTextChanged();
void showOnlySpacesChanged(); void showOnlySpacesChanged();
void hasMoreChanged(); void searchingChanged();
void loadingChanged();
}; };

View File

@@ -23,7 +23,7 @@ ReactionModel::ReactionModel(const Quotient::RoomMessageEvent *event, const NeoC
{ {
if (m_event != nullptr && m_room != nullptr) { if (m_event != nullptr && m_room != nullptr) {
connect(m_room, &NeoChatRoom::updatedEvent, this, [this](const QString &eventId) { connect(m_room, &NeoChatRoom::updatedEvent, this, [this](const QString &eventId) {
if (m_event && m_event->id() == eventId) { if (m_event->id() == eventId) {
updateReactions(); updateReactions();
} }
}); });
@@ -80,7 +80,7 @@ QVariant ReactionModel::data(const QModelIndex &index, int role) const
"%2 reacted with %3", "%2 reacted with %3",
reaction.authors.count(), reaction.authors.count(),
text, text,
m_shortcodes.contains(reaction.reaction) ? m_shortcodes[reaction.reaction] : reactionText(reaction.reaction)); reactionText(reaction.reaction));
return text; return text;
} }
@@ -111,7 +111,6 @@ void ReactionModel::updateReactions()
beginResetModel(); beginResetModel();
m_reactions.clear(); m_reactions.clear();
m_shortcodes.clear();
const auto &annotations = m_room->relatedEvents(*m_event, Quotient::EventRelation::AnnotationType); const auto &annotations = m_room->relatedEvents(*m_event, Quotient::EventRelation::AnnotationType);
if (annotations.isEmpty()) { if (annotations.isEmpty()) {
@@ -126,9 +125,6 @@ void ReactionModel::updateReactions()
} }
if (const auto &e = eventCast<const Quotient::ReactionEvent>(a)) { if (const auto &e = eventCast<const Quotient::ReactionEvent>(a)) {
reactions[e->key()].append(m_room->user(e->senderId())); reactions[e->key()].append(m_room->user(e->senderId()));
if (e->contentJson()[QStringLiteral("shortcode")].toString().length()) {
m_shortcodes[e->key()] = e->contentJson()[QStringLiteral("shortcode")].toString().toHtmlEscaped();
}
} }
} }
@@ -162,18 +158,8 @@ QHash<int, QByteArray> ReactionModel::roleNames() const
}; };
} }
QString ReactionModel::reactionText(QString text) const QString ReactionModel::reactionText(const QString &text)
{ {
text = text.toHtmlEscaped();
if (text.startsWith(QStringLiteral("mxc://"))) {
static QFont font;
static int size = font.pixelSize();
if (size == -1) {
size = font.pointSizeF() * 1.333;
}
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) { const auto isEmoji = [](const QString &text) {
#ifdef HAVE_ICU #ifdef HAVE_ICU
QTextBoundaryFinder finder(QTextBoundaryFinder::Grapheme, text); QTextBoundaryFinder finder(QTextBoundaryFinder::Grapheme, text);

View File

@@ -71,9 +71,8 @@ private:
const NeoChatRoom *m_room; const NeoChatRoom *m_room;
const Quotient::RoomMessageEvent *m_event; const Quotient::RoomMessageEvent *m_event;
QList<Reaction> m_reactions; QList<Reaction> m_reactions;
QMap<QString, QString> m_shortcodes;
void updateReactions(); void updateReactions();
QString reactionText(QString text) const; static QString reactionText(const QString &text);
}; };
Q_DECLARE_METATYPE(ReactionModel *) Q_DECLARE_METATYPE(ReactionModel *)

View File

@@ -368,6 +368,9 @@ QVariant RoomListModel::data(const QModelIndex &index, int role) const
if (role == ReplacementIdRole) { if (role == ReplacementIdRole) {
return room->successorId(); return room->successorId();
} }
if (role == IsDirectChat) {
return room->isDirectChat();
}
return QVariant(); return QVariant();
} }
@@ -401,6 +404,7 @@ QHash<int, QByteArray> RoomListModel::roleNames() const
roles[IsSpaceRole] = "isSpace"; roles[IsSpaceRole] = "isSpace";
roles[RoomIdRole] = "roomId"; roles[RoomIdRole] = "roomId";
roles[IsChildSpaceRole] = "isChildSpace"; roles[IsChildSpaceRole] = "isChildSpace";
roles[IsDirectChat] = "isDirectChat";
return roles; return roles;
} }
@@ -412,7 +416,7 @@ QString RoomListModel::categoryName(int category)
case NeoChatRoomType::Favorite: case NeoChatRoomType::Favorite:
return i18n("Favorite"); return i18n("Favorite");
case NeoChatRoomType::Direct: case NeoChatRoomType::Direct:
return i18n("Direct Messages"); return i18n("Friends");
case NeoChatRoomType::Normal: case NeoChatRoomType::Normal:
return i18n("Normal"); return i18n("Normal");
case NeoChatRoomType::Deprioritized: case NeoChatRoomType::Deprioritized:

View File

@@ -77,6 +77,7 @@ public:
IsSpaceRole, /**< Whether the room is a space. */ IsSpaceRole, /**< Whether the room is a space. */
IsChildSpaceRole, /**< Whether this space is a child of a different space. */ IsChildSpaceRole, /**< Whether this space is a child of a different space. */
ReplacementIdRole, /**< The room id of the room replacing this one, if any. */ ReplacementIdRole, /**< The room id of the room replacing this one, if any. */
IsDirectChat, /**< Whether this room is a direct chat. */
}; };
Q_ENUM(EventRoles) Q_ENUM(EventRoles)

View File

@@ -36,7 +36,7 @@ void SearchModel::setSearchText(const QString &searchText)
void SearchModel::search() void SearchModel::search()
{ {
Q_ASSERT(m_connection); Q_ASSERT(m_room);
setSearching(true); setSearching(true);
if (m_job) { if (m_job) {
m_job->abandon(); m_job->abandon();
@@ -62,7 +62,7 @@ void SearchModel::search()
}; };
auto job = m_connection->callApi<SearchJob>(SearchJob::Categories{criteria}); auto job = m_room->connection()->callApi<SearchJob>(SearchJob::Categories{criteria});
m_job = job; m_job = job;
connect(job, &BaseJob::finished, this, [this, job] { connect(job, &BaseJob::finished, this, [this, job] {
beginResetModel(); beginResetModel();
@@ -74,17 +74,6 @@ void SearchModel::search()
}); });
} }
Connection *SearchModel::connection() const
{
return m_connection;
}
void SearchModel::setConnection(Connection *connection)
{
m_connection = connection;
Q_EMIT connectionChanged();
}
QVariant SearchModel::data(const QModelIndex &index, int role) const QVariant SearchModel::data(const QModelIndex &index, int role) const
{ {
auto row = index.row(); auto row = index.row();

View File

@@ -31,11 +31,6 @@ class SearchModel : public QAbstractListModel
*/ */
Q_PROPERTY(QString searchText READ searchText WRITE setSearchText NOTIFY searchTextChanged) Q_PROPERTY(QString searchText READ searchText WRITE setSearchText NOTIFY searchTextChanged)
/**
* @brief The current connection that the model is using to search for messages.
*/
Q_PROPERTY(Quotient::Connection *connection READ connection WRITE setConnection NOTIFY connectionChanged)
/** /**
* @brief The current room that the search is being done from. * @brief The current room that the search is being done from.
*/ */
@@ -94,9 +89,6 @@ public:
QString searchText() const; QString searchText() const;
void setSearchText(const QString &searchText); void setSearchText(const QString &searchText);
Quotient::Connection *connection() const;
void setConnection(Quotient::Connection *connection);
NeoChatRoom *room() const; NeoChatRoom *room() const;
void setRoom(NeoChatRoom *room); void setRoom(NeoChatRoom *room);
@@ -130,7 +122,6 @@ public:
Q_SIGNALS: Q_SIGNALS:
void searchTextChanged(); void searchTextChanged();
void connectionChanged();
void roomChanged(); void roomChanged();
void searchingChanged(); void searchingChanged();
@@ -141,7 +132,6 @@ private:
void setSearching(bool searching); void setSearching(bool searching);
QString m_searchText; QString m_searchText;
Quotient::Connection *m_connection = nullptr;
NeoChatRoom *m_room = nullptr; NeoChatRoom *m_room = nullptr;
Quotient::Omittable<Quotient::SearchJob::ResultRoomEvents> m_result = Quotient::none; Quotient::Omittable<Quotient::SearchJob::ResultRoomEvents> m_result = Quotient::none;
Quotient::SearchJob *m_job = nullptr; Quotient::SearchJob *m_job = nullptr;

View File

@@ -83,6 +83,21 @@ bool SortFilterRoomListModel::filterAcceptsRow(int source_row, const QModelIndex
{ {
Q_UNUSED(source_parent); Q_UNUSED(source_parent);
bool acceptRoom =
sourceModel()->data(sourceModel()->index(source_row, 0), RoomListModel::DisplayNameRole).toString().contains(m_filterText, Qt::CaseInsensitive)
&& sourceModel()->data(sourceModel()->index(source_row, 0), RoomListModel::IsSpaceRole).toBool() == false;
bool isDirectChat = sourceModel()->data(sourceModel()->index(source_row, 0), RoomListModel::IsDirectChat).toBool();
// In `show direct chats` mode we only care about whether or not it's a direct chat or if the filter string matches.'
if (m_mode == DirectChats) {
return isDirectChat && acceptRoom;
}
// When not in `show direct chats` mode, filter them out.
if (isDirectChat && m_mode == Rooms) {
return false;
}
if (sourceModel()->data(sourceModel()->index(source_row, 0), RoomListModel::JoinStateRole).toString() == QStringLiteral("upgraded") if (sourceModel()->data(sourceModel()->index(source_row, 0), RoomListModel::JoinStateRole).toString() == QStringLiteral("upgraded")
&& dynamic_cast<RoomListModel *>(sourceModel()) && dynamic_cast<RoomListModel *>(sourceModel())
->connection() ->connection()
@@ -90,10 +105,6 @@ bool SortFilterRoomListModel::filterAcceptsRow(int source_row, const QModelIndex
return false; return false;
} }
bool acceptRoom =
sourceModel()->data(sourceModel()->index(source_row, 0), RoomListModel::DisplayNameRole).toString().contains(m_filterText, Qt::CaseInsensitive)
&& sourceModel()->data(sourceModel()->index(source_row, 0), RoomListModel::IsSpaceRole).toBool() == false;
if (m_activeSpaceId.isEmpty()) { if (m_activeSpaceId.isEmpty()) {
return acceptRoom; return acceptRoom;
} else { } else {
@@ -116,4 +127,20 @@ void SortFilterRoomListModel::setActiveSpaceId(const QString &spaceId)
invalidate(); invalidate();
} }
SortFilterRoomListModel::Mode SortFilterRoomListModel::mode() const
{
return m_mode;
}
void SortFilterRoomListModel::setMode(SortFilterRoomListModel::Mode mode)
{
if (m_mode == mode) {
return;
}
m_mode = mode;
Q_EMIT modeChanged();
invalidate();
}
#include "moc_sortfilterroomlistmodel.cpp" #include "moc_sortfilterroomlistmodel.cpp"

View File

@@ -47,6 +47,11 @@ class SortFilterRoomListModel : public QSortFilterProxyModel
*/ */
Q_PROPERTY(QString activeSpaceId READ activeSpaceId WRITE setActiveSpaceId NOTIFY activeSpaceIdChanged) Q_PROPERTY(QString activeSpaceId READ activeSpaceId WRITE setActiveSpaceId NOTIFY activeSpaceIdChanged)
/**
* @brief Whether only direct chats should be shown.
*/
Q_PROPERTY(Mode mode READ mode WRITE setMode NOTIFY modeChanged)
public: public:
enum RoomSortOrder { enum RoomSortOrder {
Alphabetical, Alphabetical,
@@ -55,6 +60,13 @@ public:
}; };
Q_ENUM(RoomSortOrder) Q_ENUM(RoomSortOrder)
enum Mode {
Rooms,
DirectChats,
All,
};
Q_ENUM(Mode)
explicit SortFilterRoomListModel(QObject *parent = nullptr); explicit SortFilterRoomListModel(QObject *parent = nullptr);
void setRoomSortOrder(RoomSortOrder sortOrder); void setRoomSortOrder(RoomSortOrder sortOrder);
@@ -66,6 +78,9 @@ public:
QString activeSpaceId() const; QString activeSpaceId() const;
void setActiveSpaceId(const QString &spaceId); void setActiveSpaceId(const QString &spaceId);
Mode mode() const;
void setMode(Mode mode);
protected: protected:
/** /**
* @brief Returns true if the value of source_left is less than source_right. * @brief Returns true if the value of source_left is less than source_right.
@@ -85,9 +100,11 @@ Q_SIGNALS:
void roomSortOrderChanged(); void roomSortOrderChanged();
void filterTextChanged(); void filterTextChanged();
void activeSpaceIdChanged(); void activeSpaceIdChanged();
void modeChanged();
private: private:
RoomSortOrder m_sortOrder = Categories; RoomSortOrder m_sortOrder = Categories;
Mode m_mode = All;
QString m_filterText; QString m_filterText;
QString m_activeSpaceId; QString m_activeSpaceId;
}; };

View File

@@ -26,7 +26,6 @@ void UserDirectoryListModel::setConnection(Connection *conn)
beginResetModel(); beginResetModel();
m_limited = false;
attempted = false; attempted = false;
users.clear(); users.clear();
@@ -37,53 +36,50 @@ void UserDirectoryListModel::setConnection(Connection *conn)
endResetModel(); endResetModel();
m_connection = conn; m_connection = conn;
if (job) {
job->abandon();
job = nullptr;
}
Q_EMIT connectionChanged(); Q_EMIT connectionChanged();
Q_EMIT limitedChanged();
if (m_job) {
m_job->abandon();
m_job = nullptr;
Q_EMIT searchingChanged();
}
} }
QString UserDirectoryListModel::keyword() const QString UserDirectoryListModel::searchText() const
{ {
return m_keyword; return m_searchText;
} }
void UserDirectoryListModel::setKeyword(const QString &value) void UserDirectoryListModel::setSearchText(const QString &value)
{ {
if (m_keyword == value) { if (m_searchText == value) {
return; return;
} }
m_keyword = value; m_searchText = value;
Q_EMIT searchTextChanged();
if (m_job) {
m_job->abandon();
m_job = nullptr;
Q_EMIT searchingChanged();
}
m_limited = false;
attempted = false; attempted = false;
if (job) {
job->abandon();
job = nullptr;
}
Q_EMIT keywordChanged();
Q_EMIT limitedChanged();
} }
bool UserDirectoryListModel::limited() const bool UserDirectoryListModel::searching() const
{ {
return m_limited; return m_job != nullptr;
} }
void UserDirectoryListModel::search(int count) void UserDirectoryListModel::search(int limit)
{ {
if (count < 1) { if (limit < 1) {
return; return;
} }
if (job) { if (m_job) {
qDebug() << "UserDirectoryListModel: Other jobs running, ignore"; qDebug() << "UserDirectoryListModel: Other jobs running, ignore";
return; return;
@@ -93,25 +89,22 @@ void UserDirectoryListModel::search(int count)
return; return;
} }
job = m_connection->callApi<SearchUserDirectoryJob>(m_keyword, count); m_job = m_connection->callApi<SearchUserDirectoryJob>(m_searchText, limit);
Q_EMIT searchingChanged();
connect(job, &BaseJob::finished, this, [this] { connect(m_job, &BaseJob::finished, this, [this] {
attempted = true; attempted = true;
if (job->status() == BaseJob::Success) { if (m_job->status() == BaseJob::Success) {
auto users = job->results(); auto users = m_job->results();
this->beginResetModel(); this->beginResetModel();
this->users = users; this->users = users;
this->m_limited = job->limited();
this->endResetModel(); this->endResetModel();
} }
this->job = nullptr; this->m_job = nullptr;
Q_EMIT searchingChanged();
Q_EMIT limitedChanged();
}); });
} }
@@ -127,7 +120,7 @@ QVariant UserDirectoryListModel::data(const QModelIndex &index, int role) const
return {}; return {};
} }
auto user = users.at(index.row()); auto user = users.at(index.row());
if (role == NameRole) { if (role == DisplayNameRole) {
auto displayName = user.displayName; auto displayName = user.displayName;
if (!displayName.isEmpty()) { if (!displayName.isEmpty()) {
return displayName; return displayName;
@@ -142,18 +135,17 @@ QVariant UserDirectoryListModel::data(const QModelIndex &index, int role) const
} }
if (role == AvatarRole) { if (role == AvatarRole) {
auto avatarUrl = user.avatarUrl; auto avatarUrl = user.avatarUrl;
if (avatarUrl.isEmpty() || !m_connection) {
if (avatarUrl.isEmpty()) { return QUrl();
return QString();
} }
return avatarUrl.url().remove(0, 6); return m_connection->makeMediaUrl(avatarUrl);
} }
if (role == UserIDRole) { if (role == UserIDRole) {
return user.userId; return user.userId;
} }
if (role == DirectChatsRole) { if (role == DirectChatExistsRole) {
if (!m_connection) { if (!m_connection) {
return QStringList(); return false;
}; };
auto userObj = m_connection->user(user.userId); auto userObj = m_connection->user(user.userId);
@@ -162,11 +154,11 @@ QVariant UserDirectoryListModel::data(const QModelIndex &index, int role) const
if (userObj && directChats.contains(userObj)) { if (userObj && directChats.contains(userObj)) {
auto directChatsForUser = directChats.values(userObj); auto directChatsForUser = directChats.values(userObj);
if (!directChatsForUser.isEmpty()) { if (!directChatsForUser.isEmpty()) {
return QVariant::fromValue(directChatsForUser); return true;
} }
} }
return QStringList(); return false;
} }
return {}; return {};
@@ -176,10 +168,10 @@ QHash<int, QByteArray> UserDirectoryListModel::roleNames() const
{ {
QHash<int, QByteArray> roles; QHash<int, QByteArray> roles;
roles[NameRole] = "name"; roles[DisplayNameRole] = "displayName";
roles[AvatarRole] = "avatar"; roles[AvatarRole] = "avatarUrl";
roles[UserIDRole] = "userID"; roles[UserIDRole] = "userId";
roles[DirectChatsRole] = "directChats"; roles[DirectChatExistsRole] = "directChatExists";
return roles; return roles;
} }

View File

@@ -35,24 +35,24 @@ class UserDirectoryListModel : public QAbstractListModel
Q_PROPERTY(Quotient::Connection *connection READ connection WRITE setConnection NOTIFY connectionChanged) Q_PROPERTY(Quotient::Connection *connection READ connection WRITE setConnection NOTIFY connectionChanged)
/** /**
* @brief The keyword to use in the search. * @brief The text to search the public room list for.
*/ */
Q_PROPERTY(QString keyword READ keyword WRITE setKeyword NOTIFY keywordChanged) Q_PROPERTY(QString searchText READ searchText WRITE setSearchText NOTIFY searchTextChanged)
/** /**
* @brief Whether the current results have been truncated. * @brief Whether the model is searching.
*/ */
Q_PROPERTY(bool limited READ limited NOTIFY limitedChanged) Q_PROPERTY(bool searching READ searching NOTIFY searchingChanged)
public: public:
/** /**
* @brief Defines the model roles. * @brief Defines the model roles.
*/ */
enum EventRoles { enum EventRoles {
NameRole = Qt::DisplayRole + 1, /**< The user's display name. */ DisplayNameRole = Qt::DisplayRole, /**< The user's display name. */
AvatarRole, /**< The source URL for the user's avatar. */ AvatarRole, /**< The source URL for the user's avatar. */
UserIDRole, /**< Matrix ID of the user. */ UserIDRole, /**< Matrix ID of the user. */
DirectChatsRole, /**< A list of direct chat matrix IDs with the user. */ DirectChatExistsRole, /**< Whether there is already a direct chat with the user. */
}; };
explicit UserDirectoryListModel(QObject *parent = nullptr); explicit UserDirectoryListModel(QObject *parent = nullptr);
@@ -60,17 +60,17 @@ public:
[[nodiscard]] Quotient::Connection *connection() const; [[nodiscard]] Quotient::Connection *connection() const;
void setConnection(Quotient::Connection *conn); void setConnection(Quotient::Connection *conn);
[[nodiscard]] QString keyword() const; [[nodiscard]] QString searchText() const;
void setKeyword(const QString &value); void setSearchText(const QString &searchText);
[[nodiscard]] bool limited() const; [[nodiscard]] bool searching() const;
/** /**
* @brief Get the given role value at the given index. * @brief Get the given role value at the given index.
* *
* @sa QAbstractItemModel::data * @sa QAbstractItemModel::data
*/ */
[[nodiscard]] QVariant data(const QModelIndex &index, int role = NameRole) const override; [[nodiscard]] QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
/** /**
* @brief Number of rows in the model. * @brief Number of rows in the model.
@@ -87,23 +87,23 @@ public:
[[nodiscard]] QHash<int, QByteArray> roleNames() const override; [[nodiscard]] QHash<int, QByteArray> roleNames() const override;
/** /**
* @brief Start the user search. * @brief Search the user directory.
*
* @param limit the maximum number of rooms to load.
*/ */
Q_INVOKABLE void search(int count = 50); Q_INVOKABLE void search(int limit = 50);
Q_SIGNALS: Q_SIGNALS:
void connectionChanged(); void connectionChanged();
void keywordChanged(); void searchTextChanged();
void limitedChanged(); void searchingChanged();
private: private:
Quotient::Connection *m_connection = nullptr; Quotient::Connection *m_connection = nullptr;
QString m_keyword; QString m_searchText;
bool m_limited = false;
bool attempted = false; bool attempted = false;
QList<Quotient::SearchUserDirectoryJob::User> users; QList<Quotient::SearchUserDirectoryJob::User> users;
Quotient::SearchUserDirectoryJob *job = nullptr; Quotient::SearchUserDirectoryJob *m_job = nullptr;
}; };

View File

@@ -76,7 +76,6 @@ Comment[ru]=Клиент для Matrix — децентрализованног
Comment[sk]=Klient pre matrix, decentralizovaný komunikačný protokol Comment[sk]=Klient pre matrix, decentralizovaný komunikačný protokol
Comment[sl]=Odjemalec za decentralizirani komunikacijski protokol matrix Comment[sl]=Odjemalec za decentralizirani komunikacijski protokol matrix
Comment[sv]=En klient för matrix, det decentraliserade kommunikationsprotokollet 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[tr]=Merkezi olmayan iletişim protokolü Matrix için bir istemci
Comment[uk]=Клієнт matrix, децентралізованого протоколу обміну даними Comment[uk]=Клієнт matrix, децентралізованого протоколу обміну даними
Comment[x-test]=xxA client for matrix, the decentralized communication protocolxx Comment[x-test]=xxA client for matrix, the decentralized communication protocolxx
@@ -189,6 +188,7 @@ Name[ie]=Nov invitation
Name[it]=Nuovo invito Name[it]=Nuovo invito
Name[ka]=ახალი მოსაწვევი Name[ka]=ახალი მოსაწვევი
Name[ko]=새 초대장 Name[ko]=새 초대장
Name[lt]=Naujas pakvietimas
Name[nl]=Nieuwe uitnodiging Name[nl]=Nieuwe uitnodiging
Name[nn]=Ny invitasjon Name[nn]=Ny invitasjon
Name[pa]=ਨਵਾਂ ਸੱਦਾ Name[pa]=ਨਵਾਂ ਸੱਦਾ
@@ -226,6 +226,7 @@ Comment[ie]=Vu have un nov invitation a un chambre
Comment[it]=È presente un nuovo invito a una stanza Comment[it]=È presente un nuovo invito a una stanza
Comment[ka]=გაქვთ ახალი ოთახის მოსაწვევი Comment[ka]=გაქვთ ახალი ოთახის მოსაწვევი
Comment[ko]=새로운 대화방 초대장을 받음 Comment[ko]=새로운 대화방 초대장을 받음
Comment[lt]=Yra naujas pakvietimas į kambarį
Comment[nl]=Er is een nieuwe uitnodiging naar een room Comment[nl]=Er is een nieuwe uitnodiging naar een room
Comment[nn]=Du har ein ny invitasjon til eit rom Comment[nn]=Du har ein ny invitasjon til eit rom
Comment[pa]=ਰੂਮ ਲਈ ਨਵਾਂ ਸੱਦਾ ਹੈ Comment[pa]=ਰੂਮ ਲਈ ਨਵਾਂ ਸੱਦਾ ਹੈ

View File

@@ -22,10 +22,6 @@
<label>Background transparency value</label> <label>Background transparency value</label>
<default>0.3</default> <default>0.3</default>
</entry> </entry>
<entry name="MergeRoomList" type="bool">
<label>Merge Room Lists</label>
<default>false</default>
</entry>
<entry name="AllowQuickEdit" type="bool"> <entry name="AllowQuickEdit" type="bool">
<label>Use s/text/replacement syntax to edit your last message.</label> <label>Use s/text/replacement syntax to edit your last message.</label>
<default>false</default> <default>false</default>
@@ -156,11 +152,5 @@
<default></default> <default></default>
</entry> </entry>
</group> </group>
<group name="FeatureFlags">
<entry name="Threads" type="bool">
<label>Enable threads</label>
<default>false</default>
</entry>
</group>
</kcfg> </kcfg>

View File

@@ -10,6 +10,8 @@
#include "jobs/neochatdeactivateaccountjob.h" #include "jobs/neochatdeactivateaccountjob.h"
#include "roommanager.h" #include "roommanager.h"
#include <Quotient/connection.h>
#include <Quotient/quotient_common.h>
#include <qt6keychain/keychain.h> #include <qt6keychain/keychain.h>
#include <KLocalizedString> #include <KLocalizedString>
@@ -17,7 +19,9 @@
#include <Quotient/csapi/content-repo.h> #include <Quotient/csapi/content-repo.h>
#include <Quotient/csapi/profile.h> #include <Quotient/csapi/profile.h>
#include <Quotient/database.h> #include <Quotient/database.h>
#include <Quotient/jobs/downloadfilejob.h>
#include <Quotient/qt_connection_util.h> #include <Quotient/qt_connection_util.h>
#include <Quotient/room.h>
#include <Quotient/settings.h> #include <Quotient/settings.h>
#include <Quotient/user.h> #include <Quotient/user.h>
@@ -32,6 +36,17 @@ using namespace Qt::StringLiterals;
NeoChatConnection::NeoChatConnection(QObject *parent) NeoChatConnection::NeoChatConnection(QObject *parent)
: Connection(parent) : Connection(parent)
{
connectSignals();
}
NeoChatConnection::NeoChatConnection(const QUrl &server, QObject *parent)
: Connection(server, parent)
{
connectSignals();
}
void NeoChatConnection::connectSignals()
{ {
connect(this, &NeoChatConnection::accountDataChanged, this, [this](const QString &type) { connect(this, &NeoChatConnection::accountDataChanged, this, [this](const QString &type) {
if (type == QLatin1String("org.kde.neochat.account_label")) { if (type == QLatin1String("org.kde.neochat.account_label")) {
@@ -49,14 +64,37 @@ NeoChatConnection::NeoChatConnection(QObject *parent)
Q_EMIT userConsentRequired(job->errorUrl()); Q_EMIT userConsentRequired(job->errorUrl());
} }
}); });
} connect(this, &NeoChatConnection::requestFailed, this, [](BaseJob *job) {
if (dynamic_cast<DownloadFileJob *>(job) && job->jsonData()["errcode"_ls].toString() == "M_TOO_LARGE"_ls) {
NeoChatConnection::NeoChatConnection(const QUrl &server, QObject *parent) RoomManager::instance().warning(i18n("File too large to download."), i18n("Contact your matrix server administrator for support."));
: Connection(server, parent) }
{ });
connect(this, &NeoChatConnection::accountDataChanged, this, [this](const QString &type) { connect(this, &NeoChatConnection::directChatsListChanged, this, [this](DirectChatsMap additions, DirectChatsMap removals) {
if (type == QLatin1String("org.kde.neochat.account_label")) { Q_EMIT directChatInvitesChanged();
Q_EMIT labelChanged(); for (const auto &chatId : additions) {
if (const auto chat = room(chatId)) {
connect(chat, &Room::unreadStatsChanged, this, [this]() {
Q_EMIT directChatNotificationsChanged();
});
}
}
for (const auto &chatId : removals) {
if (const auto chat = room(chatId)) {
disconnect(chat, &Room::unreadStatsChanged, this, nullptr);
}
}
});
connect(this, &NeoChatConnection::joinedRoom, this, [this](Room *room) {
if (room->isDirectChat()) {
connect(room, &Room::unreadStatsChanged, this, [this]() {
Q_EMIT directChatNotificationsChanged();
});
}
});
connect(this, &NeoChatConnection::leftRoom, this, [this](Room *room, Room *prev) {
Q_UNUSED(room)
if (prev && prev->isDirectChat()) {
Q_EMIT directChatInvitesChanged();
} }
}); });
} }
@@ -238,6 +276,20 @@ void NeoChatConnection::createSpace(const QString &name, const QString &topic, c
}); });
} }
bool NeoChatConnection::directChatExists(Quotient::User *user)
{
return directChats().contains(user);
}
void NeoChatConnection::openOrCreateDirectChat(const QString &userId)
{
if (auto user = this->user(userId)) {
openOrCreateDirectChat(user);
} else {
qWarning() << "openOrCreateDirectChat: Couldn't get user object for ID " << userId << ", unable to open/request direct chat.";
}
}
void NeoChatConnection::openOrCreateDirectChat(User *user) void NeoChatConnection::openOrCreateDirectChat(User *user)
{ {
const auto existing = directChats(); const auto existing = directChats();
@@ -252,6 +304,32 @@ void NeoChatConnection::openOrCreateDirectChat(User *user)
requestDirectChat(user); requestDirectChat(user);
} }
qsizetype NeoChatConnection::directChatNotifications() const
{
qsizetype notifications = 0;
QStringList added; // The same ID can be in the list multiple times.
for (const auto &chatId : directChats()) {
if (!added.contains(chatId)) {
if (const auto chat = room(chatId)) {
notifications += chat->notificationCount();
added += chatId;
}
}
}
return notifications;
}
bool NeoChatConnection::directChatInvites() const
{
auto inviteRooms = rooms(JoinState::Invite);
for (const auto inviteRoom : inviteRooms) {
if (inviteRoom->isDirectChat()) {
return true;
}
}
return false;
}
QCoro::Task<void> NeoChatConnection::setupPushNotifications(QString endpoint) QCoro::Task<void> NeoChatConnection::setupPushNotifications(QString endpoint)
{ {
#ifdef HAVE_KUNIFIEDPUSH #ifdef HAVE_KUNIFIEDPUSH

View File

@@ -27,6 +27,16 @@ class NeoChatConnection : public Quotient::Connection
Q_PROPERTY(QString deviceKey READ deviceKey CONSTANT) Q_PROPERTY(QString deviceKey READ deviceKey CONSTANT)
Q_PROPERTY(QString encryptionKey READ encryptionKey CONSTANT) Q_PROPERTY(QString encryptionKey READ encryptionKey CONSTANT)
/**
* @brief The total number of notifications for all direct chats.
*/
Q_PROPERTY(qsizetype directChatNotifications READ directChatNotifications NOTIFY directChatNotificationsChanged)
/**
* @brief Whether there is at least one invite to a direct chat.
*/
Q_PROPERTY(bool directChatInvites READ directChatInvites NOTIFY directChatInvitesChanged)
/** /**
* @brief Whether NeoChat is currently able to connect to the server. * @brief Whether NeoChat is currently able to connect to the server.
*/ */
@@ -80,12 +90,27 @@ public:
Q_INVOKABLE void createSpace(const QString &name, const QString &topic, const QString &parent = {}, bool setChildParent = false); Q_INVOKABLE void createSpace(const QString &name, const QString &topic, const QString &parent = {}, bool setChildParent = false);
/** /**
* @brief Join a direct chat with the given user. * @brief Whether a direct chat with the user exists.
*/
Q_INVOKABLE bool directChatExists(Quotient::User *user);
/**
* @brief Join a direct chat with the given user ID.
*
* If a direct chat with the user doesn't exist one is created and then joined.
*/
Q_INVOKABLE void openOrCreateDirectChat(const QString &userId);
/**
* @brief Join a direct chat with the given user object.
* *
* If a direct chat with the user doesn't exist one is created and then joined. * If a direct chat with the user doesn't exist one is created and then joined.
*/ */
Q_INVOKABLE void openOrCreateDirectChat(Quotient::User *user); Q_INVOKABLE void openOrCreateDirectChat(Quotient::User *user);
qsizetype directChatNotifications() const;
bool directChatInvites() const;
// note: this is intentionally a copied QString because // note: this is intentionally a copied QString because
// the reference could be destroyed before the task is finished // the reference could be destroyed before the task is finished
QCoro::Task<void> setupPushNotifications(QString endpoint); QCoro::Task<void> setupPushNotifications(QString endpoint);
@@ -97,6 +122,8 @@ public:
Q_SIGNALS: Q_SIGNALS:
void labelChanged(); void labelChanged();
void directChatNotificationsChanged();
void directChatInvitesChanged();
void isOnlineChanged(); void isOnlineChanged();
void passwordStatus(NeoChatConnection::PasswordStatus status); void passwordStatus(NeoChatConnection::PasswordStatus status);
void userConsentRequired(QUrl url); void userConsentRequired(QUrl url);
@@ -104,4 +131,6 @@ Q_SIGNALS:
private: private:
bool m_isOnline = true; bool m_isOnline = true;
void setIsOnline(bool isOnline); void setIsOnline(bool isOnline);
void connectSignals();
}; };

View File

@@ -1826,7 +1826,11 @@ int NeoChatRoom::maxRoomVersion() const
Quotient::User *NeoChatRoom::directChatRemoteUser() const Quotient::User *NeoChatRoom::directChatRemoteUser() const
{ {
return connection()->directChatUsers(this)[0]; auto users = connection()->directChatUsers(this);
if (users.isEmpty()) {
return nullptr;
}
return users[0];
} }
void NeoChatRoom::sendLocation(float lat, float lon, const QString &description) void NeoChatRoom::sendLocation(float lat, float lon, const QString &description)

View File

@@ -63,6 +63,7 @@ Comment[ie]=Trovar chambres in NeoChat
Comment[it]=Trova stanze in NeoChat Comment[it]=Trova stanze in NeoChat
Comment[ka]=იპოვე ოთახები NeoChat-ში Comment[ka]=იპოვე ოთახები NeoChat-ში
Comment[ko]=NeoChat에서 대화방 찾기 Comment[ko]=NeoChat에서 대화방 찾기
Comment[lt]=Rasti kambarius NeoChat
Comment[nl]=Rooms zoeken in NeoChat Comment[nl]=Rooms zoeken in NeoChat
Comment[nn]=Finn rom i NeoChat Comment[nn]=Finn rom i NeoChat
Comment[pl]=Znajdź pokoje w NeoChat Comment[pl]=Znajdź pokoje w NeoChat

35
src/proxycontroller.cpp Normal file
View File

@@ -0,0 +1,35 @@
// SPDX-FileCopyrightText: 2024 Tobias Fella <tobias.fella@kde.org>
// SPDX-License-Identifier: LGPL-2.0-or-later
#include "proxycontroller.h"
#include <QNetworkProxy>
#include "neochatconfig.h"
void ProxyController::setApplicationProxy()
{
auto cfg = NeoChatConfig::self();
QNetworkProxy proxy;
switch (cfg->proxyType()) {
case 1:
proxy.setType(QNetworkProxy::HttpProxy);
break;
case 2:
proxy.setType(QNetworkProxy::Socks5Proxy);
break;
default:
break;
}
proxy.setHostName(cfg->proxyHost());
proxy.setPort(cfg->proxyPort());
proxy.setUser(cfg->proxyUser());
proxy.setPassword(cfg->proxyPassword());
QNetworkProxy::setApplicationProxy(proxy);
}
ProxyController::ProxyController(QObject *parent)
: QObject(parent)
{
}

36
src/proxycontroller.h Normal file
View File

@@ -0,0 +1,36 @@
// SPDX-FileCopyrightText: 2024 Tobias Fella <tobias.fella@kde.org>
// SPDX-License-Identifier: LGPL-2.0-or-later
#pragma once
#include <QObject>
#include <QQmlEngine>
class ProxyController : public QObject
{
Q_OBJECT
QML_ELEMENT
QML_SINGLETON
public:
static ProxyController &instance()
{
static ProxyController _instance;
return _instance;
}
static ProxyController *create(QQmlEngine *engine, QJSEngine *)
{
engine->setObjectOwnership(&instance(), QQmlEngine::CppOwnership);
return &instance();
}
/**
* @brief Sets the QNetworkProxy for the application.
*
* @sa QNetworkProxy::setApplicationProxy
*/
Q_INVOKABLE void setApplicationProxy();
private:
explicit ProxyController(QObject *parent = nullptr);
};

View File

@@ -42,8 +42,7 @@ MessageDelegate {
bubbleContent: ColumnLayout { bubbleContent: ColumnLayout {
MediaPlayer { MediaPlayer {
id: audio id: audio
onErrorOccurred: (error, errorString) => console.warn("Audio playback error:" + error + errorString) source: root.progressInfo.localPath
audioOutput: AudioOutput {}
} }
states: [ states: [
@@ -54,7 +53,7 @@ MessageDelegate {
PropertyChanges { PropertyChanges {
target: playButton target: playButton
icon.name: "media-playback-start" icon.name: "media-playback-start"
onClicked: currentRoom.downloadFile(root.eventId) onClicked: root.room.downloadFile(root.eventId)
} }
}, },
State { State {
@@ -68,25 +67,24 @@ MessageDelegate {
target: playButton target: playButton
icon.name: "media-playback-stop" icon.name: "media-playback-stop"
onClicked: { onClicked: {
currentRoom.cancelFileTransfer(root.eventId) root.room.cancelFileTransfer(root.eventId)
} }
} }
}, },
State { State {
name: "paused" name: "paused"
when: root.progressInfo.completed && (audio.playbackState === MediaPlayer.StoppedState || audio.playbackState === MediaPlayer.PausedState) when: root.progressInfo.completed && (audio.playbackState === Audio.StoppedState || audio.playbackState === Audio.PausedState)
PropertyChanges { PropertyChanges {
target: playButton target: playButton
icon.name: "media-playback-start" icon.name: "media-playback-start"
onClicked: { onClicked: {
audio.source = root.progressInfo.localPath; audio.play()
audio.play();
} }
} }
}, },
State { State {
name: "playing" name: "playing"
when: root.progressInfo.completed && audio.playbackState === MediaPlayer.PlayingState when: root.progressInfo.completed && audio.playbackState === Audio.PlayingState
PropertyChanges { PropertyChanges {
target: playButton target: playButton

View File

@@ -13,7 +13,7 @@ import org.kde.kirigamiaddons.labs.components as KirigamiComponents
Delegates.RoundedItemDelegate { Delegates.RoundedItemDelegate {
id: root id: root
required property url source property url source
signal contextMenuRequested() signal contextMenuRequested()
signal selected() signal selected()

View File

@@ -220,7 +220,7 @@ QQC2.Control {
x: textField.cursorRectangle.x x: textField.cursorRectangle.x
y: textField.cursorRectangle.y - height y: textField.cursorRectangle.y - height
onFormattingSelected: _private.formatText(format, selectionStart, selectionEnd) onFormattingSelected: root.formatText(format, selectionStart, selectionEnd)
} }
Keys.onDeletePressed: { Keys.onDeletePressed: {

View File

@@ -30,7 +30,7 @@ QQC2.ItemDelegate {
width: ListView.view.width width: ListView.view.width
height: visible ? ListView.view.width : 0 height: visible ? ListView.view.width : 0
visible: root.categoryVisible || filterText.length > 0 || Config.mergeRoomList visible: root.categoryVisible || filterText.length > 0
contentItem: KirigamiComponents.Avatar { contentItem: KirigamiComponents.Avatar {
source: root.avatar ? `image://mxc/${root.avatar}` : "" source: root.avatar ? `image://mxc/${root.avatar}` : ""

View File

@@ -70,6 +70,7 @@ Loader {
QQC2.Menu { QQC2.Menu {
title: i18n("Notification State") title: i18n("Notification State")
icon.name: "notifications"
QQC2.MenuItem { QQC2.MenuItem {
text: i18n("Follow Global Setting") text: i18n("Follow Global Setting")

View File

@@ -103,7 +103,7 @@ FormCard.FormCardPage {
visible: !chosenRoomDelegate.visible visible: !chosenRoomDelegate.visible
text: i18nc("@action:button", "Pick room") text: i18nc("@action:button", "Pick room")
onClicked: { onClicked: {
let dialog = pageStack.pushDialogLayer("qrc:/org/kde/neochat/qml/JoinRoomPage.qml", {connection: root.connection}, {title: i18nc("@title", "Explore Rooms")}) let dialog = pageStack.pushDialogLayer("qrc:/org/kde/neochat/qml/ExploreRoomsPage.qml", {connection: root.connection}, {title: i18nc("@title", "Explore Rooms")})
dialog.roomSelected.connect((roomId, displayName, avatarUrl, alias, topic, memberCount, isJoined) => { dialog.roomSelected.connect((roomId, displayName, avatarUrl, alias, topic, memberCount, isJoined) => {
chosenRoomDelegate.roomId = roomId; chosenRoomDelegate.roomId = roomId;
chosenRoomDelegate.displayName = displayName; chosenRoomDelegate.displayName = displayName;
@@ -182,7 +182,7 @@ FormCard.FormCardPage {
} }
onClicked: { onClicked: {
let dialog = pageStack.pushDialogLayer("qrc:/org/kde/neochat/qml/JoinRoomPage.qml", {connection: root.connection}, {title: i18nc("@title", "Explore Rooms")}) let dialog = pageStack.pushDialogLayer("qrc:/org/kde/neochat/qml/ExploreRoomsPage.qml", {connection: root.connection}, {title: i18nc("@title", "Explore Rooms")})
dialog.roomSelected.connect((roomId, displayName, avatarUrl, alias, topic, memberCount, isJoined) => { dialog.roomSelected.connect((roomId, displayName, avatarUrl, alias, topic, memberCount, isJoined) => {
chosenRoomDelegate.roomId = roomId; chosenRoomDelegate.roomId = roomId;
chosenRoomDelegate.displayName = displayName; chosenRoomDelegate.displayName = displayName;

View File

@@ -11,9 +11,12 @@ import org.kde.neochat
DelegateChooser { DelegateChooser {
id: root id: root
/**
* @brief The NeoChatRoom the delegate is being displayed in.
*/
required property NeoChatRoom room
role: "delegateType" role: "delegateType"
property var room
required property NeoChatConnection connection
DelegateChoice { DelegateChoice {
roleValue: DelegateType.State roleValue: DelegateType.State
@@ -23,63 +26,63 @@ DelegateChooser {
DelegateChoice { DelegateChoice {
roleValue: DelegateType.Emote roleValue: DelegateType.Emote
delegate: TextDelegate { delegate: TextDelegate {
connection: root.connection room: root.room
} }
} }
DelegateChoice { DelegateChoice {
roleValue: DelegateType.Message roleValue: DelegateType.Message
delegate: TextDelegate { delegate: TextDelegate {
connection: root.connection room: root.room
} }
} }
DelegateChoice { DelegateChoice {
roleValue: DelegateType.Notice roleValue: DelegateType.Notice
delegate: TextDelegate { delegate: TextDelegate {
connection: root.connection room: root.room
} }
} }
DelegateChoice { DelegateChoice {
roleValue: DelegateType.Image roleValue: DelegateType.Image
delegate: ImageDelegate { delegate: ImageDelegate {
connection: root.connection room: root.room
} }
} }
DelegateChoice { DelegateChoice {
roleValue: DelegateType.Sticker roleValue: DelegateType.Sticker
delegate: ImageDelegate { delegate: ImageDelegate {
connection: root.connection room: root.room
} }
} }
DelegateChoice { DelegateChoice {
roleValue: DelegateType.Audio roleValue: DelegateType.Audio
delegate: AudioDelegate { delegate: AudioDelegate {
connection: root.connection room: root.room
} }
} }
DelegateChoice { DelegateChoice {
roleValue: DelegateType.Video roleValue: DelegateType.Video
delegate: VideoDelegate { delegate: VideoDelegate {
connection: root.connection room: root.room
} }
} }
DelegateChoice { DelegateChoice {
roleValue: DelegateType.File roleValue: DelegateType.File
delegate: FileDelegate { delegate: FileDelegate {
connection: root.connection room: root.room
} }
} }
DelegateChoice { DelegateChoice {
roleValue: DelegateType.Encrypted roleValue: DelegateType.Encrypted
delegate: EncryptedDelegate { delegate: EncryptedDelegate {
connection: root.connection room: root.room
} }
} }
@@ -91,14 +94,14 @@ DelegateChooser {
DelegateChoice { DelegateChoice {
roleValue: DelegateType.Poll roleValue: DelegateType.Poll
delegate: PollDelegate { delegate: PollDelegate {
connection: root.connection room: root.room
} }
} }
DelegateChoice { DelegateChoice {
roleValue: DelegateType.Location roleValue: DelegateType.Location
delegate: LocationDelegate { delegate: LocationDelegate {
connection: root.connection room: root.room
} }
} }
@@ -106,7 +109,6 @@ DelegateChooser {
roleValue: DelegateType.LiveLocation roleValue: DelegateType.LiveLocation
delegate: LiveLocationDelegate { delegate: LiveLocationDelegate {
room: root.room room: root.room
connection: root.connection
} }
} }

View File

@@ -21,7 +21,7 @@ RowLayout {
text: i18n("Explore rooms") text: i18n("Explore rooms")
icon.name: "compass" icon.name: "compass"
onTriggered: { onTriggered: {
let dialog = pageStack.pushDialogLayer("qrc:/org/kde/neochat/qml/JoinRoomPage.qml", {connection: root.connection}, {title: i18nc("@title", "Explore Rooms")}) let dialog = pageStack.pushDialogLayer("qrc:/org/kde/neochat/qml/ExploreRoomsPage.qml", {connection: root.connection}, {title: i18nc("@title", "Explore Rooms")})
dialog.roomSelected.connect((roomId, displayName, avatarUrl, alias, topic, memberCount, isJoined) => { dialog.roomSelected.connect((roomId, displayName, avatarUrl, alias, topic, memberCount, isJoined) => {
if (isJoined) { if (isJoined) {
RoomManager.enterRoom(root.connection.room(roomId)) RoomManager.enterRoom(root.connection.room(roomId))
@@ -32,9 +32,9 @@ RowLayout {
} }
} }
property Kirigami.Action chatAction: Kirigami.Action { property Kirigami.Action chatAction: Kirigami.Action {
text: i18n("Start a Chat") text: i18n("Find your friends")
icon.name: "list-add-user" icon.name: "list-add-user"
onTriggered: pageStack.pushDialogLayer("qrc:/org/kde/neochat/qml/StartChatPage.qml", {connection: root.connection}, {title: i18nc("@title", "Start a Chat")}) onTriggered: pageStack.pushDialogLayer("qrc:/org/kde/neochat/qml/UserSearchPage.qml", {connection: root.connection}, {title: i18nc("@title", "Find your friends")})
} }
property Kirigami.Action roomAction: Kirigami.Action { property Kirigami.Action roomAction: Kirigami.Action {
text: i18n("Create a Room") text: i18n("Create a Room")

View File

@@ -52,7 +52,7 @@ ColumnLayout {
text: i18n("Explore rooms") text: i18n("Explore rooms")
icon.name: "compass" icon.name: "compass"
onTriggered: { onTriggered: {
let dialog = pageStack.pushDialogLayer("qrc:/org/kde/neochat/qml/JoinRoomPage.qml", {connection: root.connection}, {title: i18nc("@title", "Explore Rooms")}) let dialog = pageStack.pushDialogLayer("qrc:/org/kde/neochat/qml/ExploreRoomsPage.qml", {connection: root.connection}, {title: i18nc("@title", "Explore Rooms")})
dialog.roomSelected.connect((roomId, displayName, avatarUrl, alias, topic, memberCount, isJoined) => { dialog.roomSelected.connect((roomId, displayName, avatarUrl, alias, topic, memberCount, isJoined) => {
if (isJoined) { if (isJoined) {
RoomManager.enterRoom(root.connection.room(roomId)); RoomManager.enterRoom(root.connection.room(roomId));
@@ -64,10 +64,10 @@ ColumnLayout {
} }
}, },
Kirigami.Action { Kirigami.Action {
text: i18n("Start a Chat") text: i18n("Find your friends")
icon.name: "list-add-user" icon.name: "list-add-user"
onTriggered: { onTriggered: {
pageStack.pushDialogLayer("qrc:/org/kde/neochat/qml/StartChatPage.qml", {connection: root.connection}, {title: i18nc("@title", "Start a Chat")}) pageStack.pushDialogLayer("qrc:/org/kde/neochat/qml/UserSearchPage.qml", {connection: root.connection}, {title: i18nc("@title", "Find your friends")})
exploreTabBar.currentIndex = -1; exploreTabBar.currentIndex = -1;
} }
}, },

View File

@@ -0,0 +1,112 @@
// SPDX-FileCopyrightText: 2019 Black Hat <bhat@encom.eu.org>
// SPDX-FileCopyrightText: 2020 Carl Schwan <carl@carlschwan.eu>
// SPDX-License-Identifier: GPL-3.0-only
import QtQuick
import QtQuick.Controls as QQC2
import QtQuick.Layouts
import Qt.labs.qmlmodels
import org.kde.kirigami as Kirigami
import org.kde.kirigamiaddons.delegates as Delegates
import org.kde.neochat
/**
* @brief Component for finding rooms for the public list.
*
* This component is based on a SearchPage, adding the functionality to select or
* enter a server in the header, as well as the ability to manually type a room in
* if the public room search cannot find it.
*
* @sa SearchPage
*/
SearchPage {
id: root
/**
* @brief The connection for the current local user.
*/
required property NeoChatConnection connection
/**
* @brief Whether results should only includes spaces.
*/
property bool showOnlySpaces: false
/**
* @brief Signal emitted when a room is selected.
*
* The signal contains all the room's info so that it can be acted
* upon as required, e.g. joining or entering the room or adding the room as
* the child of a space.
*/
signal roomSelected(string roomId,
string displayName,
url avatarUrl,
string alias,
string topic,
int memberCount,
bool isJoined)
title: i18nc("@action:title", "Explore Rooms")
Component.onCompleted: focusSearch()
headerTrailing: ServerComboBox {
id: serverComboBox
connection: root.connection
}
model: PublicRoomListModel {
id: publicRoomListModel
connection: root.connection
server: serverComboBox.server
showOnlySpaces: root.showOnlySpaces
}
modelDelegate: ExplorerDelegate {
onRoomSelected: (roomId, displayName, avatarUrl, alias, topic, memberCount, isJoined) => {
root.roomSelected(roomId, displayName, avatarUrl, alias, topic, memberCount, isJoined);
root.closeDialog();
}
}
listHeaderDelegate: Delegates.RoundedItemDelegate {
onClicked: _private.openManualRoomDialog()
text: i18n("Enter a room address")
icon.name: "compass"
icon.width: Kirigami.Units.gridUnit * 2
icon.height: Kirigami.Units.gridUnit * 2
}
listFooterDelegate: QQC2.ProgressBar {
width: ListView.view.width
leftInset: Kirigami.Units.largeSpacing
rightInset: Kirigami.Units.largeSpacing
visible: root.count !== 0 && publicRoomListModel.searching
indeterminate: true
}
searchFieldPlaceholder: i18n("Find a room…")
noResultPlaceholderMessage: i18nc("@info:label", "No public rooms found")
Component {
id: manualRoomDialog
ManualRoomDialog {}
}
QtObject {
id: _private
function openManualRoomDialog() {
let dialog = manualRoomDialog.createObject(applicationWindow().overlay, {connection: root.connection});
dialog.roomSelected.connect((roomId, displayName, avatarUrl, alias, topic, memberCount, isJoined) => {
root.roomSelected(roomId, displayName, avatarUrl, alias, topic, memberCount, isJoined);
root.closeDialog();
});
dialog.open();
}
}
}

View File

@@ -5,6 +5,7 @@ import QtQuick
import QtQuick.Controls as QQC2 import QtQuick.Controls as QQC2
import QtQuick.Layouts import QtQuick.Layouts
import Qt.labs.platform import Qt.labs.platform
import Qt.labs.qmlmodels
import org.kde.coreaddons import org.kde.coreaddons
import org.kde.kirigami as Kirigami import org.kde.kirigami as Kirigami
@@ -41,8 +42,11 @@ MessageDelegate {
*/ */
property bool autoOpenFile: false property bool autoOpenFile: false
onDownloadedChanged: if (autoOpenFile) { onDownloadedChanged: {
openSavedFile(); itineraryModel.path = root.progressInfo.localPath
if (autoOpenFile) {
openSavedFile();
}
} }
onOpenContextMenu: RoomManager.viewEventMenu(eventId, author, delegateType, plainText, "", "", mediaInfo.mimeType, progressInfo) onOpenContextMenu: RoomManager.viewEventMenu(eventId, author, delegateType, plainText, "", "", mediaInfo.mimeType, progressInfo)
@@ -50,142 +54,224 @@ MessageDelegate {
function saveFileAs() { function saveFileAs() {
const dialog = fileDialog.createObject(QQC2.ApplicationWindow.overlay) const dialog = fileDialog.createObject(QQC2.ApplicationWindow.overlay)
dialog.open() dialog.open()
dialog.currentFile = dialog.folder + "/" + currentRoom.fileNameToDownload(root.eventId) dialog.currentFile = dialog.folder + "/" + root.room.fileNameToDownload(root.eventId)
} }
function openSavedFile() { function openSavedFile() {
UrlHelper.openUrl(root.progressInfo.localPath); UrlHelper.openUrl(root.progressInfo.localPath);
} }
bubbleContent: RowLayout { bubbleContent: ColumnLayout {
spacing: Kirigami.Units.largeSpacing spacing: Kirigami.Units.largeSpacing
RowLayout {
spacing: Kirigami.Units.largeSpacing
states: [ states: [
State { State {
name: "downloadedInstant" name: "downloadedInstant"
when: root.progressInfo.completed && autoOpenFile when: root.progressInfo.completed && autoOpenFile
PropertyChanges { PropertyChanges {
target: openButton target: openButton
icon.name: "document-open" icon.name: "document-open"
onClicked: openSavedFile() onClicked: openSavedFile()
}
PropertyChanges {
target: downloadButton
icon.name: "download"
QQC2.ToolTip.text: i18nc("tooltip for a button on a message; offers ability to download its file", "Download")
onClicked: saveFileAs()
}
},
State {
name: "downloaded"
when: root.progressInfo.completed && !autoOpenFile
PropertyChanges {
target: openButton
visible: false
}
PropertyChanges {
target: downloadButton
icon.name: "document-open"
QQC2.ToolTip.text: i18nc("tooltip for a button on a message; offers ability to open its downloaded file with an appropriate application", "Open File")
onClicked: openSavedFile()
}
},
State {
name: "downloading"
when: root.progressInfo.active
PropertyChanges {
target: openButton
visible: false
}
PropertyChanges {
target: sizeLabel
text: i18nc("file download progress", "%1 / %2", Format.formatByteSize(root.progressInfo.progress), Format.formatByteSize(root.progressInfo.total))
}
PropertyChanges {
target: downloadButton
icon.name: "media-playback-stop"
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()
}
} }
]
PropertyChanges { Kirigami.Icon {
target: downloadButton source: root.mediaInfo.mimeIcon
icon.name: "download" fallback: "unknown"
QQC2.ToolTip.text: i18nc("tooltip for a button on a message; offers ability to download its file", "Download")
onClicked: saveFileAs()
}
},
State {
name: "downloaded"
when: root.progressInfo.completed && !autoOpenFile
PropertyChanges {
target: openButton
visible: false
}
PropertyChanges {
target: downloadButton
icon.name: "document-open"
QQC2.ToolTip.text: i18nc("tooltip for a button on a message; offers ability to open its downloaded file with an appropriate application", "Open File")
onClicked: openSavedFile()
}
},
State {
name: "downloading"
when: root.progressInfo.active
PropertyChanges {
target: openButton
visible: false
}
PropertyChanges {
target: sizeLabel
text: i18nc("file download progress", "%1 / %2", Format.formatByteSize(root.progressInfo.progress), Format.formatByteSize(root.progressInfo.total))
}
PropertyChanges {
target: downloadButton
icon.name: "media-playback-stop"
QQC2.ToolTip.text: i18nc("tooltip for a button on a message; stops downloading the message's file", "Stop Download")
onClicked: currentRoom.cancelFileTransfer(root.eventId)
}
},
State {
name: "raw"
when: true
PropertyChanges {
target: downloadButton
onClicked: root.saveFileAs()
}
}
]
Kirigami.Icon {
source: root.mediaInfo.mimeIcon
fallback: "unknown"
}
ColumnLayout {
spacing: 0
QQC2.Label {
Layout.fillWidth: true
text: root.display
wrapMode: Text.Wrap
elide: Text.ElideRight
}
QQC2.Label {
id: sizeLabel
Layout.fillWidth: true
text: Format.formatByteSize(root.mediaInfo.size)
opacity: 0.7
elide: Text.ElideRight
maximumLineCount: 1
}
}
QQC2.Button {
id: openButton
icon.name: "document-open"
onClicked: {
autoOpenFile = true;
currentRoom.downloadTempFile(root.eventId);
} }
QQC2.ToolTip.text: i18nc("tooltip for a button on a message; offers ability to open its downloaded file with an appropriate application", "Open File") ColumnLayout {
QQC2.ToolTip.visible: hovered spacing: 0
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay QQC2.Label {
} Layout.fillWidth: true
text: root.display
wrapMode: Text.Wrap
elide: Text.ElideRight
}
QQC2.Label {
id: sizeLabel
Layout.fillWidth: true
text: Format.formatByteSize(root.mediaInfo.size)
opacity: 0.7
elide: Text.ElideRight
maximumLineCount: 1
}
}
QQC2.Button { QQC2.Button {
id: downloadButton id: openButton
icon.name: "download" icon.name: "document-open"
onClicked: {
autoOpenFile = true;
root.room.downloadTempFile(root.eventId);
}
QQC2.ToolTip.text: i18nc("tooltip for a button on a message; offers ability to download its file", "Download") QQC2.ToolTip.text: i18nc("tooltip for a button on a message; offers ability to open its downloaded file with an appropriate application", "Open File")
QQC2.ToolTip.visible: hovered QQC2.ToolTip.visible: hovered
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
} }
Component { QQC2.Button {
id: fileDialog id: downloadButton
icon.name: "download"
FileDialog { QQC2.ToolTip.text: i18nc("tooltip for a button on a message; offers ability to download its file", "Download")
fileMode: FileDialog.SaveFile QQC2.ToolTip.visible: hovered
folder: Config.lastSaveDirectory.length > 0 ? Config.lastSaveDirectory : StandardPaths.writableLocation(StandardPaths.DownloadLocation) QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
onAccepted: { }
Config.lastSaveDirectory = folder
Config.save() Component {
if (autoOpenFile) { id: fileDialog
UrlHelper.copyTo(root.progressInfo.localPath, file)
} else { FileDialog {
currentRoom.download(root.eventId, file); fileMode: FileDialog.SaveFile
folder: Config.lastSaveDirectory.length > 0 ? Config.lastSaveDirectory : StandardPaths.writableLocation(StandardPaths.DownloadLocation)
onAccepted: {
Config.lastSaveDirectory = folder
Config.save()
if (autoOpenFile) {
UrlHelper.copyTo(root.progressInfo.localPath, file)
} else {
root.room.download(root.eventId, file);
}
} }
} }
} }
} }
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

@@ -140,29 +140,6 @@ FormCard.FormCardPage {
} }
} }
} }
FormCard.FormHeader {
title: i18n("Rooms and private chats")
}
FormCard.FormCard {
FormCard.FormRadioDelegate {
text: i18n("Separated")
checked: !Config.mergeRoomList
enabled: !Config.isMergeRoomListImmutable
onToggled: {
Config.mergeRoomList = false
Config.save()
}
}
FormCard.FormRadioDelegate {
text: i18n("Intermixed")
checked: Config.mergeRoomList
enabled: !Config.isMergeRoomListImmutable
onToggled: {
Config.mergeRoomList = true
Config.save()
}
}
}
FormCard.FormHeader { FormCard.FormHeader {
title: i18nc("Chat Editor", "Editor") title: i18nc("Chat Editor", "Editor")
} }

View File

@@ -45,13 +45,13 @@ Labs.MenuBar {
title: i18nc("menu", "File") title: i18nc("menu", "File")
Labs.MenuItem { Labs.MenuItem {
text: i18nc("menu", "New Private Chat…") text: i18nc("menu", "Find your friends")
enabled: pageStack.layers.currentItem.title !== i18n("Start a Chat") && AccountRegistry.accountCount > 0 enabled: pageStack.layers.currentItem.title !== i18n("Find your friends") && AccountRegistry.accountCount > 0
onTriggered: pushReplaceLayer("qrc:/org/kde/neochat/qml/StartChatPage.qml", {connection: root.connection}) onTriggered: pushReplaceLayer("qrc:/org/kde/neochat/qml/UserSearchPage.qml", {connection: root.connection}, {title: i18nc("@title", "Find your friends")})
} }
Labs.MenuItem { Labs.MenuItem {
text: i18nc("menu", "New Group…") text: i18nc("menu", "New Group…")
enabled: pageStack.layers.currentItem.title !== i18n("Start a Chat") && AccountRegistry.accountCount > 0 enabled: pageStack.layers.currentItem.title !== i18n("Find your friends") && AccountRegistry.accountCount > 0
shortcut: StandardKey.New shortcut: StandardKey.New
onTriggered: { onTriggered: {
const dialog = createRoomDialog.createObject(root.overlay) const dialog = createRoomDialog.createObject(root.overlay)
@@ -61,7 +61,7 @@ Labs.MenuBar {
Labs.MenuItem { Labs.MenuItem {
text: i18nc("menu", "Browse Chats…") text: i18nc("menu", "Browse Chats…")
onTriggered: { onTriggered: {
let dialog = pageStack.pushDialogLayer("qrc:/org/kde/neochat/qml/JoinRoomPage.qml", {connection: root.connection}, {title: i18nc("@title", "Explore Rooms")}) let dialog = pageStack.pushDialogLayer("qrc:/org/kde/neochat/qml/ExploreRoomsPage.qml", {connection: root.connection}, {title: i18nc("@title", "Explore Rooms")})
dialog.roomSelected.connect((roomId, displayName, avatarUrl, alias, topic, memberCount, isJoined) => { dialog.roomSelected.connect((roomId, displayName, avatarUrl, alias, topic, memberCount, isJoined) => {
if (isJoined) { if (isJoined) {
RoomManager.enterRoom(root.connection.room(roomId)) RoomManager.enterRoom(root.connection.room(roomId))

View File

@@ -7,7 +7,6 @@ import QtQuick.Layouts
import org.kde.kirigami as Kirigami import org.kde.kirigami as Kirigami
import org.kde.neochat import org.kde.neochat
import org.kde.neochat.config
/** /**
* @brief A component that provides a set of actions when a message is hovered in the timeline. * @brief A component that provides a set of actions when a message is hovered in the timeline.
@@ -109,7 +108,7 @@ QQC2.Control {
} }
}, },
Kirigami.Action { Kirigami.Action {
visible: Config.threads && !root.currentRoom.readOnly visible: !root.currentRoom.readOnly
text: i18n("Reply in Thread") text: i18n("Reply in Thread")
icon.name: "dialog-messages" icon.name: "dialog-messages"
onTriggered: { onTriggered: {

View File

@@ -148,7 +148,7 @@ MessageDelegate {
openSavedFile() openSavedFile()
} else { } else {
openOnFinished = true openOnFinished = true
ListView.view.currentRoom.downloadFile(root.eventId, StandardPaths.writableLocation(StandardPaths.CacheLocation) + "/" + root.eventId.replace(":", "_").replace("/", "_").replace("+", "_") + ListView.view.currentRoom.fileNameToDownload(root.eventId)) root.room.downloadFile(root.eventId, StandardPaths.writableLocation(StandardPaths.CacheLocation) + "/" + root.eventId.replace(":", "_").replace("/", "_").replace("+", "_") + root.room.fileNameToDownload(root.eventId))
} }
} }

View File

@@ -31,7 +31,7 @@ Kirigami.ScrollablePage {
Kirigami.SearchField { Kirigami.SearchField {
id: identifierField id: identifierField
property bool isUserID: text.match(/@(.+):(.+)/g) property bool isUserId: text.match(/@(.+):(.+)/g)
Layout.fillWidth: true Layout.fillWidth: true
placeholderText: i18n("Find a user...") placeholderText: i18n("Find a user...")
@@ -39,7 +39,7 @@ Kirigami.ScrollablePage {
} }
QQC2.Button { QQC2.Button {
visible: identifierField.isUserID visible: identifierField.isUserId
text: i18n("Add") text: i18n("Add")
highlighted: true highlighted: true
@@ -59,7 +59,7 @@ Kirigami.ScrollablePage {
id: userDictListModel id: userDictListModel
connection: root.room.connection connection: root.room.connection
keyword: identifierField.text searchText: identifierField.text
} }
Kirigami.PlaceholderMessage { Kirigami.PlaceholderMessage {
@@ -73,25 +73,25 @@ Kirigami.ScrollablePage {
delegate: Delegates.RoundedItemDelegate { delegate: Delegates.RoundedItemDelegate {
id: delegate id: delegate
required property string userID required property string userId
required property string name required property string displayName
required property string avatar required property url avatarUrl
property bool inRoom: room && room.containsUser(userID) property bool inRoom: room && room.containsUser(userId)
text: name text: displayName
contentItem: RowLayout { contentItem: RowLayout {
KirigamiComponents.Avatar { KirigamiComponents.Avatar {
Layout.preferredWidth: Kirigami.Units.iconSizes.medium Layout.preferredWidth: Kirigami.Units.iconSizes.medium
Layout.preferredHeight: Kirigami.Units.iconSizes.medium Layout.preferredHeight: Kirigami.Units.iconSizes.medium
source: delegate.avatar ? ("image://mxc/" + delegate.avatar) : "" source: delegate.avatarUrl
name: delegate.name name: delegate.displayName
} }
Delegates.SubtitleContentItem { Delegates.SubtitleContentItem {
itemDelegate: delegate itemDelegate: delegate
subtitle: delegate.userID subtitle: delegate.userId
labelItem.textFormat: Text.PlainText labelItem.textFormat: Text.PlainText
} }
@@ -107,7 +107,7 @@ Kirigami.ScrollablePage {
if (inRoom) { if (inRoom) {
checked = true checked = true
} else { } else {
room.inviteToRoom(delegate.userID); room.inviteToRoom(delegate.userId);
applicationWindow().pageStack.layers.pop(); applicationWindow().pageStack.layers.pop();
} }
} }

View File

@@ -1,285 +0,0 @@
// SPDX-FileCopyrightText: 2019 Black Hat <bhat@encom.eu.org>
// SPDX-FileCopyrightText: 2020 Carl Schwan <carl@carlschwan.eu>
// SPDX-License-Identifier: GPL-3.0-only
import QtQuick
import QtQuick.Controls as QQC2
import QtQuick.Layouts
import Qt.labs.qmlmodels
import org.kde.kirigami as Kirigami
import org.kde.kirigamiaddons.delegates as Delegates
import org.kde.neochat
Kirigami.ScrollablePage {
id: root
required property NeoChatConnection connection
property bool showOnlySpaces: false
property alias keyword: identifierField.text
property string server
/**
* @brief Signal emitted when a room is selected.
*
* The signal contains all the room's info so that it can be acted
* upon as required, e.g. joinng or entering the room or adding the room as
* the child of a space.
*/
signal roomSelected(string roomId,
string displayName,
url avatarUrl,
string alias,
string topic,
int memberCount,
bool isJoined)
title: i18n("Explore Rooms")
Component.onCompleted: identifierField.forceActiveFocus()
header: QQC2.Control {
padding: Kirigami.Units.largeSpacing
background: Rectangle {
Kirigami.Theme.colorSet: Kirigami.Theme.Window
Kirigami.Theme.inherit: false
color: Kirigami.Theme.backgroundColor
}
contentItem: RowLayout {
Kirigami.SearchField {
id: identifierField
Layout.fillWidth: true
placeholderText: i18n("Find a room...")
}
QQC2.ComboBox {
id: serverField
// TODO: in KF6 we should be able to switch to using implicitContentWidthPolicy
Layout.preferredWidth: Kirigami.Units.gridUnit * 10
Component.onCompleted: currentIndex = 0
textRole: "url"
valueRole: "url"
model: ServerListModel {
id: serverListModel
connection: root.connection
}
delegate: Delegates.RoundedItemDelegate {
id: serverItem
required property int index
required property string url
required property bool isAddServerDelegate
required property bool isHomeServer
required property bool isDeletable
text: isAddServerDelegate ? i18n("Add New Server") : url
highlighted: false
topInset: index === 0 ? Kirigami.Units.smallSpacing : Math.round(Kirigami.Units.smallSpacing / 2)
bottomInset: index === ListView.view.count - 1 ? Kirigami.Units.smallSpacing : Math.round(Kirigami.Units.smallSpacing / 2)
onClicked: if (isAddServerDelegate) {
addServerSheet.open()
}
contentItem: RowLayout {
spacing: Kirigami.Units.smallSpacing
Delegates.SubtitleContentItem {
itemDelegate: serverItem
subtitle: serverItem.isHomeServer ? i18n("Home Server") : ""
Layout.fillWidth: true
}
QQC2.ToolButton {
visible: serverItem.isAddServerDelegate || serverItem.isDeletable
icon.name: serverItem.isAddServerDelegate ? "list-add" : "dialog-close"
text: i18nc("@action:button", "Add new server")
Accessible.name: text
display: QQC2.AbstractButton.IconOnly
onClicked: {
if (serverField.currentIndex === serverItem.index && serverItem.isDeletable) {
serverField.currentIndex = 0;
server = serverField.currentValue;
serverField.popup.close();
}
if (serverItem.isAddServerDelegate) {
addServerSheet.open();
serverItem.clicked();
} else {
serverListModel.removeServerAtIndex(serverItem.index);
}
}
}
}
}
onActivated: {
if (currentIndex !== count - 1) {
server = currentValue
}
}
Kirigami.OverlaySheet {
id: addServerSheet
parent: applicationWindow().overlay
title: i18nc("@title:window", "Add server")
onOpened: if (!serverUrlField.isValidServer && !addServerSheet.opened) {
serverField.currentIndex = 0
server = serverField.currentValue
} else if (addServerSheet.opened) {
serverUrlField.forceActiveFocus()
}
contentItem: Kirigami.FormLayout {
QQC2.Label {
Layout.minimumWidth: Kirigami.Units.gridUnit * 20
text: serverUrlField.length > 0 ? (serverUrlField.acceptableInput ? (serverUrlField.isValidServer ? i18n("Valid server entered") : i18n("This server cannot be resolved or has already been added")) : i18n("The entered text is not a valid url")) : i18n("Enter server url e.g. kde.org")
color: serverUrlField.length > 0 ? (serverUrlField.acceptableInput ? (serverUrlField.isValidServer ? Kirigami.Theme.positiveTextColor : Kirigami.Theme.negativeTextColor) : Kirigami.Theme.negativeTextColor) : Kirigami.Theme.textColor
}
QQC2.TextField {
id: serverUrlField
property bool isValidServer: false
Kirigami.FormData.label: i18n("Server URL")
onTextChanged: {
if(acceptableInput) {
serverListModel.checkServer(text)
}
}
validator: RegularExpressionValidator {
regularExpression: /^[a-zA-Z0-9-]{1,61}\.([a-zA-Z]{2,}|[a-zA-Z0-9-]{2,}\.[a-zA-Z]{2,3})$/
}
Connections {
target: serverListModel
function onServerCheckComplete(url, valid) {
if (url == serverUrlField.text && valid) {
serverUrlField.isValidServer = true
}
}
}
}
QQC2.Button {
id: okButton
text: i18nc("@action:button", "Ok")
enabled: serverUrlField.acceptableInput && serverUrlField.isValidServer
onClicked: {
serverListModel.addServer(serverUrlField.text)
serverField.currentIndex = serverField.indexOfValue(serverUrlField.text)
server = serverField.currentValue
serverUrlField.text = ""
addServerSheet.close();
}
}
}
}
}
}
Kirigami.Separator {
z: 999
anchors {
left: parent.left
right: parent.right
top: parent.bottom
}
}
}
ListView {
id: publicRoomsListView
topMargin: Math.round(Kirigami.Units.smallSpacing / 2)
bottomMargin: Math.round(Kirigami.Units.smallSpacing / 2)
model: PublicRoomListModel {
id: publicRoomListModel
connection: root.connection
server: root.server
keyword: root.keyword
showOnlySpaces: root.showOnlySpaces
}
onContentYChanged: {
if(publicRoomListModel.hasMore && contentHeight - contentY < publicRoomsListView.height + 200)
publicRoomListModel.next();
}
delegate: ExplorerDelegate {
onRoomSelected: (roomId, displayName, avatarUrl, alias, topic, memberCount, isJoined) => {
root.roomSelected(roomId, displayName, avatarUrl, alias, topic, memberCount, isJoined);
root.closeDialog();
}
}
header: Delegates.RoundedItemDelegate {
Layout.fillWidth: true
onClicked: _private.openManualRoomDialog()
text: i18n("Enter a room address")
icon.name: "compass"
icon.width: Kirigami.Units.gridUnit * 2
icon.height: Kirigami.Units.gridUnit * 2
}
footer: QQC2.ProgressBar {
width: parent.width
visible: publicRoomsListView.count !== 0 && publicRoomsListView.model.loading
indeterminate: true
padding: Kirigami.Units.largeSpacing * 2
Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
Layout.topMargin: Kirigami.Units.largeSpacing
Layout.bottomMargin: Kirigami.Units.largeSpacing
Layout.leftMargin: Kirigami.Units.largeSpacing
Layout.rightMargin: Kirigami.Units.largeSpacing
}
Kirigami.LoadingPlaceholder {
anchors.centerIn: parent
visible: publicRoomsListView.model.loading && publicRoomsListView.count === 0
}
Kirigami.PlaceholderMessage {
anchors.centerIn: parent
visible: !publicRoomsListView.model.loading && publicRoomsListView.count === 0
text: i18nc("@info:label", "No public rooms found")
}
}
Component {
id: manualRoomDialog
ManualRoomDialog {}
}
QtObject {
id: _private
function openManualRoomDialog() {
let dialog = manualRoomDialog.createObject(applicationWindow().overlay, {connection: root.connection});
dialog.roomSelected.connect((roomId, displayName, avatarUrl, alias, topic, memberCount, isJoined) => {
root.roomSelected(roomId, displayName, avatarUrl, alias, topic, memberCount, isJoined);
root.closeDialog();
});
dialog.open();
}
}
}

View File

@@ -18,12 +18,11 @@ import org.kde.neochat
MessageDelegate { MessageDelegate {
id: root id: root
property alias room: liveLocationModel.room
bubbleContent: ColumnLayout { bubbleContent: ColumnLayout {
LiveLocationsModel { LiveLocationsModel {
id: liveLocationModel id: liveLocationModel
eventId: root.eventId eventId: root.eventId
room: root.room
} }
MapView { MapView {
id: mapView id: mapView

View File

@@ -29,6 +29,11 @@ import org.kde.neochat.config
TimelineDelegate { TimelineDelegate {
id: root id: root
/**
* @brief The NeoChatRoom the delegate is being displayed in.
*/
required property NeoChatRoom room
/** /**
* @brief The index of the delegate in the model. * @brief The index of the delegate in the model.
*/ */
@@ -244,8 +249,6 @@ TimelineDelegate {
*/ */
readonly property alias hovered: bubble.hovered readonly property alias hovered: bubble.hovered
required property NeoChatConnection connection
/** /**
* @brief Open the context menu for the message. * @brief Open the context menu for the message.
*/ */
@@ -326,7 +329,7 @@ TimelineDelegate {
Component.onCompleted: { Component.onCompleted: {
if (root.isReply && root.replyDelegateType === DelegateType.Other) { if (root.isReply && root.replyDelegateType === DelegateType.Other) {
currentRoom.loadReply(root.eventId, root.replyId) root.room.loadReply(root.eventId, root.replyId)
} }
} }
@@ -437,7 +440,7 @@ TimelineDelegate {
visible: root.showReactions visible: root.showReactions
model: root.reaction model: root.reaction
onReactionClicked: (reaction) => currentRoom.toggleReaction(root.eventId, reaction) onReactionClicked: (reaction) => root.room.toggleReaction(root.eventId, reaction)
} }
AvatarFlow { AvatarFlow {
Layout.alignment: Qt.AlignRight Layout.alignment: Qt.AlignRight

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