Compare commits

...

101 Commits

Author SHA1 Message Date
Tobias Fella
43e5adee7e Use AccountSettingsGroup 2025-10-27 16:34:03 -04:00
James Graham
f65e9f7599 Fix the chatbar so that the icons show on android. Overring the contentItem was what was causing them to disappear. 2025-10-27 16:13:54 -04:00
l10n daemon script
4a39810923 GIT_SILENT Sync po/docbooks with svn 2025-10-26 01:38:47 +00:00
l10n daemon script
6720e2dfaa GIT_SILENT Sync po/docbooks with svn 2025-10-25 01:39:53 +00:00
l10n daemon script
eb1bf2ed2b GIT_SILENT Sync po/docbooks with svn 2025-10-24 01:39:25 +00:00
l10n daemon script
cc26b1b62f GIT_SILENT Sync po/docbooks with svn 2025-10-23 01:38:07 +00:00
l10n daemon script
59c2c88eb6 GIT_SILENT made messages (after extraction) 2025-10-22 00:43:30 +00:00
l10n daemon script
99e8fd16eb GIT_SILENT Sync po/docbooks with svn 2025-10-21 01:39:43 +00:00
l10n daemon script
eaa4bdb0b7 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"
2025-10-21 01:34:16 +00:00
l10n daemon script
892b61e6bb GIT_SILENT Sync po/docbooks with svn 2025-10-20 01:41:26 +00:00
Nicolas Fella
6d56b673c4 Don't call direct messages friends
Not every person I communicate with is necessarily my friend

Use a more neutral term

BUG: 480167
2025-10-19 19:42:21 +02:00
l10n daemon script
f59e5a4b3c GIT_SILENT Sync po/docbooks with svn 2025-10-19 01:42:36 +00:00
Nicolas Fella
16e3fd4476 Don't show direct messages in room list when not in a space
2b0251c593 changed the logic to also show direct messages inside a space

However this also causes direct messages to be shown when *not* inside a space, which is undesirable
2025-10-18 15:02:07 +02:00
l10n daemon script
82d0fdefd2 GIT_SILENT Sync po/docbooks with svn 2025-10-18 02:03:22 +00:00
Tobias Fella
cbde14d58b Cleanup DevicesPage 2025-10-17 12:23:58 +00:00
l10n daemon script
f3c37e4304 GIT_SILENT Sync po/docbooks with svn 2025-10-17 01:57:08 +00:00
l10n daemon script
369008cec3 GIT_SILENT Sync po/docbooks with svn 2025-10-16 01:40:30 +00:00
l10n daemon script
1a2af57901 GIT_SILENT Sync po/docbooks with svn 2025-10-15 01:39:22 +00:00
l10n daemon script
dbb41d061c 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"
2025-10-15 01:33:59 +00:00
l10n daemon script
d1d3c43c6f GIT_SILENT Sync po/docbooks with svn 2025-10-14 01:40:53 +00:00
Paul Brown
ba2b1529ea Edit org.kde.neochat.appdata.xml 2025-10-13 10:50:10 +00:00
l10n daemon script
b5150f82f0 GIT_SILENT Sync po/docbooks with svn 2025-10-13 01:40:08 +00:00
Tobias Fella
00c81de035 Fix pushing welcome page onto layer
BUG: 510285
2025-10-12 10:58:21 +00:00
Tobias Fella
52d4451932 Add state event texts for some element-specific widget events 2025-10-12 10:49:45 +00:00
l10n daemon script
3efc11b9aa GIT_SILENT Sync po/docbooks with svn 2025-10-12 01:39:29 +00:00
James Graham
5fd9f07664 Some further belt and braces to ensure that we don't crash in the timeline
Some further belt and braces to ensure that we don't crash in the timeline. This does the following:
- Checks all callback inputs
- Makes sure that any objects are cleaned up from incubators when deleting the delegate as it turns out that clear() doesn't do this. Hopefully this stops callbacks into an already deleted parent.

Hopefully helps with https://crash-reports.kde.org/organizations/kde/issues/261627/events/0b6bef500c5641e1924aa0bc2b0c11bd/
2025-10-11 18:11:57 +00:00
l10n daemon script
bec9f36bce GIT_SILENT Sync po/docbooks with svn 2025-10-11 10:19:58 +00:00
l10n daemon script
a631acc0ac GIT_SILENT Sync po/docbooks with svn 2025-10-10 01:38:30 +00:00
l10n daemon script
b729aaf6ee GIT_SILENT Sync po/docbooks with svn 2025-10-09 01:43:02 +00:00
Tobias Fella
94b7fc5cdf Fix delegate menu for state events 2025-10-08 21:50:56 +00:00
Marco Martin
ec5355d86e Push to PageRow on mobile too
This fixes some glitches during animation, which had the background
disappearing when the pop animation was running,
and now uses always the pagerow for viewing the room.

This depends from the new PageRow animation on mobile,
https://invent.kde.org/frameworks/kirigami/-/merge_requests/1925
which makes all applications coherent with an application which
looks a bit more like the one on most android apps.

Also, the swipe back gesture now will work there too
2025-10-08 15:44:46 +00:00
Arno Rehn
1a43d15c6d EventHandler: Acknowledge that non-room-events can have a body as well
Specifically, in the current architecture, these can be EncryptedEvents
which are redactions of previous events. These will have artifical bodies.
2025-10-08 14:01:48 +00:00
James Graham
7356a68f4c Hopefully this stops any crashes around QuickActions.qml and EmojiDialog.qml
Hopefully this stops any crashes around QuickActions.qml and EmojiDialog.qml. Best I can guess this is some race condition where QuickActions are deleted in the time it takes to instnatiate the EmojiDialog popup. I've also rearranged the updateQuickActions function to stop a possible race condition there.

BUG: 509484
2025-10-08 13:07:02 +00:00
Tobias Fella
1070427a0d Try fixing crash in QConcatenateTablesProxyModel 2025-10-08 11:39:39 +00:00
l10n daemon script
196ef535ca GIT_SILENT Sync po/docbooks with svn 2025-10-08 01:40:26 +00:00
Tobias Fella
88fd173829 Show a placeholder message if there are no extensions 2025-10-07 22:43:18 +02:00
Arno Rehn
8b0698c670 Support adding Jitsi and removing widgets 2025-10-07 21:58:34 +02:00
Arno Rehn
5b07a0ff45 Add jitsi button to RoomPage 2025-10-07 21:58:34 +02:00
Arno Rehn
ba1d175d67 Add "Extensions" item to the room info 2025-10-07 21:58:34 +02:00
Arno Rehn
36ce55e892 Add WidgetModel 2025-10-07 21:56:18 +02:00
Arno Rehn
ba4a83d38c Don't switch to global DMs when space contains DM 2025-10-06 14:14:04 +00:00
Arno Rehn
5c49c35b82 Space overview: Fix displayed name of direct chats
When in the space overview page, direct chats were previously only
shown with their room ID. Fix this by getting the actual NeoChatRoom
and its displayName.
2025-10-06 14:14:04 +00:00
Arno Rehn
2b0251c593 Room list: Show direct chats in spaces
Direct Chats can be added to spaces as well. No need to exclude them.
2025-10-06 14:14:04 +00:00
l10n daemon script
3b08d0f382 GIT_SILENT Sync po/docbooks with svn 2025-10-06 01:43:51 +00:00
l10n daemon script
f64e8a3192 GIT_SILENT Sync po/docbooks with svn 2025-10-04 15:09:41 +00:00
Tobias Fella
eab45e761a Use KirigamiApp 2025-10-03 11:32:15 +00:00
l10n daemon script
0f03290c57 GIT_SILENT Sync po/docbooks with svn 2025-10-03 01:42:04 +00:00
Tobias Fella
59f87bb2c2 Set deprecation level 2025-10-02 16:43:27 +02:00
Tobias Fella
9afc10160d Raise compiler settings level to 6.17 2025-10-02 16:19:01 +02:00
Tobias Fella
cfc7f50a1f MessageContentModel: Guard data() against being called when there is no room 2025-10-01 23:48:49 +02:00
Heiko Becker
a2fc00365e GIT_SILENT Update Appstream for new release
(cherry picked from commit debbe8e478)
2025-10-01 00:40:10 +02:00
Joshua Goins
466cfd971d Fix room succession not actually working
I broke this in 409b6da160 making it not
possible to join a succeeding room. I decided to keep the old behavior
of not prompting you to join, because:

1. You are intentionally choosing to tap the button.
2. Room successions aren't random, a privileged user in the room is
creating it and there's already some implicit existing trust

(Also the current join confirmation dialog isn't useful enough yet to
make an actual informed decision anyway.)
2025-09-30 16:05:36 -04:00
Joshua Goins
8e83febb24 Remove some dead code from MessageModel::createEventObjects
This senderId variable doesn't seem to be used in the current iteration
of this function, lets remove it.
2025-09-30 12:07:49 -04:00
l10n daemon script
12072b0a73 GIT_SILENT Sync po/docbooks with svn 2025-09-30 01:47:20 +00:00
l10n daemon script
83b1daac07 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"
2025-09-30 01:36:30 +00:00
Arno Rehn
981ea053a6 Pinned messages: Decrypt events before handling them 2025-09-29 18:59:53 +02:00
Arno Rehn
e41fd7d986 PollHandler: Decrypt EncryptedEvents
Related events for a poll will be EncryptedEvents in an encrypted
room. Decrypt them before handling.
2025-09-29 18:59:53 +02:00
Arno Rehn
a88a82ca08 PollHandler: Correctly handle paginated events 2025-09-29 18:59:20 +02:00
Arno Rehn
23dc88df60 PollComponent: Apply word wrapping on long messages
Long messages were not wrapped and would overflow.
Correctly apply word wrapping and align all elements on the top of a
row.
2025-09-29 18:58:57 +02:00
l10n daemon script
520b1daeec GIT_SILENT Sync po/docbooks with svn 2025-09-29 01:41:25 +00:00
l10n daemon script
cbe7ace32d GIT_SILENT Sync po/docbooks with svn 2025-09-28 01:46:45 +00:00
l10n daemon script
2b4471ea91 GIT_SILENT Sync po/docbooks with svn 2025-09-27 01:40:14 +00:00
Joshua Goins
88e1e1dd2a Add the ability to scroll up in the message search window
This wasn't a thing yet, so the search experience compared to something
like Element Web was much worse. Now you can scroll back as far as any
other client can!

I had to do a bit of refactoring to stop resetting the model all the
time, and append instead. Otherwise, it was quite straightforward to
implement.
2025-09-26 17:49:22 -04:00
Joshua Goins
94ea1305b2 Fix some miscellaneous weirdness around push notifications
For some reason, I *stopped* the timer if we get a push message - which
makes *no* sense. It meant that NeoChat kept running in the background
for basically no reason.

We can also make the NotificationsManager::postPushNotification function
static and avoid creating some singletons.
2025-09-26 15:14:05 -04:00
Joshua Goins
960377838d Replace KDBusService usage with QDBusConnection when D-Bus activated
This is lighter and more reliable when it comes to being activated by
KUnifiedPush. We also don't need all of the features of KDBusService
here.
2025-09-26 15:14:05 -04:00
Joshua Goins
a48e8662d6 Remove useless translated string from dbus-activated CLI option
It never gets shown, so let's not waste our translators time.
2025-09-26 15:12:56 -04:00
l10n daemon script
9c90a38efc GIT_SILENT Sync po/docbooks with svn 2025-09-26 01:39:23 +00:00
l10n daemon script
8696a24127 GIT_SILENT Sync po/docbooks with svn 2025-09-25 01:40:55 +00:00
l10n daemon script
f2d1b4c1e1 GIT_SILENT Sync po/docbooks with svn 2025-09-24 01:40:47 +00:00
Laurent Montel
4cdc2b5e58 GIT_SILENT: Don't duplicate headers between .h/.cpp 2025-09-22 04:43:02 +00:00
l10n daemon script
e26f02d9e2 GIT_SILENT Sync po/docbooks with svn 2025-09-22 01:38:51 +00:00
l10n daemon script
5d83fe9a1d GIT_SILENT Sync po/docbooks with svn 2025-09-21 01:41:04 +00:00
l10n daemon script
6a506f237a GIT_SILENT Sync po/docbooks with svn 2025-09-20 01:41:10 +00:00
l10n daemon script
3a5a0153d8 GIT_SILENT Sync po/docbooks with svn 2025-09-18 01:40:38 +00:00
l10n daemon script
27519b8788 GIT_SILENT Sync po/docbooks with svn 2025-09-17 01:39:11 +00:00
l10n daemon script
dd45fe16a5 GIT_SILENT Sync po/docbooks with svn 2025-09-16 01:41:59 +00:00
Vlad Zahorodnii
161815acff Fix inserting UserListModel items without beginInsertRows()
If an item is added, the corresponding code should be wrapped with
beginInsertRows() and endInsertRows(), otherwise proxy or filter models
can end up with corrupted internal state.

m_members.insert() in refreshMember() should be unnecessary because
if pos is not the same as m_members.size(), then it means there's already
a member.id() item in the member list.
2025-09-15 18:04:29 +00:00
Tobias Fella
438edf2155 Fix qml warnings in ImageEditorPage 2025-09-15 16:13:49 +02:00
Tobias Fella
6607a4b72c Fix qml warnings in ChatBar 2025-09-15 16:11:20 +02:00
Tobias Fella
8f0c4ab133 Fix warning in FullScreenMap 2025-09-15 16:06:58 +02:00
Tobias Fella
78e5cd51cd remove unused imports 2025-09-15 16:05:22 +02:00
Tobias Fella
f5da44655e Fix qml warnings in SpaceDrawer 2025-09-15 16:03:38 +02:00
Tobias Fella
a91863c60d Fix qml warnings in AccountSwitchDialog 2025-09-15 14:39:40 +02:00
Tobias Fella
29e3a09aba Fix qml warning 2025-09-15 14:29:55 +02:00
Tobias Fella
a59952f189 Fix qml warnings in AccountData 2025-09-15 14:24:42 +02:00
Tobias Fella
b169da25ab Fix qml warnings in RoomSortParameterDialog 2025-09-15 14:21:47 +02:00
Tobias Fella
833e357d70 Fix qml warnings in login module 2025-09-15 14:15:38 +02:00
Tobias Fella
f9bf2b8f7a Fix warnings in NeoChatMaximizeComponent 2025-09-15 14:06:17 +02:00
Tobias Fella
7b27579f2d Fix qml warnings in ExploreRoomsPage 2025-09-15 13:54:43 +02:00
Tobias Fella
edb9d9f54e Remove unused import 2025-09-15 13:51:23 +02:00
Tobias Fella
abf9a486d0 Fix qml warnings in RoomData 2025-09-15 13:49:08 +02:00
Tobias Fella
b0bc19c055 Fix qml warnings in RoomInformation 2025-09-15 13:41:32 +02:00
Tobias Fella
71776ef275 Fix qml warnings in TypingPane 2025-09-15 13:30:55 +02:00
Tobias Fella
8dc7d1d39d Fix most qml warnings in StateKeys 2025-09-15 13:25:41 +02:00
Tobias Fella
adb9e59503 Fix qml warnings 2025-09-15 13:21:17 +02:00
Tobias Fella
518044e34a Fix qml warning 2025-09-15 13:17:32 +02:00
Tobias Fella
d6d747bb99 Fix qml warnings in EmoticonFormCard 2025-09-15 13:15:59 +02:00
l10n daemon script
f390702a7a GIT_SILENT Sync po/docbooks with svn 2025-09-15 01:39:42 +00:00
Tobias Fella
e056360ddd Various fixes for ExportKeysDialog 2025-09-14 18:31:26 +02:00
l10n daemon script
85163791ce GIT_SILENT Sync po/docbooks with svn 2025-09-14 01:39:17 +00:00
l10n daemon script
b5a853bc96 GIT_SILENT Sync po/docbooks with svn 2025-09-13 01:46:27 +00:00
134 changed files with 30791 additions and 20298 deletions

View File

@@ -24,7 +24,7 @@ set(CMAKE_MODULE_PATH ${ECM_MODULE_PATH} ${CMAKE_SOURCE_DIR}/cmake)
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(KDE_COMPILERSETTINGS_LEVEL 6.0)
set(KDE_COMPILERSETTINGS_LEVEL 6.17)
include(FeatureSummary)
include(ECMSetupVersion)
@@ -39,6 +39,7 @@ include(ECMCheckOutboundLicense)
include(ECMQtDeclareLoggingCategory)
include(ECMAddAndroidApk)
include(ECMQmlModule)
include(ECMDeprecationSettings)
include(GenerateExportHeader)
include(ECMGenerateHeaders)
if (NOT ANDROID)
@@ -51,6 +52,8 @@ endif()
set(QUOTIENT_FORCE_NAMESPACED_INCLUDES TRUE)
ecm_set_disabled_deprecation_versions(Qt 6.9.0 KF 6.17.0)
ecm_setup_version(${PROJECT_VERSION}
VARIABLE_PREFIX NEOCHAT
VERSION_HEADER ${CMAKE_CURRENT_BINARY_DIR}/neochat-version.h
@@ -75,7 +78,7 @@ set_package_properties(KF6Kirigami PROPERTIES
TYPE REQUIRED
PURPOSE "Kirigami application UI framework"
)
find_package(KF6KirigamiAddons 1.6.0 REQUIRED)
find_package(KF6KirigamiAddons 1.10.0 REQUIRED)
if (UNIX AND NOT APPLE AND NOT ANDROID AND NOT NEOCHAT_FLATPAK AND NOT NEOCHAT_APPIMAGE)
find_package(KF6 ${KF_MIN_VERSION} REQUIRED COMPONENTS Purpose)

View File

@@ -4,15 +4,12 @@
#include "server.h"
#include <QFile>
#include <QHttpServer>
#include <QHttpServerResponder>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QNetworkReply>
#include <QSslCertificate>
#include <QSslKey>
#include <QSslServer>
#include <QUuid>
#include <Quotient/networkaccessmanager.h>

View File

@@ -41,6 +41,7 @@
<name xml:lang="pl">NeoChat</name>
<name xml:lang="pt">NeoChat</name>
<name xml:lang="pt-BR">NeoChat</name>
<name xml:lang="ro">NeoChat</name>
<name xml:lang="ru">NeoChat</name>
<name xml:lang="sa">नवचैट्</name>
<name xml:lang="sk">NeoChat</name>
@@ -75,6 +76,7 @@
<summary xml:lang="nn">Prat med via Matrix</summary>
<summary xml:lang="pl">Rozmawiaj na Matriksie</summary>
<summary xml:lang="pt-BR">Bate-papo na Matrix</summary>
<summary xml:lang="ro">Discutați pe Matrix</summary>
<summary xml:lang="ru">Общение в Matrix</summary>
<summary xml:lang="sa">Matrix इत्यत्र गपशपं कुर्वन्तु</summary>
<summary xml:lang="sl">Klepet na Matrixu</summary>
@@ -109,6 +111,7 @@
<p xml:lang="nn">NeoChat er ein prateapp som lèt deg bruka all funksjonalitet i Matrix-nettverket. Du kan utveksla tekst, lyd og videoar med vennar, familie og kollegaar på ein trygg måte.</p>
<p xml:lang="pl">NoeChat to aplikacja do rozmów, która umożliwia wykorzystanie wszystkich możliwości Matriksa. Umożliwia wysyłanie wiadomości tekstowych, filmów i dźwięków w bezpieczny sposób do twojej rodziny, kolegów i przyjaciół.</p>
<p xml:lang="pt-BR">O NeoChat é um aplicativo de bate-papo que permite que você aproveite ao máximo a rede Matrix. Ele oferece uma maneira segura de enviar mensagens de texto, vídeos e arquivos de áudio para sua família, colegas e amigos.</p>
<p xml:lang="ro">NeoChat e o aplicație de discuții ce vă ajută să profitați din plin de rețeaua Matrix. Aceasta oferă o modalitate sigură de a trimite mesaje textuale, videoclipuri și fișiere audio familiei, colegilor și prietenilor.</p>
<p xml:lang="ru">NeoChat — приложение для общения, предоставляющее все преимущества сети Matrix. С его помощью можно безопасно отправлять текстовые сообщения, видеозаписи и звуковые файлы родственникам, коллегам и друзьям.</p>
<p xml:lang="sa">NeoChat इति एकं गपशप-अनुप्रयोगं यत् भवान् Matrix-जालस्य पूर्णं लाभं ग्रहीतुं शक्नोति । एतत् भवन्तं भवतः परिवाराय, सहकारिभ्यः, मित्रेभ्यः च पाठसन्देशान्, भिडियो, श्रव्यसञ्चिकाः च प्रेषयितुं सुरक्षितं मार्गं प्रदाति ।</p>
<p xml:lang="sl">NeoChat je aplikacija za klepet, ki vam omogoča, da v celoti izkoristite omrežje Matrix. Zagotavlja vam varen način za pošiljanje besedilnih sporočil, videoposnetkov in zvočnih datotek vaši družini, sodelavcem in prijateljem.</p>
@@ -142,6 +145,7 @@
<p xml:lang="pl">NeoChat w zamyśle ma być pełnowartościową aplikacją wg wytycznych Matriksa. Z tego powodu, wszystko, co jest obecnie w stabilnych wytycznych z pominięciem VoIP, wątków i niektórych części szyfrowania Użytkownik-do-Użytkownika są obecnie obsługiwane. Pominięto też kilka mniejszych rzeczy ze względu na ciągły rozwój wytycznych Matriksa, lecz celem nadal jest zapewnienie obsługi wszystkich wytycznych.</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-BR">O NeoChat pretende ser um aplicativo completo para a especificação Matrix. Dessa forma, tudo na especificação estável atual, com as notáveis exceções de VoIP, tópicos e alguns aspectos da criptografia de ponta a ponta, é suportado. Há algumas outras pequenas omissões devido ao fato de a especificação Matrix estar em constante evolução, mas o objetivo continua sendo fornecer suporte eventual para toda a especificação.</p>
<p xml:lang="ro">NeoChat vrea să fie o aplicație completă pentru specificațiile Matrix. Astfel, susține tot ce se găsește acum în specificațiile stabile cu excepția VoIP, a firelor de discuții, și a unor părți din criptarea punct-la-punct. Sunt și câteva omisiuni minore din cauza faptului că specificația Matrix evoluează continuu, dar scopul rămâne acela de a implementa întreaga specificație.</p>
<p xml:lang="ru">Целью создания NeoChat является полноценная реализация программы для спецификации Matrix. Как следствие, реализовано всё в текущей стабильной спецификации (за исключением голосовой интернет-связи, потоков и некоторых аспектов сквозного шифрования). Есть также несколько других незначительных пробелов, обусловленных постоянными изменениями спецификации Matrix. Тем не менее, стоит задача в итоге предоставить полную поддержку спецификации.</p>
<p xml:lang="sa">NeoChat इत्यस्य उद्देश्यं Matrix विनिर्देशस्य कृते पूर्णतया विशेषतायुक्तः अनुप्रयोगः भवितुम् अस्ति । यथा तथा वर्तमानस्थिरविनिर्देशे सर्वं VoIP इत्यस्य उल्लेखनीयअपवादैः सह, थ्रेड्स तथा च End-to-End Encryption इत्यस्य केचन पक्षाः समर्थिताः सन्ति । अन्ये कतिचन लघु लोपाः सन्ति यतोहि Matrix spec निरन्तरं विकसितः अस्ति परन्तु उद्देश्यं सम्पूर्ण spec कृते अन्ततः समर्थनं प्रदातुं अवशिष्टम् अस्ति</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>
@@ -175,6 +179,7 @@
<p xml:lang="pl">Ze względu na sposób rozwoju Matriksa, NeoChat obsługuje także kilka niestabilnych możliwości. Obecnie są to:</p>
<p xml:lang="pt">Devido à natureza do desenvolvimento da especificação do Matrix, o NeoChat também suporta diversas funcionalidades instáveis. De momento são:</p>
<p xml:lang="pt-BR">Devido à natureza do desenvolvimento da especificação Matrix, o NeoChat também suporta diversos recursos instáveis. Atualmente, são eles:</p>
<p xml:lang="ro">Datorită modului de dezvoltare a specificațiilor Matrix, NeoChat susține și numeroase caracteristici nestabile. Acum, acestea sunt:</p>
<p xml:lang="ru">В силу природы разработки спецификации Matrix в NeoChat тоже предусмотрена поддержка многочисленных нестабильных возможностей. В текущей версии это следующие возможности:</p>
<p xml:lang="sa">Matrix विनिर्देशविकासस्य प्रकृतेः कारणात् NeoChat अपि अनेकानाम् अस्थिरविशेषतानां समर्थनं करोति । सम्प्रति एते सन्ति :</p>
<p xml:lang="sl">Zaradi narave razvoja specifikacije Matrixa NeoChat podpira tudi številne nestabilne zmožnosti. Trenutno so to:</p>
@@ -209,6 +214,7 @@
<li xml:lang="pl">Ankiety - MSC3381</li>
<li xml:lang="pt">Inquéritos - MSC3381</li>
<li xml:lang="pt-BR">Enquetes - MSC3381</li>
<li xml:lang="ro">Sondaje - MSC3381</li>
<li xml:lang="ru">Голосования — MSC3381</li>
<li xml:lang="sa">मतदान - MSC3381</li>
<li xml:lang="sl">Polls - MSC3381</li>
@@ -242,6 +248,7 @@
<li xml:lang="pl">Paczki naklejek - MSC2545</li>
<li xml:lang="pt">Pacotes de Autocolantes - MSC2545</li>
<li xml:lang="pt-BR">Pacotes de Stickers - MSC2545</li>
<li xml:lang="ro">Colecții de abțibilduri - MSC2545</li>
<li xml:lang="ru">Наборы стикеров — MSC2545</li>
<li xml:lang="sa">स्टिकर पैक - MSC2545</li>
<li xml:lang="sl">Sticker Packs - MSC2545</li>
@@ -275,6 +282,7 @@
<li xml:lang="pl">Wydarzenia w miejscach - MSC3488</li>
<li xml:lang="pt">Eventos com Localizações - MSC3488</li>
<li xml:lang="pt-BR">Localização de eventos - MSC3488</li>
<li xml:lang="ro">Evenimente de amplasare - MSC3488</li>
<li xml:lang="ru">События местоположения — MSC3488</li>
<li xml:lang="sa">स्थान घटनाएँ - MSC3488</li>
<li xml:lang="sl">Location Events - MSC3488</li>
@@ -312,7 +320,7 @@
<value key="KDE::windows_store::StoreLogoSquare">https://invent.kde.org/network/neochat/-/raw/master/icons/windows/storelogo-1080x1080.png</value>
<value key="KDE::windows_store::Icon">https://invent.kde.org/network/neochat/-/raw/master/icons/300-apps-neochat.png</value>
<value key="KDE::windows_store::PromotionalArt16x9">https://invent.kde.org/network/neochat/-/raw/master/icons/windows/promoimage-1920x1080.png</value>
<value key="KDE::supporters">Tanguy Fardet;[dabe](https://freeradical.zone/@dabe);[lengau](https://mastodon.world/@lengau);Joshua Strobl;Stuart Turton</value>
<value key="KDE::supporters">.</value>
</custom>
<launchable type="desktop-id">org.kde.neochat.desktop</launchable>
<screenshots>
@@ -344,6 +352,7 @@
<caption xml:lang="pl">Główny widok z wykazem pokojów, rozmowami i szczegółami pokojów</caption>
<caption xml:lang="pt">A área principal com a lista de salas e com informações sobre a conversa e a sala</caption>
<caption xml:lang="pt-BR">Visão principal com lista de salas, bate-papo e informações sobre as salas</caption>
<caption xml:lang="ro">Vederea principală cu lista de camere, discuție, și informații despre cameră</caption>
<caption xml:lang="ru">Главное окно со списком комнат, чатом и информацией о комнате</caption>
<caption xml:lang="sa">कक्षसूची, गपशपः, कक्षसूचना च सह मुख्यदृश्यम्</caption>
<caption xml:lang="sl">Glavni pogled s seznamom sob, klepetom in informacijami o sobah</caption>
@@ -380,6 +389,7 @@
<caption xml:lang="nn">Oppdag nye fellesskap med Matrix Spaces</caption>
<caption xml:lang="pl">Odkrywaj nowe społeczności w Przestrzeniach Matriksa</caption>
<caption xml:lang="pt-BR">Descubra novas comunidades com os Espaços Matrix</caption>
<caption xml:lang="ro">Descoperiți comunități noi cu Spații Matrix</caption>
<caption xml:lang="ru">Поиск новых сообществ с помощью Matrix Spaces</caption>
<caption xml:lang="sa">Matrix Spaces इत्यनेन सह नूतनानां समुदायानाम् अन्वेषणं कुर्वन्तु</caption>
<caption xml:lang="sl">Odkrijte nove skupnosti z Matrix Spaces</caption>
@@ -424,6 +434,7 @@
<caption xml:lang="pl">Główny widok z wykazem pokojów, rozmowami i szczegółami pokojów</caption>
<caption xml:lang="pt">A área principal com a lista de salas e com informações sobre a conversa e a sala</caption>
<caption xml:lang="pt-BR">Visão principal com lista de salas, bate-papo e informações sobre as salas</caption>
<caption xml:lang="ro">Vederea principală cu lista de camere, discuție, și informații despre cameră</caption>
<caption xml:lang="ru">Главное окно со списком комнат, чатом и информацией о комнате</caption>
<caption xml:lang="sa">कक्षसूची, गपशपः, कक्षसूचना च सह मुख्यदृश्यम्</caption>
<caption xml:lang="sl">Glavni pogled s seznamom sob, klepetom in informacijami o sobah</caption>
@@ -462,6 +473,7 @@
<caption xml:lang="pl">Ekran logowania</caption>
<caption xml:lang="pt">Ecrã de autenticação</caption>
<caption xml:lang="pt-BR">Tela de login</caption>
<caption xml:lang="ro">Ecran de autentificare</caption>
<caption xml:lang="ru">Окно входа</caption>
<caption xml:lang="sa">लॉगिन् स्क्रीन</caption>
<caption xml:lang="sl">Prijavni zaslon</caption>
@@ -476,6 +488,7 @@
<content_attribute id="social-chat">intense</content_attribute>
</content_rating>
<releases>
<release version="25.08.2" date="2025-10-09"/>
<release version="25.08.1" date="2025-09-11"/>
<release version="25.08.0" date="2025-08-14"/>
<release version="25.04.3" date="2025-07-03"/>

View File

@@ -112,6 +112,7 @@ Comment[lv]=Tērzējiet „Matrix“ tīklā
Comment[nl]=Chat op Matrix
Comment[pl]=Rozmawiaj na Matriksie
Comment[pt_BR]=Bate papo na Matrix
Comment[ro]=Discutați pe Matrix
Comment[ru]=Общение в Matrix
Comment[sa]=Matrix इत्यत्र गपशपं कुर्वन्तु
Comment[sl]=Klepet na Matrixu

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

6863
po/ro/neochat.po Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -204,6 +204,8 @@ target_link_libraries(neochat PUBLIC
KF6::SonnetCore
KF6::IconThemes
KF6::ItemModels
KF6::I18nQml
KirigamiApp
QuotientQt6
Login
Rooms
@@ -325,6 +327,7 @@ if(ANDROID)
"kt-restore-defaults-symbolic"
"user-symbolic"
"mark-location-symbolic"
"amarok_playcount"
${KIRIGAMI_ADDONS_ICONS}
)

View File

@@ -17,7 +17,6 @@
#include <Quotient/qt_connection_util.h>
#include <Quotient/settings.h>
#include "accountmanager.h"
#include "enums/roomsortparameter.h"
#include "general_logging.h"
#include "mediasizehelper.h"
@@ -26,9 +25,7 @@
#include "models/roomlistmodel.h"
#include "models/roomtreemodel.h"
#include "neochatconfig.h"
#include "neochatconnection.h"
#include "neochatroom.h"
#include "notificationsmanager.h"
#include "proxycontroller.h"
#include "roommanager.h"
@@ -309,8 +306,7 @@ void Controller::listenForNotifications()
connect(timer, &QTimer::timeout, qGuiApp, &QGuiApplication::quit);
connect(connector, &KUnifiedPush::Connector::messageReceived, [timer](const QByteArray &data) {
instance().m_notificationsManager.postPushNotification(data);
timer->stop();
NotificationsManager::postPushNotification(data);
});
// Wait five seconds to see if we received any messages or this happened to be an erroneous activation.

View File

@@ -33,13 +33,10 @@
#include <KWindowSystem>
#endif
#if __has_include("KCrash")
#include <KCrash>
#endif
#include <KIconTheme>
#include <KLocalizedContext>
#include <KLocalizedQmlContext>
#include <KLocalizedString>
#include <KirigamiApp>
#include "neochat-version.h"
@@ -104,7 +101,6 @@ Q_DECL_EXPORT
#endif
int main(int argc, char *argv[])
{
KIconTheme::initTheme();
QNetworkProxyFactory::setUseSystemConfiguration(true);
#ifdef HAVE_WEBVIEW
@@ -113,24 +109,10 @@ int main(int argc, char *argv[])
QQuickWindow::setGraphicsApi(QSGRendererInterface::OpenGLRhi);
#endif
#ifdef Q_OS_ANDROID
QGuiApplication app(argc, argv);
QQuickStyle::setStyle(u"org.kde.breeze"_s);
#else
QIcon::setFallbackThemeName("breeze"_L1);
QApplication app(argc, argv);
// Default to org.kde.desktop style unless the user forces another style
if (qEnvironmentVariableIsEmpty("QT_QUICK_CONTROLS_STYLE")) {
QQuickStyle::setStyle(u"org.kde.desktop"_s);
}
#endif
KirigamiApp::App app(argc, argv);
KirigamiApp kirigamiApp;
#ifdef Q_OS_WINDOWS
if (AttachConsole(ATTACH_PARENT_PROCESS)) {
freopen("CONOUT$", "w", stdout);
freopen("CONOUT$", "w", stderr);
}
QApplication::setStyle(u"breeze"_s);
QFont font(u"Segoe UI Emoji"_s);
font.setPointSize(10);
@@ -177,10 +159,6 @@ int main(int argc, char *argv[])
KAboutData::setApplicationData(about);
QGuiApplication::setWindowIcon(QIcon::fromTheme(u"org.kde.neochat"_s));
#if __has_include("KCrash")
KCrash::initialize();
#endif
Connection::setEncryptionDefault(true);
Connection::setDirectChatEncryptionDefault(true);
@@ -205,7 +183,7 @@ int main(int argc, char *argv[])
parser.addOption(testOption);
#ifdef HAVE_KUNIFIEDPUSH
QCommandLineOption dbusActivatedOption(u"dbus-activated"_s, i18n("Internal usage only."));
QCommandLineOption dbusActivatedOption(u"dbus-activated"_s);
dbusActivatedOption.setFlags(QCommandLineOption::Flag::HiddenFromHelp);
parser.addOption(dbusActivatedOption);
#endif
@@ -219,8 +197,14 @@ int main(int argc, char *argv[])
#ifdef HAVE_KUNIFIEDPUSH
if (parser.isSet(dbusActivatedOption)) {
// We want to be replaceable by the main client
KDBusService service(KDBusService::Replace);
#ifdef HAVE_KDBUSADDONS
// We *don't* want to use KDBusService here. I don't know why, but it makes activation super unreliable. We don't really need it anyway.
if (!QDBusConnection::sessionBus().registerService(QStringLiteral("org.kde.neochat"))) {
// Gracefully fail if NeoChat is already running
qWarning() << "NeoChat already running, not sending push notifications.";
return 0;
}
#endif
#ifdef HAVE_RUNNER
// If we are built with KRunner and KUnifiedPush support, we need to do something special.
@@ -279,7 +263,7 @@ int main(int argc, char *argv[])
});
#endif
engine.rootContext()->setContextObject(new KLocalizedContext(&engine));
KLocalization::setupLocalizedContext(&engine);
engine.setNetworkAccessManagerFactory(new NetworkAccessManagerFactory());
if (parser.isSet("ignore-ssl-errors"_L1)) {
@@ -294,7 +278,9 @@ int main(int argc, char *argv[])
engine.addImageProvider(u"blurhash"_s, new BlurhashImageProvider);
engine.loadFromModule("org.kde.neochat", "Main");
if (!kirigamiApp.start("org.kde.neochat", "Main", &engine)) {
return -1;
}
if (!parser.positionalArguments().isEmpty() && !parser.isSet("share"_L1)) {
RoomManager::instance().setUrlArgument(parser.positionalArguments()[0]);

View File

@@ -211,6 +211,7 @@ Name[pa]=ਨਵਾਂ ਸੱਦਾ
Name[pl]=Nowe zaproszenie
Name[pt]=Novo Convite
Name[pt_BR]=Novo convite
Name[ro]=Invitație nouă
Name[ru]=Новое приглашение
Name[sa]=नवीन आमन्त्रणम्
Name[sl]=Novo povabilo
@@ -252,6 +253,7 @@ Comment[pa]=ਰੂਮ ਲਈ ਨਵਾਂ ਸੱਦਾ ਹੈ
Comment[pl]=Dostępna jest nowe zaproszenie do pokoju
Comment[pt]=Existe um novo convite para uma sala
Comment[pt_BR]=Existe um novo convite para uma sala
Comment[ro]=E o nouă invitație la o cameră
Comment[ru]=Доступно новое приглашение в комнату
Comment[sa]=कक्षस्य नूतनं निमन्त्रणम् अस्ति
Comment[sl]=Tam je novo povabilo v sobo
@@ -290,6 +292,7 @@ Name[nl]=Gedeelde
Name[nn]=Del
Name[pl]=Udostępnij
Name[pt_BR]=Compartilhar
Name[ro]=Partajare
Name[ru]=Публикация
Name[sa]=संविभागः
Name[sl]=Deli
@@ -324,6 +327,7 @@ Comment[nl]=Het resultaat van het delen van een stukje inhoud
Comment[nn]=Resultatet av deling av innhald
Comment[pl]=Wynik udostępniania kawałka treści
Comment[pt_BR]=O resultado de compartilhar um conteúdo
Comment[ro]=Rezultatul partajării unei bucăți de conținut
Comment[ru]=Результат публикации данных
Comment[sa]=सामग्रीखण्डस्य साझाकरणस्य परिणामः
Comment[sl]=Rezultat deljenega kosa vsebine

View File

@@ -388,7 +388,7 @@ void NotificationsManager::postPushNotification(const QByteArray &message)
#ifdef HAVE_KIO
auto openAction = notification->addAction(i18n("Open NeoChat"));
connect(openAction, &KNotificationAction::activated, this, [=]() {
connect(openAction, &KNotificationAction::activated, notification, [=]() {
QString properId = roomId;
properId = properId.replace(u"#"_s, QString());
properId = properId.replace(u"!"_s, QString());
@@ -402,8 +402,6 @@ void NotificationsManager::postPushNotification(const QByteArray &message)
connect(notification, &KNotification::closed, qGuiApp, &QGuiApplication::quit);
notification->sendEvent();
m_notifications.insert(roomId, {json["ts"_L1].toVariant().toLongLong(), notification});
} else {
qWarning() << "Skipping unsupported push notification" << type;
}

View File

@@ -53,7 +53,7 @@ public:
/**
* @brief Display a native notification for the given push notification.
*/
void postPushNotification(const QByteArray &message);
static void postPushNotification(const QByteArray &message);
/**
* @brief Handle the notifications for the given connection.

View File

@@ -75,6 +75,7 @@ Comment[nn]=Finn rom i NeoChat
Comment[pl]=Znajdź pokoje w NeoChat
Comment[pt]=Procurar salas no NeoChat
Comment[pt_BR]=Encontrar salas no NeoChat
Comment[ro]=Găsește camere în NeoChat
Comment[ru]=Поиск комнат NeoChat
Comment[sa]=NeoChat इत्यत्र कक्ष्याः अन्वेषणं कुर्वन्तु
Comment[sl]=Najdi sobe v NeoChatu

View File

@@ -61,7 +61,7 @@ Kirigami.Dialog {
}
onClicked: {
pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat.login', 'WelcomePage'), {}, {
((root.QQC2.ApplicationWindow.window as Kirigami.ApplicationWindow).pageStack as Kirigami.PageRow).pushDialogLayer(Qt.createComponent('org.kde.neochat.login', 'WelcomePage'), {}, {
title: i18nc("@title:window", "Login")
});
root.close();
@@ -95,8 +95,8 @@ Kirigami.Dialog {
accountView.decrementCurrentIndex();
}
}
Keys.onEnterPressed: (accountView.currentItem as Delegates.RoundedItemDelegate ?? accountView.footerItem).clicked()
Keys.onReturnPressed: (accountView.currentItem as Delegates.RoundedItemDelegate ?? accountView.footerItem).clicked()
Keys.onEnterPressed: ((accountView.currentItem ?? accountView.footerItem) as Delegates.RoundedItemDelegate).clicked()
Keys.onReturnPressed: ((accountView.currentItem ?? accountView.footerItem) as Delegates.RoundedItemDelegate).clicked()
onVisibleChanged: {
for (let i = 0; i < accountView.count; i++) {

View File

@@ -17,7 +17,7 @@ ApplicationWindow {
property real longitude: NaN
property string asset
property var author
property QtObject liveLocationModel: null
property LiveLocationsModel liveLocationModel: null
flags: Qt.FramelessWindowHint | Qt.WA_TranslucentBackground
visibility: Qt.WindowFullScreen
@@ -59,7 +59,7 @@ ApplicationWindow {
Connections {
target: mapView.map
function onCopyrightLinkActivated() {
function onCopyrightLinkActivated(link: string) {
Qt.openUrlExternally(link);
}
}

View File

@@ -22,12 +22,12 @@ Labs.MenuBar {
Labs.MenuItem {
icon.name: "list-add-user"
text: i18nc("@action:inmenu", "Find your Friends")
text: i18nc("@action:inmenu", "Find User")
enabled: root.connection
onTriggered: root.appWindow.pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat', 'UserSearchPage'), {
connection: root.connection
}, {
title: i18nc("@title", "Find your friends")
title: i18nc("@title", "Find User")
})
}
Labs.MenuItem {

View File

@@ -100,7 +100,8 @@ Kirigami.ApplicationWindow {
function onCurrentRoomChanged() {
if (RoomManager.currentRoom && root.pageStack.depth <= 1 && root.initialized && Kirigami.Settings.isMobile) {
let roomPage = root.pageStack.layers.push(Qt.createComponent('org.kde.neochat', 'RoomPage'));
let roomPage = pageStack.push(Qt.createComponent('org.kde.neochat', 'RoomPage'));
roomPage.forceActiveFocus();
roomPage.backRequested.connect(event => {
RoomManager.clearCurrentRoom();
});

View File

@@ -1,6 +1,8 @@
// SPDX-FileCopyrightText: 2023 James Graham <james.h.graham@protonmail.com>
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
pragma ComponentBehavior: Bound
import QtCore as Core
import QtQuick
import QtQuick.Controls as QQC2
@@ -21,13 +23,13 @@ Components.AlbumMaximizeComponent {
*/
required property NeoChatRoom currentRoom
readonly property string currentEventId: model.data(model.index(content.currentIndex, 0), TimelineMessageModel.EventIdRole)
readonly property string currentEventId: model.data(model.index((content as ListView).currentIndex, 0), TimelineMessageModel.EventIdRole)
readonly property var currentAuthor: model.data(model.index(content.currentIndex, 0), TimelineMessageModel.AuthorRole)
readonly property var currentAuthor: model.data(model.index((content as ListView).currentIndex, 0), TimelineMessageModel.AuthorRole)
readonly property var currentTime: model.data(model.index(content.currentIndex, 0), TimelineMessageModel.TimeRole)
readonly property var currentTime: model.data(model.index((content as ListView).currentIndex, 0), TimelineMessageModel.TimeRole)
readonly property var currentProgressInfo: model.data(model.index(content.currentIndex, 0), TimelineMessageModel.ProgressInfoRole)
readonly property var currentProgressInfo: model.data(model.index((content as ListView).currentIndex, 0), TimelineMessageModel.ProgressInfoRole)
actions: [
ShareAction {
@@ -59,28 +61,28 @@ Components.AlbumMaximizeComponent {
downloadAction: Components.DownloadAction {
onTriggered: {
currentRoom.downloadFile(root.currentEventId, Core.StandardPaths.writableLocation(Core.StandardPaths.CacheLocation) + "/" + root.currentEventId.replace(":", "_").replace("/", "_").replace("+", "_") + currentRoom.fileNameToDownload(root.currentEventId));
root.currentRoom.downloadFile(root.currentEventId, Core.StandardPaths.writableLocation(Core.StandardPaths.CacheLocation) + "/" + root.currentEventId.replace(":", "_").replace("/", "_").replace("+", "_") + root.currentRoom.fileNameToDownload(root.currentEventId));
}
}
playAction: Kirigami.Action {
onTriggered: {
MediaManager.startPlayback();
currentItem.play();
(root.currentItem as Components.VideoMaximizeDelegate).play();
}
}
Connections {
target: MediaManager
function onPlaybackStarted() {
if (currentItem.playbackState === MediaPlayer.PlayingState) {
currentItem.pause();
if ((root.currentItem as Components.VideoMaximizeDelegate).playbackState === MediaPlayer.PlayingState) {
(root.currentItem as Components.VideoMaximizeDelegate).pause();
}
}
}
Connections {
target: currentRoom
target: root.currentRoom
function onFileTransferProgress(id, progress, total) {
if (id == root.currentEventId) {
@@ -123,7 +125,7 @@ Components.AlbumMaximizeComponent {
onItemRightClicked: RoomManager.viewEventMenu(root.currentEventId, root.currentRoom)
onSaveItem: {
var dialog = saveAsDialog.createObject(QQC2.Overlay.overlay);
var dialog = saveAsDialog.createObject(QQC2.Overlay.overlay) as Dialogs.FileDialog;
dialog.selectedFile = currentRoom.fileNameToDownload(root.currentEventId);
dialog.open();
}
@@ -146,7 +148,7 @@ Components.AlbumMaximizeComponent {
if (!selectedFile) {
return;
}
currentRoom.downloadFile(root.currentEventId, selectedFile);
root.currentRoom.downloadFile(root.currentEventId, selectedFile);
}
}
}

View File

@@ -59,11 +59,37 @@ Kirigami.Page {
*/
property MediaMessageFilterModel mediaMessageFilterModel: RoomManager.mediaMessageFilterModel
/**
* @brief The WidgetModel to use.
*
* This model has the list of widgets available in the current room.
*
* @note For loading a room in a different window, override this with a new
* WidgetModel.
*
* @sa WidgetModel
*/
property WidgetModel widgetModel: RoomManager.widgetModel
title: root.currentRoom ? root.currentRoom.displayName : ""
focus: true
padding: 0
actions: [
Kirigami.Action {
tooltip: i18nc("@action:button", "Open Jitsi Meet in browser")
icon.name: "camera-video-symbolic"
onTriggered: {
let url
if (root.widgetModel.jitsiIndex < 0) {
url = root.widgetModel.addJitsiConference();
} else {
let idx = root.widgetModel.index(root.widgetModel.jitsiIndex, 0);
url = root.widgetModel.data(idx, WidgetModel.UrlRole);
}
Qt.openUrlExternally(url);
}
},
Kirigami.Action {
visible: Kirigami.Settings.isMobile || !(root.Kirigami.PageStack.pageStack as Kirigami.PageRow).wideMode
icon.name: "view-right-new"
@@ -226,12 +252,6 @@ Kirigami.Page {
}
}
background: Rectangle {
Kirigami.Theme.colorSet: Kirigami.Theme.View
Kirigami.Theme.inherit: false
color: NeoChatConfig.compactLayout ? Kirigami.Theme.backgroundColor : "transparent"
}
footer: Loader {
id: chatBarLoader
height: active ? (item as ChatBar).implicitHeight : 0

View File

@@ -30,7 +30,7 @@ SearchPage {
*/
required property NeoChatConnection connection
title: i18nc("@action:title", "Find Your Friends")
title: i18nc("@action:title", "Find User")
Component.onCompleted: focusSearch()
@@ -81,7 +81,7 @@ SearchPage {
}
QQC2.Label {
visible: userDelegate.directChatExists
text: i18nc("@info", "Friends")
text: i18nc("@info", "Direct Messages")
textFormat: Text.PlainText
color: Kirigami.Theme.positiveTextColor
}

View File

@@ -9,8 +9,6 @@
#include "controller.h"
#include "eventhandler.h"
#include "models/actionsmodel.h"
#include "models/messagefiltermodel.h"
#include "models/sortfilterroomtreemodel.h"
#include "neochatconfig.h"
#include "neochatconnection.h"
#include "neochatroom.h"
@@ -43,6 +41,7 @@ RoomManager::RoomManager(QObject *parent)
, m_messageFilterModel(new MessageFilterModel(this, m_timelineModel))
, m_mediaMessageFilterModel(new MediaMessageFilterModel(this, m_messageFilterModel))
, m_userListModel(new UserListModel(this))
, m_widgetModel(new WidgetModel(this))
{
#if defined(Q_OS_ANDROID) || defined(Q_OS_IOS) || defined(UBUNTU_TOUCH)
m_isMobile = true;
@@ -55,6 +54,7 @@ RoomManager::RoomManager(QObject *parent)
#endif
connect(this, &RoomManager::currentRoomChanged, this, [this]() {
m_widgetModel->setRoom(m_currentRoom);
m_userListModel->setRoom(m_currentRoom);
m_timelineModel->setRoom(m_currentRoom);
m_sortFilterRoomTreeModel->setCurrentRoom(m_currentRoom);
@@ -197,6 +197,11 @@ void RoomManager::activateUserModel()
m_userListModel->activate();
}
WidgetModel *RoomManager::widgetModel() const
{
return m_widgetModel;
}
void RoomManager::resolveResource(const QString &idOrUri, const QString &action)
{
resolveResource(Uri{idOrUri}, action);
@@ -514,7 +519,14 @@ void RoomManager::setCurrentSpace(const QString &spaceId, bool setRoom)
// This need to happen before the signal so TreeView.expandRecursively() can work nicely.
m_sortFilterRoomTreeModel->setActiveSpaceId(m_currentSpaceId);
m_sortFilterRoomTreeModel->setMode(m_currentSpaceId == u"DM"_s ? SortFilterRoomTreeModel::DirectChats : SortFilterRoomTreeModel::Rooms);
if (m_currentSpaceId == u"DM") {
m_sortFilterRoomTreeModel->setMode(SortFilterRoomTreeModel::DirectChats);
} else if (m_currentSpaceId.isEmpty()) {
m_sortFilterRoomTreeModel->setMode(SortFilterRoomTreeModel::Rooms);
} else {
m_sortFilterRoomTreeModel->setMode(SortFilterRoomTreeModel::All);
}
Q_EMIT currentSpaceChanged();
if (m_connection) {
@@ -578,7 +590,8 @@ void RoomManager::setCurrentRoom(const QString &roomId)
return;
}
if (m_currentRoom->isDirectChat()) {
if (m_currentSpaceId != "DM"_L1) {
const auto roomsInSpace = SpaceHierarchyCache::instance().getRoomListForSpace(m_currentSpaceId, false);
if (!roomsInSpace.contains(m_currentRoom->id()) && m_currentSpaceId != "DM"_L1) {
setCurrentSpace("DM"_L1, false);
}
return;

View File

@@ -22,6 +22,7 @@
#include "models/sortfilterspacelistmodel.h"
#include "models/timelinemodel.h"
#include "models/userlistmodel.h"
#include "models/widgetmodel.h"
#include "neochatroommember.h"
class NeoChatRoom;
@@ -129,6 +130,14 @@ class RoomManager : public QObject, public UriResolverBase
*/
Q_PROPERTY(UserListModel *userListModel READ userListModel CONSTANT)
/**
* @brief The WidgetModel that should be used for room widget visualisation.
*
* @note Available here so that the room page and drawer both have access to the
* same model.
*/
Q_PROPERTY(WidgetModel *widgetModel READ widgetModel CONSTANT)
/**
* @brief Whether a room is currently open in NeoChat.
*
@@ -160,6 +169,8 @@ public:
UserListModel *userListModel() const;
Q_INVOKABLE void activateUserModel();
WidgetModel *widgetModel() const;
/**
* @brief Resolve the given resource.
*
@@ -356,6 +367,7 @@ private:
MediaMessageFilterModel *m_mediaMessageFilterModel;
UserListModel *m_userListModel;
WidgetModel *m_widgetModel;
QPointer<NeoChatConnection> m_connection;

View File

@@ -8,8 +8,6 @@
#include <KWindowSystem>
#include "controller.h"
#include "models/roomlistmodel.h"
#include "models/sortfilterroomlistmodel.h"
#include "roommanager.h"
#include "windowcontroller.h"

View File

@@ -2,6 +2,8 @@
// SPDX-FileCopyrightText: 2020 Noah Davis <noahadvs@gmail.com>
// SPDX-License-Identifier: GPL-2.0-or-later
pragma ComponentBehavior: Bound
import QtCore
import QtQuick
import QtQuick.Controls as QQC2
@@ -87,9 +89,15 @@ QQC2.Control {
displayHint: Kirigami.DisplayHint.IconOnly
onTriggered: {
let dialog = (Clipboard.hasImage ? attachDialog : openFileDialog).createObject(root.QQC2.Overlay.overlay);
dialog.chosen.connect(path => _private.chatBarCache.attachmentPath = path);
dialog.open();
if (Clipboard.hasImage) {
let dialog = attachDialog.createObject(root.QQC2.Overlay.overlay) as AttachDialog;
dialog.chosen.connect(path => _private.chatBarCache.attachmentPath = path);
dialog.open();
} else {
let dialog = openFileDialog.createObject(root.QQC2.Overlay.overlay) as OpenFileDialog;
dialog.chosen.connect(path => _private.chatBarCache.attachmentPath = path);
dialog.open();
}
}
tooltip: text
@@ -122,9 +130,9 @@ QQC2.Control {
displayHint: QQC2.AbstractButton.IconOnly
onTriggered: {
locationChooser.createObject(QQC2.Overlay.overlay, {
(locationChooser.createObject(QQC2.Overlay.overlay, {
room: root.currentRoom
}).open();
}) as LocationChooser).open();
}
tooltip: text
},
@@ -136,9 +144,9 @@ QQC2.Control {
displayHint: QQC2.AbstractButton.IconOnly
onTriggered: {
newPollDialog.createObject(QQC2.Overlay.overlay, {
(newPollDialog.createObject(QQC2.Overlay.overlay, {
room: root.currentRoom
}).open();
}) as NewPollDialog).open();
}
tooltip: text
},
@@ -367,7 +375,9 @@ QQC2.Control {
id: actionDelegate
required property BusyAction modelData
icon.name: modelData.isBusy ? "" : (modelData.icon.name.length > 0 ? modelData.icon.name : modelData.icon.source)
onClicked: modelData.trigger()
onClicked: if (!pieProgress.visible) {
modelData.trigger()
}
padding: Kirigami.Units.smallSpacing
@@ -375,7 +385,9 @@ QQC2.Control {
QQC2.ToolTip.text: modelData.tooltip
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
contentItem: PieProgressBar {
PieProgressBar {
id: pieProgress
anchors.fill: parent
visible: actionDelegate.modelData.isBusy
progress: root.currentRoom.fileUploadingProgress
}
@@ -402,7 +414,7 @@ QQC2.Control {
ReplyComponent {
id: replyComponent
replyContentModel: ContentProvider.contentModelForEvent(root.currentRoom, _private.chatBarCache.replyId, true)
Message.maxContentWidth: replyLoader.item.width
Message.maxContentWidth: (replyLoader.item as Item).width
// When the user replies to a message and the preview is loaded, make sure the text field is focused again
Component.onCompleted: textField.forceActiveFocus(Qt.OtherFocusReason)

View File

@@ -3,14 +3,13 @@
import QtQuick
import QtQuick.Controls as QQC2
import QtQuick.Layouts
import QtCore as Core
import org.kde.kirigami as Kirigami
import org.kde.kquickimageeditor as KQuickImageEditor
Kirigami.Page {
id: rootEditorView
id: root
property bool resizing: false
required property string imagePath
@@ -26,7 +25,7 @@ Kirigami.Page {
function crop() {
const ratioX = editImage.paintedWidth / editImage.nativeWidth;
const ratioY = editImage.paintedHeight / editImage.nativeHeight;
rootEditorView.resizing = false;
root.resizing = false;
imageDoc.crop(selectionTool.selectionX / ratioX, selectionTool.selectionY / ratioY, selectionTool.selectionWidth / ratioX, selectionTool.selectionHeight / ratioY);
}
@@ -43,9 +42,9 @@ Kirigami.Page {
text: i18nc("@action:button Accept image modification", "Accept")
icon.name: "dialog-ok"
onTriggered: {
let newPath = Core.StandardPaths.writableLocation(Core.StandardPaths.CacheLocation) + "/" + (new Date()).getTime() + "." + imagePath.split('.').pop();
let newPath = Core.StandardPaths.writableLocation(Core.StandardPaths.CacheLocation) + "/" + (new Date()).getTime() + "." + root.imagePath.split('.').pop();
if (imageDoc.saveAs(newPath)) {
newPathChanged(newPath);
root.newPathChanged(newPath);
} else {
msg.type = Kirigami.MessageType.Error;
msg.text = i18n("Unable to save file. Check if you have the correct permission to edit the cache directory.");
@@ -80,12 +79,12 @@ Kirigami.Page {
KQuickImageEditor.ImageDocument {
id: imageDoc
path: rootEditorView.imagePath
path: root.imagePath
}
KQuickImageEditor.SelectionTool {
id: selectionTool
visible: rootEditorView.resizing
visible: root.resizing
width: editImage.paintedWidth
height: editImage.paintedHeight
x: editImage.horizontalPadding
@@ -101,7 +100,7 @@ Kirigami.Page {
Connections {
target: selectionTool.selectionArea
function onDoubleClicked() {
rootEditorView.crop();
root.crop();
}
}
}
@@ -119,8 +118,8 @@ Kirigami.Page {
display: QQC2.Button.TextBesideIcon
actions: [
Kirigami.Action {
icon.name: rootEditorView.resizing ? "dialog-cancel" : "transform-crop"
text: rootEditorView.resizing ? i18n("Cancel") : i18nc("@action:button Crop an image", "Crop")
icon.name: root.resizing ? "dialog-cancel" : "transform-crop"
text: root.resizing ? i18n("Cancel") : i18nc("@action:button Crop an image", "Crop")
onTriggered: {
resizeRectangle.width = editImage.paintedWidth;
resizeRectangle.height = editImage.paintedHeight;
@@ -130,38 +129,38 @@ Kirigami.Page {
resizeRectangle.insideY = 100;
resizeRectangle.insideWidth = 100;
resizeRectangle.insideHeight = 100;
rootEditorView.resizing = !rootEditorView.resizing;
root.resizing = !root.resizing;
}
},
Kirigami.Action {
icon.name: "dialog-ok"
visible: rootEditorView.resizing
visible: root.resizing
text: i18nc("@action:button Crop an image", "Crop")
onTriggered: rootEditorView.crop()
onTriggered: root.crop()
},
Kirigami.Action {
icon.name: "object-rotate-left"
text: i18nc("@action:button Rotate an image to the left", "Rotate left")
onTriggered: imageDoc.rotate(-90)
visible: !rootEditorView.resizing
visible: !root.resizing
},
Kirigami.Action {
icon.name: "object-rotate-right"
text: i18nc("@action:button Rotate an image to the right", "Rotate right")
onTriggered: imageDoc.rotate(90)
visible: !rootEditorView.resizing
visible: !root.resizing
},
Kirigami.Action {
icon.name: "object-flip-vertical"
text: i18nc("@action:button Mirror an image vertically", "Flip")
onTriggered: imageDoc.mirror(false, true)
visible: !rootEditorView.resizing
visible: !root.resizing
},
Kirigami.Action {
icon.name: "object-flip-horizontal"
text: i18nc("@action:button Mirror an image horizontally", "Mirror")
onTriggered: imageDoc.mirror(true, false)
visible: !rootEditorView.resizing
visible: !root.resizing
}
]
}

View File

@@ -1,6 +1,8 @@
// SPDX-FileCopyrightText: 2024 Tobias Fella <tobias.fella@kde.org>
// SPDX-License-Identifier: LGPL-2.0-or-later
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Controls as QQC2
import QtQuick.Layouts
@@ -23,8 +25,9 @@ ColumnLayout {
Repeater {
model: root.connection.accountDataEventTypes
delegate: FormCard.FormButtonDelegate {
required property string modelData
text: modelData
onClicked: root.Window.window.pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat', 'MessageSourceSheet'), {
onClicked: (root.Kirigami.PageStack.pageStack as Kirigami.PageRow).pushDialogLayer(Qt.createComponent('org.kde.neochat', 'MessageSourceSheet'), {
sourceText: root.connection.accountDataJsonString(modelData)
}, {
title: i18nc("@title:window", "Event Source"),

View File

@@ -17,6 +17,8 @@ ecm_add_qml_module(Devtools GENERATE_PLUGIN_SOURCE
models/statefiltermodel.cpp
models/statekeysmodel.cpp
models/statemodel.cpp
DEPENDENCIES
QtCore
)
target_include_directories(Devtools PRIVATE ${CMAKE_BINARY_DIR} ${CMAKE_CURRENT_SOURCE_DIR}/models)

View File

@@ -1,6 +1,8 @@
// SPDX-FileCopyrightText: 2022 James Graham <james.h.graham@protonmail.com>
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Layouts
import QtQuick.Window
@@ -25,7 +27,7 @@ ColumnLayout {
description: i18nc("@info", "Click to choose a room");
onClicked: {
let dialog = root.Window.window.pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat', 'ChooseRoomDialog'), {
let dialog = (root.Kirigami.PageStack.pageStack as Kirigami.PageRow).pushDialogLayer(Qt.createComponent('org.kde.neochat', 'ChooseRoomDialog'), {
connection: root.connection,
}, {
title: i18nc("@title:dialog", "Choose Room"),
@@ -49,8 +51,9 @@ ColumnLayout {
id: roomAccountData
model: root.room.accountDataEventTypes
delegate: FormCard.FormButtonDelegate {
required property string modelData
text: modelData
onClicked: root.Window.window.pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat', 'MessageSourceSheet'), {
onClicked: (root.Kirigami.PageStack.pageStack as Kirigami.PageRow).pushDialogLayer(Qt.createComponent('org.kde.neochat', 'MessageSourceSheet'), {
sourceText: root.room.roomAcountDataJson(text)
}, {
title: i18n("Event Source"),
@@ -74,15 +77,18 @@ ColumnLayout {
}
delegate: FormCard.FormButtonDelegate {
text: model.type
description: i18ncp("'Event' being some JSON data, not something physically happening.", "%1 event of this type", "%1 events of this type", model.eventCount)
required property string type
required property int eventCount
required property string stateKey
text: type
description: i18ncp("'Event' being some JSON data, not something physically happening.", "%1 event of this type", "%1 events of this type", eventCount)
onClicked: {
if (model.eventCount === 1) {
openEventSource(model.type, model.stateKey);
if (eventCount === 1) {
root.openEventSource(type, stateKey);
} else {
root.Window.window.pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat.devtools', 'StateKeys'), {
(root.Kirigami.PageStack.pageStack as Kirigami.PageRow).pushDialogLayer(Qt.createComponent('org.kde.neochat.devtools', 'StateKeys'), {
room: root.room,
eventType: model.type
eventType: type
}, {
title: i18nc("'Event' being some JSON data, not something physically happening.", "Event Information")
});
@@ -92,7 +98,7 @@ ColumnLayout {
}
}
function openEventSource(type: string, stateKey: string): void {
onClicked: root.Window.window.pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat', 'MessageSourceSheet'), {
onClicked: (root.Kirigami.PageStack.pageStack as Kirigami.PageRow).pushDialogLayer(Qt.createComponent('org.kde.neochat', 'MessageSourceSheet'), {
model: stateModel,
allowEdit: true,
room: root.room,

View File

@@ -1,8 +1,9 @@
// SPDX-FileCopyrightText: 2024 Tobias Fella <tobias.fella@kde.org>
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Layouts
import QtQuick.Window
import org.kde.kirigami as Kirigami
@@ -30,14 +31,15 @@ FormCard.FormCardPage {
}
delegate: FormCard.FormButtonDelegate {
text: model.stateKey
onClicked: openEventSource(model.stateKey)
required property string stateKey
text: stateKey
onClicked: root.openEventSource(stateKey)
}
}
}
function openEventSource(stateKey: string): void {
root.Window.window.pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat', 'MessageSourceSheet'), {
(Kirigami.PageStack.pageStack as Kirigami.PageRow).pushDialogLayer(Qt.createComponent('org.kde.neochat', 'MessageSourceSheet'), {
model: stateKeysModel,
allowEdit: true,
room: root.room,

View File

@@ -45,6 +45,8 @@ target_sources(LibNeoChat PRIVATE
models/stickermodel.cpp
models/userfiltermodel.cpp
models/userlistmodel.cpp
models/widgetmodel.cpp
models/widgetmodel.h
)
if (TARGET KF6::KIOWidgets)

View File

@@ -15,6 +15,7 @@
#include "general_logging.h"
using namespace Qt::StringLiterals;
using namespace Quotient;
AccountManager::AccountManager(bool testMode, QObject *parent)
: QObject(parent)
@@ -39,7 +40,7 @@ Quotient::AccountRegistry *AccountManager::accounts()
void AccountManager::loadAccountsFromCache()
{
for (const auto &accountId : Quotient::SettingsGroup("Accounts"_L1).childGroups()) {
for (const auto &accountId : AccountSettingsGroup().childGroups()) {
Quotient::AccountSettings account{accountId};
m_accountsLoading += accountId;
Q_EMIT accountsLoadingChanged();

View File

@@ -30,7 +30,7 @@ public:
ServerNotice, /**< Official messages from the server. */
Deprioritized, /**< The room is set as low priority. */
Space, /**< The room is a space. */
AddDirect, /**< So we can show the add friend delegate. */
AddDirect, /**< So we can show the add direct message delegate. */
TypesCount, /**< Number of different types (this should always be last). */
};
Q_ENUM(Types);
@@ -66,7 +66,7 @@ public:
case NeoChatRoomType::Favorite:
return i18n("Favorite");
case NeoChatRoomType::Direct:
return i18n("Friends");
return i18n("Direct Messages");
case NeoChatRoomType::Normal:
return i18n("Normal");
case NeoChatRoomType::Deprioritized:

View File

@@ -10,7 +10,6 @@
#include <Quotient/events/encryptionevent.h>
#include <Quotient/events/event.h>
#include <Quotient/events/eventcontent.h>
#include <Quotient/events/reactionevent.h>
#include <Quotient/events/redactionevent.h>
#include <Quotient/events/roomavatarevent.h>
@@ -196,20 +195,25 @@ bool EventHandler::isHidden(const NeoChatRoom *room, const Quotient::RoomEvent *
return false;
}
Qt::TextFormat EventHandler::messageBodyInputFormat(const Quotient::RoomMessageEvent &event)
Qt::TextFormat EventHandler::messageBodyInputFormat(const Quotient::RoomEvent &event)
{
if (event.isRedacted() && !event.isStateEvent()) {
return Qt::RichText;
}
if (event.mimeType().name() == "text/plain"_L1) {
auto msgEvent = eventCast<const Quotient::RoomMessageEvent>(&event);
if (!msgEvent) {
return Qt::PlainText;
}
if (msgEvent->mimeType().name() == "text/plain"_L1) {
return Qt::PlainText;
} else {
return Qt::RichText;
}
}
QString EventHandler::rawMessageBody(const Quotient::RoomMessageEvent &event)
QString EventHandler::rawMessageBody(const RoomEvent &event)
{
if (event.isRedacted() && !event.isStateEvent()) {
auto reason = event.redactedBecause()->reason();
@@ -218,21 +222,26 @@ QString EventHandler::rawMessageBody(const Quotient::RoomMessageEvent &event)
QString body;
if (event.has<EventContent::FileContent>()) {
auto msgEvent = eventCast<const Quotient::RoomMessageEvent>(&event);
if (!msgEvent) {
return body;
}
if (msgEvent->has<EventContent::FileContent>()) {
// if filename is given or body is equal to filename,
// then body is a caption
QString filename = event.get<EventContent::FileContent>()->originalName;
QString body = event.plainBody();
QString filename = msgEvent->get<EventContent::FileContent>()->originalName;
QString body = msgEvent->plainBody();
if (filename.isEmpty() || filename == body) {
return QString();
}
return body;
}
if (event.has<EventContent::TextContent>() && event.content()) {
body = event.get<EventContent::TextContent>()->body;
if (msgEvent->has<EventContent::TextContent>() && msgEvent->content()) {
body = msgEvent->get<EventContent::TextContent>()->body;
} else {
body = event.plainBody();
body = msgEvent->plainBody();
}
return body;
}
@@ -451,6 +460,9 @@ QString EventHandler::getBody(const NeoChatRoom *room, const Quotient::RoomEvent
return i18nc("[User] joined a [voice/video] call", "joined a call");
}
}
if (e.matrixType() == "io.element.integrations.installations"_L1) {
return i18nc("[User] configured an extension", "configured an extension");
}
return e.stateKey().isEmpty() ? i18n("updated %1 state", e.matrixType())
: i18n("updated %1 state for %2", e.matrixType(), prettyPrint ? e.stateKey().toHtmlEscaped() : e.stateKey());
},
@@ -664,6 +676,9 @@ QString EventHandler::genericBody(const NeoChatRoom *room, const Quotient::RoomE
return i18nc("[User] joined a [voice/video] call", "%1 joined a call", senderString);
}
}
if (e.matrixType() == "io.element.integrations.installations"_L1) {
return i18nc("[User] configured an extension", "%1 configured an extension", senderString);
}
return i18n("%1 updated the state", senderString);
},
[senderString](const PollStartEvent &) {

View File

@@ -122,15 +122,15 @@ public:
* I.e. if the message has only a body the format will be Qt::PlainText, if it
* has a formatted body it will be Qt::RichText.
*/
static Qt::TextFormat messageBodyInputFormat(const Quotient::RoomMessageEvent &event);
static Qt::TextFormat messageBodyInputFormat(const Quotient::RoomEvent &event);
/**
* @brief Output a string for the room message content without any formatting.
* @brief Output a string for the 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);
static QString rawMessageBody(const Quotient::RoomEvent &event);
/**
* @brief Output a string for the message content ready for display in a rich text field.

View File

@@ -2,7 +2,6 @@
// SPDX-License-Identifier: LGPL-2.0-or-later
#include "pollevent.h"
#include <Quotient/converters.h>
using namespace Quotient;

View File

@@ -146,9 +146,13 @@ bool UserListModel::event(QEvent *event)
void UserListModel::memberJoined(const Quotient::RoomMember &member)
{
auto pos = findUserPos(member);
beginInsertRows(QModelIndex(), pos, pos);
m_members.insert(pos, member.id());
if (m_members.contains(member.id())) {
return;
}
const int row = m_members.size();
beginInsertRows(QModelIndex(), row, row);
m_members.append(member.id());
endInsertRows();
}
@@ -168,8 +172,6 @@ void UserListModel::refreshMember(const Quotient::RoomMember &member, const QLis
{
auto pos = findUserPos(member);
if (pos != m_members.size()) {
// The update will have changed the state event so we need to insert the updated member object.
m_members.insert(pos, member.id());
Q_EMIT dataChanged(index(pos), index(pos), roles);
} else {
qWarning() << "Trying to access a room member not in the user list";

View File

@@ -0,0 +1,356 @@
// SPDX-FileCopyrightText: 2025 Arno Rehn <arno@arnorehn.de>
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
#include "widgetmodel.h"
#include <Quotient/jobs/downloadfilejob.h>
#include <Quotient/user.h>
#include <widgetevent.h>
using namespace Quotient;
namespace
{
// This URL will take us to a jitsi room that is configured on a best effort basis.
// Critically, it appears that we cannot pass avatars via the "fragment API".
// The important stuff is working though: conference id, user name, subject.
//
// The alternative would be to rely on a Jitsi wrapper like https://app.element.io/jitsi.html.
// This one parses the fragment and then uses the the actual Jitsi IFrame API to set
// up the Jitsi meeting. Nice, but ties us to that external wrapper.
//
// Or we ship our own wrapper as a local HTML file and open that one?
constexpr char JitsiMeetUrlTemplate[] =
"https://$domain/$conferenceId"
"#jitsi_meet_external_api_id=0"
"&config.subject=\"$roomName\""
"&config.startAudioOnly=$isAudioOnly"
"&config.startWithAudioMuted=$startWithAudioMuted"
"&config.startWithVideoMuted=$startWithVideoMuted"
"&isVideoChannel=$isVideoChannel"
"&userInfo.displayName=\"$matrix_display_name\"";
// Same thing with etherpad, as long as it's on scalar.vector.im
constexpr char EtherpadScalarVectorImUrl[] = "https://scalar.vector.im/api/widgets/etherpad.html";
constexpr char EtherpadUrlTemplate[] =
"https://etherpad.integrations.element.io/p/"
"$padName"
"?showControls=true"
"&showChat=false"
"&chatAndUsers=false"
"&alwaysShowChat=false"
"&showLineNumbers=true"
"&useMonospaceFont=false"
"&userName=$matrix_user_id";
QUrl avatarUrl(NeoChatRoom *room)
{
const auto avatarUrl = room->member(room->connection()->userId()).avatarUrl();
return room->connection()->getUrlForApi<DownloadFileJob>(avatarUrl);
}
// Many widgets require proper interfacing with integration managers, but this needs authentication
// with an integration manager: https://github.com/matrix-org/matrix-spec-proposals/blob/main/proposals/1961-integrations-auth.md
// Also requires accepting the terms: https://github.com/matrix-org/matrix-spec-proposals/blob/main/proposals/2140-terms-of-service-2.md
// Example:
// curl -X POST "https://scalar.vector.im/_matrix/integrations/v1/register" \
// -H "Content-Type: application/json" \
// --data '<OpenIdCredentials>'
// Then append the resulting json key-value pairs as query items to the widget url.
QUrl buildWidgetUrl(QByteArray templateString, NeoChatRoom *room, const QJsonObject &data)
{
constexpr auto enc = [](const auto &s) {
return QUrl::toPercentEncoding(s);
};
templateString.replace("$matrix_user_id"_L1, enc(room->connection()->userId()))
.replace("$matrix_room_id"_L1, enc(room->id()))
.replace("$matrix_display_name"_L1, enc(room->member(room->connection()->userId()).displayName()))
.replace("$matrix_avatar_url"_L1, avatarUrl(room).toEncoded());
for (auto it = data.begin(); it != data.end(); ++it) {
const QByteArray key = '$' + it.key().toUtf8();
templateString.replace(key, enc(it.value().toString()));
}
return QUrl::fromEncoded(templateString);
}
}
class WidgetModelPrivate
{
Q_DISABLE_COPY(WidgetModelPrivate);
public:
WidgetModelPrivate(WidgetModel *q)
: q_ptr(q)
{
}
Q_DECLARE_PUBLIC(WidgetModel)
WidgetModel *const q_ptr;
// clang-format off
struct NoModelResetTag_t {};
static constexpr NoModelResetTag_t NoModelReset {};
// clang-format on
void reload();
void reload(NoModelResetTag_t);
void updateWidgets(Quotient::RoomEventsRange events);
void handleEvent(const Quotient::RoomEvent *event);
void handlePendingEvent(const Quotient::RoomEvent *event);
void buildJitsiIndex();
NeoChatRoom *room = nullptr;
QMap<QString, const WidgetEvent *> state;
int jitsiIndex = -1;
};
WidgetModel::WidgetModel(QObject *parent)
: QAbstractListModel{parent}
, d_ptr(std::make_unique<WidgetModelPrivate>(this))
{
}
WidgetModel::~WidgetModel()
{
}
QHash<int, QByteArray> WidgetModel::roleNames() const
{
return {
{TextRole, "text"},
{UrlRole, "url"},
{TypeRole, "type"},
};
}
QVariant WidgetModel::data(const QModelIndex &index, int role) const
{
Q_D(const WidgetModel);
const int row = index.row();
if (row < 0 || row >= d->state.count()) {
return {};
}
const auto *ev = std::next(d->state.begin(), row).value();
const auto type = ev->contentPart<QString>("type"_L1);
const auto url = ev->contentPart<QString>("url"_L1).toUtf8();
switch (role) {
case TextRole:
return ev->contentPart<QString>("name"_L1);
case TypeRole:
return type;
case UrlRole: {
QByteArray urlTemplate = url;
if (type == "jitsi"_L1) {
// Jitsi is special-cased even in Element. The URL that we get from the
// widget is just "https://scalar.vector.im/api/widgets/jitsi.html" which
// is not sufficient.
urlTemplate = JitsiMeetUrlTemplate;
} else if (type == "m.etherpad"_L1 && url.startsWith(EtherpadScalarVectorImUrl)) {
// Etherpad is not special-cased, but the scalar.vector.im wrapper requires authentication
// with the integration manager. We can just resolve this to the actual etherpad instance
// to skip integration manager auth.
urlTemplate = EtherpadUrlTemplate;
}
return buildWidgetUrl(urlTemplate, room(), ev->contentPart<QJsonObject>("data"_L1));
}
}
return {};
}
int WidgetModel::rowCount(const QModelIndex &parent) const
{
Q_D(const WidgetModel);
Q_UNUSED(parent);
return d->state.count();
}
void WidgetModelPrivate::reload(NoModelResetTag_t)
{
state.clear();
if (room) {
const auto events = room->currentState().eventsOfType(Quotient::WidgetEvent::MetaType.matrixId);
for (auto ev : events) {
handleEvent(ev);
}
}
}
void WidgetModelPrivate::reload()
{
Q_Q(WidgetModel);
q->beginResetModel();
reload(NoModelReset);
buildJitsiIndex();
q->endResetModel();
}
void WidgetModelPrivate::updateWidgets(Quotient::RoomEventsRange events)
{
using namespace std::ranges;
constexpr auto isWidgetEvent = [](auto &&ev) {
return ev->template is<WidgetEvent>();
};
if (any_of(events, isWidgetEvent)) {
reload();
}
}
void WidgetModelPrivate::handleEvent(const Quotient::RoomEvent *event)
{
const WidgetEvent *widgetEvent = eventCast<const WidgetEvent>(event);
if (!widgetEvent) {
return;
}
auto it = state.find(widgetEvent->stateKey());
if (it != state.end() && widgetEvent->replacedState() == it.value()->id()) {
// we alrady have this and it should be...
if (widgetEvent->contentJson().isEmpty()) {
// ... removed because replacement is empty
state.erase(it);
} else {
// replaced
it.value() = widgetEvent;
}
} else if (!widgetEvent->contentJson().isEmpty()) {
// insert if not empty
it = state.insert(widgetEvent->stateKey(), widgetEvent);
}
}
void WidgetModelPrivate::handlePendingEvent(const RoomEvent *event)
{
Q_Q(WidgetModel);
if (!event->is<WidgetEvent>()) {
return;
}
q->beginResetModel();
reload(NoModelReset);
handleEvent(event);
buildJitsiIndex();
q->endResetModel();
}
void WidgetModelPrivate::buildJitsiIndex()
{
Q_Q(WidgetModel);
const auto newIndex = [&] -> int {
auto it = std::find_if(state.cbegin(), state.cend(), [](const WidgetEvent *e) {
return e->contentPart<QString>("type"_L1) == "jitsi"_L1;
});
if (it == state.cend()) {
return -1;
}
return std::distance(state.cbegin(), it);
}();
if (newIndex != jitsiIndex) {
jitsiIndex = newIndex;
Q_EMIT q->jitsiIndexChanged();
}
}
NeoChatRoom *WidgetModel::room() const
{
Q_D(const WidgetModel);
return d->room;
}
void WidgetModel::setRoom(NeoChatRoom *newRoom)
{
Q_D(WidgetModel);
if (d->room == newRoom) {
return;
}
if (d->room) {
disconnect(d->room, &NeoChatRoom::baseStateLoaded, this, nullptr);
disconnect(d->room, &NeoChatRoom::aboutToAddNewMessages, this, nullptr);
disconnect(d->room, &NeoChatRoom::pendingEventAboutToAdd, this, nullptr);
}
d->room = newRoom;
if (d->room) {
connect(d->room, &NeoChatRoom::baseStateLoaded, this, [d] {
d->reload();
});
connect(d->room, &NeoChatRoom::aboutToAddNewMessages, this, [d](Quotient::RoomEventsRange events) {
d->updateWidgets(events);
});
connect(d->room, &NeoChatRoom::pendingEventAboutToAdd, this, [d](Quotient::RoomEvent *event) {
d->handlePendingEvent(event);
});
}
d->reload();
Q_EMIT roomChanged();
}
int WidgetModel::jitsiIndex() const
{
Q_D(const WidgetModel);
return d->jitsiIndex;
}
QUrl WidgetModel::addJitsiConference()
{
// URL is not spec-compliant, but this is what Element does as well.
const auto conferenceId = QUuid::createUuid().toString(QUuid::WithoutBraces);
// clang-format off
const QJsonObject content{
{"name"_L1, "Jitsi Meet"_L1},
{"type"_L1, "jitsi"_L1},
{"url"_L1, "https://scalar.vector.im/api/widgets/jitsi.html"_L1},
{"data"_L1, QJsonObject{
{"conferenceId"_L1, conferenceId},
{"domain"_L1, "meet.element.io"_L1}, // TODO: make domain configurable
{"isAudioOnly"_L1, false},
{"roomName"_L1, room()->displayName()},
}},
};
// clang-format on
// Re-use conferenceId as state_key
room()->setState(WidgetEvent::MetaType.matrixId, conferenceId, content);
return buildWidgetUrl(JitsiMeetUrlTemplate, room(), content["data"_L1].toObject());
}
bool WidgetModel::removeWidget(int index)
{
Q_D(const WidgetModel);
if (index < 0 || index >= rowCount()) {
return false;
}
const auto stateKey = std::next(d->state.begin(), index).value()->stateKey();
room()->setState(WidgetEvent::MetaType.matrixId, stateKey, QJsonObject{});
return true;
}

View File

@@ -0,0 +1,74 @@
// SPDX-FileCopyrightText: 2025 Arno Rehn <arno@arnorehn.de>
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
#ifndef WIDGETMODEL_H
#define WIDGETMODEL_H
#include <QAbstractListModel>
#include <QQmlEngine>
#include "neochatroom.h"
class WidgetModelPrivate;
/**
* @class WidgetModel
*
* `WidgetModel` provides a list model of widgets for a given room.
*
* It also supports adding and removing widgets.
*/
class WidgetModel : public QAbstractListModel
{
Q_OBJECT
QML_ELEMENT
/**
* @brief The room for which this model lists the widgets
*/
Q_PROPERTY(NeoChatRoom *room READ room WRITE setRoom NOTIFY roomChanged)
/**
* @brief The row index of the first Jitsi widget
*/
Q_PROPERTY(int jitsiIndex READ jitsiIndex NOTIFY jitsiIndexChanged)
public:
enum Roles {
TextRole = Qt::DisplayRole,
UrlRole = Qt::UserRole,
TypeRole,
};
Q_ENUM(Roles)
explicit WidgetModel(QObject *parent = nullptr);
~WidgetModel() override;
NeoChatRoom *room() const;
void setRoom(NeoChatRoom *newRoom);
[[nodiscard]] QHash<int, QByteArray> roleNames() const override;
[[nodiscard]] QVariant data(const QModelIndex &index, int roleName) const override;
[[nodiscard]] int rowCount(const QModelIndex &parent = {}) const override;
int jitsiIndex() const;
/**
* @brief Adds a new Jitsi widget
* @return The URL of the newly added Jitsi conference
*/
Q_INVOKABLE QUrl addJitsiConference();
/**
* @brief Removes the widget at @p index
* @return `true` on success, `false` otherwise
*/
Q_INVOKABLE bool removeWidget(int index);
Q_SIGNALS:
void roomChanged();
void jitsiIndexChanged();
private:
Q_DECLARE_PRIVATE(WidgetModel)
const std::unique_ptr<WidgetModelPrivate> d_ptr;
};
#endif // WIDGETMODEL_H

View File

@@ -9,7 +9,6 @@
#include "neochatroom.h"
#include "spacehierarchycache.h"
#include <Quotient/connection.h>
#include <Quotient/jobs/basejob.h>
#include <Quotient/quotient_common.h>
#include <qt6keychain/keychain.h>
@@ -191,7 +190,7 @@ void NeoChatConnection::setKeywordPushRuleDefault(PushRuleAction::Action default
void NeoChatConnection::logout(bool serverSideLogout)
{
SettingsGroup(u"Accounts"_s).remove(userId());
AccountSettingsGroup().remove(userId());
QKeychain::DeletePasswordJob job(qAppName());
job.setAutoDelete(true);

View File

@@ -45,7 +45,6 @@
#include "chatbarcache.h"
#include "clipboard.h"
#include "eventhandler.h"
#include "events/pollevent.h"
#include "filetransferpseudojob.h"
#include "neochatconnection.h"
#include "roomlastmessageprovider.h"
@@ -1181,6 +1180,9 @@ void NeoChatRoom::loadPinnedMessage()
const QString &mostRecentEventId = events.last();
connection()->callApi<GetOneRoomEventJob>(id(), mostRecentEventId).then([this](const auto &job) {
auto event = fromJson<event_ptr_tt<RoomEvent>>(job->jsonData());
if (auto encEv = eventCast<EncryptedEvent>(event.get())) {
event = decryptMessage(*encEv);
}
m_pinnedMessage = EventHandler::richBody(this, event.get());
Q_EMIT pinnedMessageChanged();
});

View File

@@ -2,10 +2,11 @@
// SPDX-FileCopyrightText: 2020 Carl Schwan <carl@carlschwan.eu>
// SPDX-License-Identifier: GPL-3.0-only
pragma ComponentBehavior: Bound
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
@@ -129,8 +130,8 @@ SearchPage {
function openManualRoomDialog() {
let dialog = manualRoomDialog.createObject(root.QQC2.Overlay.overlay, {
connection: root.connection
});
dialog.parent = root.Window.window.overlay;
}) as ManualRoomDialog;
dialog.parent = root.QQC2.Overlay.overlay;
dialog.roomSelected.connect((roomId, displayName, avatarUrl, alias, topic, memberCount, isJoined) => {
root.roomSelected(roomId, displayName, avatarUrl, alias, topic, memberCount, isJoined);
root.closeDialog();

View File

@@ -2,7 +2,6 @@
// SPDX-License-Identifier: GPL-2.0-or-later
import QtQuick
import QtQuick.Layouts
import org.kde.kirigami as Kirigami
import org.kde.kirigamiaddons.formcard as FormCard

View File

@@ -2,7 +2,6 @@
// SPDX-License-Identifier: GPL-2.0-or-later
import QtQuick
import QtQuick.Layouts
import org.kde.kirigami as Kirigami
import org.kde.kirigamiaddons.formcard as FormCard

View File

@@ -3,7 +3,6 @@
import QtQuick
import QtQuick.Controls as QQC2
import QtQuick.Layouts
import org.kde.kirigamiaddons.formcard as FormCard

View File

@@ -39,11 +39,11 @@ LoginStep {
text: LoginHelper.isLoggedIn ? i18n("Already logged in") : (LoginHelper.testing && matrixIdField.acceptableInput) ? i18n("Loading…") : i18nc("@action:button", "Continue")
onTriggered: {
if (LoginHelper.supportsSso && LoginHelper.supportsPassword) {
processed("LoginMethod");
root.processed("LoginMethod");
} else if (LoginHelper.supportsSso) {
processed("Sso");
root.processed("Sso");
} else {
processed("Password");
root.processed("Password");
}
}
enabled: LoginHelper.homeserverReachable

View File

@@ -18,12 +18,12 @@ LoginStep {
FormCard.FormButtonDelegate {
id: loginPasswordButton
text: i18nc("@action:button", "Login with password")
onClicked: processed("Password")
onClicked: root.processed("Password")
}
FormCard.FormButtonDelegate {
id: loginSsoButton
text: i18nc("@action:button", "Login with single sign-on")
onClicked: processed("Sso")
onClicked: root.processed("Sso")
}
}

View File

@@ -2,7 +2,6 @@
// SPDX-License-Identifier: GPL-2.0-or-later
import QtQuick
import QtQuick.Layouts
import org.kde.kirigami as Kirigami
import org.kde.kirigamiaddons.formcard as FormCard
@@ -15,7 +14,7 @@ LoginStep {
Connections {
target: LoginHelper
function onConnected() {
processed("Loading");
root.processed("Loading");
}
}
@@ -46,6 +45,6 @@ LoginStep {
}
}
previousAction: Kirigami.Action {
onTriggered: processed("Login")
onTriggered: root.processed("Login")
}
}

View File

@@ -2,7 +2,6 @@
// SPDX-License-Identifier: GPL-2.0-or-later
import QtQuick
import QtQuick.Layouts
import org.kde.kirigami as Kirigami
import org.kde.kirigamiaddons.formcard as FormCard
@@ -22,7 +21,7 @@ LoginStep {
UrlHelper.openUrl(LoginHelper.ssoUrl);
}
function onConnected() {
processed("Loading");
root.processed("Loading");
}
}
@@ -31,7 +30,7 @@ LoginStep {
}
previousAction: Kirigami.Action {
onTriggered: processed("Login")
onTriggered: root.processed("Login")
}
nextAction: Kirigami.Action {

View File

@@ -22,8 +22,11 @@ LoginStep {
Repeater {
model: Registration.terms
delegate: FormCard.FormTextDelegate {
text: "<a href=\"" + modelData.url + "\">" + modelData.title + "</a>"
onLinkActivated: Qt.openUrlExternally(modelData.url)
required property string url
required property string title
text: "<a href=\"" + url + "\">" + title + "</a>"
onLinkActivated: Qt.openUrlExternally(url)
}
}

View File

@@ -1,6 +1,8 @@
// SPDX-FileCopyrightText: 2020 Tobias Fella <tobias.fella@kde.org>
// SPDX-License-Identifier: GPL-2.0-or-later
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Controls as QQC2
import QtQuick.Layouts
@@ -121,6 +123,8 @@ Kirigami.Page {
delegate: FormCard.AbstractFormDelegate {
id: loadingDelegate
required property string modelData
topPadding: Kirigami.Units.smallSpacing
bottomPadding: Kirigami.Units.smallSpacing
@@ -130,7 +134,7 @@ Kirigami.Page {
QQC2.Label {
Layout.fillWidth: true
text: i18nc("As in 'this account is still loading'", "%1 (loading)", modelData)
text: i18nc("As in 'this account is still loading'", "%1 (loading)", loadingDelegate.modelData)
elide: Text.ElideRight
wrapMode: Text.Wrap
maximumLineCount: 2
@@ -141,7 +145,7 @@ Kirigami.Page {
QQC2.ToolButton {
text: i18nc("@action:button", "Log out of this account")
icon.name: "im-kick-user"
onClicked: Controller.removeConnection(modelData)
onClicked: Controller.removeConnection(loadingDelegate.modelData)
display: QQC2.Button.IconOnly
QQC2.ToolTip.text: text
QQC2.ToolTip.visible: hovered
@@ -159,7 +163,7 @@ Kirigami.Page {
}
}
onCountChanged: {
if (loadingAccounts.count === 0 && loadedAccounts.count === 1 && showExisting) {
if (loadingAccounts.count === 0 && loadedAccounts.count === 1 && root.showExisting) {
Controller.activeConnection = AccountRegistry.data(AccountRegistry.index(0, 0), 257);
root.connectionChosen();
}
@@ -181,15 +185,15 @@ Kirigami.Page {
Connections {
id: stepConnections
target: currentStep
target: root.currentStep
function onProcessed(nextStep: string): void {
module.source = nextStep + ".qml";
root.currentStepString = nextStep;
headerMessage.text = "";
headerMessage.visible = false;
if (!module.item.noControls) {
module.item.forceActiveFocus();
if (!(root.currentStep as LoginStep).noControls) {
(root.currentStep as LoginStep).forceActiveFocus();
} else {
continueButton.forceActiveFocus();
}
@@ -242,24 +246,24 @@ Kirigami.Page {
FormCard.FormDelegateSeparator {
below: continueButton
visible: root.currentStep.nextAction
visible: (root.currentStep as LoginStep).nextAction
}
FormCard.FormButtonDelegate {
id: continueButton
text: root.currentStep.nextAction && root.currentStep.nextAction.text ? root.currentStep.nextAction.text : i18nc("@action:button", "Continue")
visible: root.currentStep.nextAction
onClicked: root.currentStep.nextAction.trigger()
text: (root.currentStep as LoginStep).nextAction && (root.currentStep as LoginStep).nextAction.text ? (root.currentStep as LoginStep).nextAction.text : i18nc("@action:button", "Continue")
visible: (root.currentStep as LoginStep).nextAction
onClicked: (root.currentStep as LoginStep).nextAction.trigger()
icon.name: "arrow-right-symbolic"
enabled: root.currentStep.nextAction ? root.currentStep.nextAction.enabled : false
enabled: (root.currentStep as LoginStep).nextAction ? (root.currentStep as LoginStep).nextAction.enabled : false
}
FormCard.FormButtonDelegate {
text: i18nc("@action:button", "Go back")
visible: root.currentStep.previousAction
onClicked: root.currentStep.previousAction.trigger()
visible: (root.currentStep as LoginStep).previousAction
onClicked: (root.currentStep as LoginStep).previousAction.trigger()
icon.name: "arrow-left-symbolic"
enabled: root.currentStep.previousAction ? root.currentStep.previousAction.enabled : false
enabled: (root.currentStep as LoginStep).previousAction ? (root.currentStep as LoginStep).previousAction.enabled : false
}
}
@@ -278,7 +282,7 @@ Kirigami.Page {
Component.onCompleted: {
LoginHelper.init();
module.item.forceActiveFocus();
(root.currentStep as LoginStep).forceActiveFocus();
Registration.username = "";
Registration.password = "";
Registration.email = "";

View File

@@ -92,23 +92,31 @@ ColumnLayout {
Layout.fillWidth: true
QQC2.CheckBox {
Layout.alignment: Qt.AlignTop
enabled: !root.pollHandler.hasEnded
checked: answerDelegate.localChoice
onClicked: answerDelegate.clicked()
}
QQC2.Label {
Layout.alignment: Qt.AlignTop
Layout.fillWidth: true
text: answerDelegate.answerText
wrapMode: QQC2.Label.WordWrap
}
Kirigami.Icon {
Layout.alignment: Qt.AlignTop
implicitWidth: Kirigami.Units.iconSizes.small
implicitHeight: implicitWidth
visible: answerDelegate.isWinner
source: "favorite-favorited"
}
QQC2.Label {
Layout.alignment: Qt.AlignTop
visible: root.pollHandler.kind == PollKind.Disclosed || root.pollHandler.hasEnded
horizontalAlignment: Text.AlignRight
text: i18ncp("@info", "%1 Vote", "%1 Votes", answerDelegate.count)

View File

@@ -2,14 +2,11 @@
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
import QtQuick
import QtQuick.Controls as QQC2
import QtQuick.Layouts
import org.kde.kirigami as Kirigami
import org.kde.kirigamiaddons.delegates as Delegates
import org.kde.neochat
import org.kde.neochat.chatbar
/**
* @brief A component to show a reply button for threads in a message bubble.

View File

@@ -366,55 +366,42 @@ void EventMessageContentModel::updateReplyModel()
QList<MessageComponent> EventMessageContentModel::componentsForType(MessageComponentType::Type type)
{
const auto event = m_room->getEvent(m_eventId);
if (event.first == nullptr) {
const auto [event, _] = m_room->getEvent(m_eventId);
if (event == nullptr) {
return {};
}
const auto roomMessageEvent = eventCast<const Quotient::RoomMessageEvent>(event);
switch (type) {
case MessageComponentType::Verification: {
return {MessageComponent{MessageComponentType::Verification, QString(), {}}};
}
case MessageComponentType::Text: {
if (const auto roomMessageEvent = eventCast<const Quotient::RoomMessageEvent>(event.first)) {
return TextHandler().textComponents(EventHandler::rawMessageBody(*roomMessageEvent),
EventHandler::messageBodyInputFormat(*roomMessageEvent),
m_room,
roomMessageEvent,
roomMessageEvent->isReplaced());
} else {
return TextHandler().textComponents(EventHandler::plainBody(m_room, event.first), Qt::TextFormat::PlainText, m_room, event.first, false);
}
const auto roomMessageEvent = eventCast<const Quotient::RoomMessageEvent>(event.first);
return TextHandler().textComponents(EventHandler::rawMessageBody(*roomMessageEvent),
EventHandler::messageBodyInputFormat(*roomMessageEvent),
return TextHandler().textComponents(EventHandler::rawMessageBody(*event),
EventHandler::messageBodyInputFormat(*event),
m_room,
roomMessageEvent,
roomMessageEvent->isReplaced());
event,
roomMessageEvent ? roomMessageEvent->isReplaced() : false);
}
case MessageComponentType::File: {
QList<MessageComponent> components;
components += MessageComponent{MessageComponentType::File, {}, EventHandler::mediaInfo(m_room, event.first)};
const auto roomMessageEvent = eventCast<const Quotient::RoomMessageEvent>(event.first);
auto body = EventHandler::rawMessageBody(*roomMessageEvent);
components += MessageComponent{MessageComponentType::File, {}, EventHandler::mediaInfo(m_room, event)};
auto body = EventHandler::rawMessageBody(*event);
if (!body.isEmpty()) {
components += TextHandler().textComponents(body,
EventHandler::messageBodyInputFormat(*roomMessageEvent),
m_room,
roomMessageEvent,
roomMessageEvent->isReplaced());
EventHandler::messageBodyInputFormat(*event),
m_room,
event,
roomMessageEvent ? roomMessageEvent->isReplaced() : false);
}
return components;
}
case MessageComponentType::Image:
case MessageComponentType::Audio:
case MessageComponentType::Video: {
QList<MessageComponent> components = {
MessageComponent{type, EventHandler::richBody(m_room, event.first), EventHandler::mediaInfo(m_room, event.first)}};
QList<MessageComponent> components = {MessageComponent{type, EventHandler::richBody(m_room, event), EventHandler::mediaInfo(m_room, event)}};
if (!event.first->is<StickerEvent>()) {
const auto roomMessageEvent = eventCast<const Quotient::RoomMessageEvent>(event.first);
if (!event->is<StickerEvent>() && roomMessageEvent) {
const auto fileContent = roomMessageEvent->get<EventContent::FileContentBase>();
if (fileContent != nullptr) {
const auto fileInfo = fileContent->commonInfo();
@@ -433,11 +420,11 @@ QList<MessageComponent> EventMessageContentModel::componentsForType(MessageCompo
}
case MessageComponentType::Location:
return {MessageComponent{type,
EventHandler::plainBody(m_room, event.first),
EventHandler::plainBody(m_room, event),
{
{u"latitude"_s, EventHandler::latitude(event.first)},
{u"longitude"_s, EventHandler::longitude(event.first)},
{u"asset"_s, EventHandler::locationAssetType(event.first)},
{u"latitude"_s, EventHandler::latitude(event)},
{u"longitude"_s, EventHandler::longitude(event)},
{u"asset"_s, EventHandler::locationAssetType(event)},
}}};
default:
return {MessageComponent{type, QString(), {}}};

View File

@@ -9,7 +9,6 @@
#include "chatbarcache.h"
#include "contentprovider.h"
#include "enums/messagecomponenttype.h"
#include "neochatconnection.h"
#include "texthandler.h"
@@ -119,6 +118,11 @@ QVariant MessageContentModel::data(const QModelIndex &index, int role) const
return {};
}
if (!m_room) {
qWarning() << "MessageContentModel::data called without room";
return {};
}
const auto component = m_components[index.row()];
if (role == DisplayRole) {

View File

@@ -12,7 +12,6 @@
#include "contentprovider.h"
#include "enums/messagecomponenttype.h"
#include "eventhandler.h"
#include "messagecontentmodel.h"
#include "neochatroom.h"
ThreadModel::ThreadModel(const QString &threadRootId, NeoChatRoom *room)

View File

@@ -5,7 +5,6 @@
#include <KLocalization>
#include "events/pollevent.h"
#include "neochatroom.h"
#include <Quotient/csapi/relations.h>
@@ -42,22 +41,33 @@ void PollHandler::updatePoll(Quotient::RoomEventsRange events)
}
}
void PollHandler::checkLoadRelations()
void PollHandler::checkLoadRelations(const QString &nextBatch)
{
const auto pollStartEvent = m_room->getEvent(m_pollStartId).first;
if (pollStartEvent == nullptr) {
return;
}
m_room->connection()->callApi<GetRelatingEventsJob>(m_room->id(), pollStartEvent->id()).onResult([this](const auto &job) {
m_room->connection()->callApi<GetRelatingEventsJob>(m_room->id(), pollStartEvent->id(), nextBatch).onResult([this](const auto &job) {
for (const auto &event : job->chunk()) {
handleEvent(event.get());
}
// This is paginated API. If it indicates that there's more data, run again starting from the supplied pagination token.
if (!job->nextBatch().isEmpty()) {
checkLoadRelations(job->nextBatch());
}
});
}
void PollHandler::handleEvent(Quotient::RoomEvent *event)
{
if (auto encEvent = eventCast<const EncryptedEvent>(event)) {
const auto decrypted = room()->decryptMessage(*encEvent);
handleEvent(decrypted.get());
return;
}
auto pollStartEvent = eventCast<const PollStartEvent>(m_room->getEvent(m_pollStartId).first);
if (pollStartEvent == nullptr) {
return;

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