Compare commits

...

47 Commits

Author SHA1 Message Date
Laurent Montel
a244293f9f Fix port deprecated [=] operator in c++20 2023-08-30 10:52:20 +02:00
Tobias Fella
50ad18d095 Merge branch 'master' into kf6 2023-08-29 22:57:12 +02:00
Tobias Fella
9fcfad7058 Fix supportStreaming in VideoDelegate 2023-08-29 20:48:02 +00:00
Tobias Fella
cfcc1756dd Implement deactivating accounts 2023-08-29 22:38:32 +02:00
Tobias Fella
e15bec2295 Introduce NeoChatConnection
Previously, some functions that conceptually belong to the connection needed to be in the Controller, since we didn't have a place to put them.
This fixes that by extending the Connection class in a similar way as we extend the Room class.
2023-08-29 20:14:44 +00:00
Tobias Fella
ca53d163eb Merge branch 'master' into kf6 2023-08-29 19:56:46 +02:00
James Graham
9071cf827f Make sure that the event after a state event is also a state event before filtering it out. 2023-08-29 17:41:20 +01:00
Yuri Chornoivan
c7deaaba84 Fix minor typo 2023-08-29 09:52:45 +03:00
l10n daemon script
414035de8b GIT_SILENT Sync po/docbooks with svn 2023-08-29 01:44:33 +00:00
James Graham
3bac752463 Merge branch 'master' into kf6 2023-08-28 14:04:17 +01:00
James Graham
442612d31d Merge the functionality of CollapseStateProxyModel into MessageFilterModel
Merge the functionality of CollapseStateProxyModel into MessageFilterModel there is no need for a whole separate model the filters can be combined trivialy.
2023-08-28 10:31:18 +00:00
Tobias Fella
2ba3d970e1 Merge branch 'master' into kf6 2023-08-28 12:05:45 +02:00
Tobias Fella
59164d3bb2 Implement account registration
Implements #7
2023-08-28 10:05:09 +00:00
Tobias Fella
cc60dde62d Deselect space when leaving it
BUG: 473271
2023-08-28 11:47:39 +02:00
Tobias Fella
a19502ca40 Fix KF6 crash in emoji completion 2023-08-28 09:46:47 +00:00
l10n daemon script
cbed8148a3 GIT_SILENT Sync po/docbooks with svn 2023-08-28 02:09:20 +00:00
Tobias Fella
1b2919587a Fix opening CreateSpaceDialog in Qt6 2023-08-28 00:24:42 +02:00
Tobias Fella
e5420973f5 Merge branch 'master' into kf6 2023-08-27 21:49:53 +02:00
James Graham
50e8b9ebf6 Visit User
Move to using visit user in all cases.
2023-08-27 19:05:55 +00:00
Nicolas Fella
f5ad2ad162 Fix enter key naviation in login flow 2023-08-27 19:32:41 +02:00
Tobias Fella
931d20fecd Merge branch 'master' into kf6 2023-08-27 19:18:07 +02:00
Tobias Fella
d11d6c74b3 Fix opening settings
BUG: 473789
2023-08-27 17:17:12 +00:00
James Graham
725c6c30ce Get KF6 Working
Minimal set of changes so that a compiled version of NeoChat on KF6 will run and at least be functional.
2023-08-27 16:17:56 +00:00
James Graham
63ed69a5d4 Use updated AlbumMaximizeDelegate
Autoload is being removed so don't use it.

Requires libraries/kirigami-addons!146
2023-08-27 16:02:07 +00:00
Tobias Fella
a8aa775575 Implement message forwarding
Implements #338
2023-08-27 09:55:31 +00:00
Tobias Fella
6305359b3c Use round instead of ceil in delegatesizehelper
Fixes #592
2023-08-27 09:54:57 +00:00
Tobias Fella
7e859364af Don't show "no canonical alias" message in GroupChatDrawerHeader
The word "canonical" is confusing to average people and there isn't really a point in showing the message.
2023-08-27 09:54:41 +00:00
Tobias Fella
8abd0db012 Remove redundant NeoChatRoom::htmlSafeDisplayName 2023-08-27 09:54:28 +00:00
Tobias Fella
dbc10685f0 Port login to FormCard 2023-08-27 09:54:12 +00:00
l10n daemon script
96582a12bc GIT_SILENT Sync po/docbooks with svn 2023-08-27 01:50:22 +00:00
l10n daemon script
0ac61854bd GIT_SILENT made messages (after extraction) 2023-08-27 00:46:25 +00:00
Tobias Fella
68298f038d Fix opening report sheet 2023-08-26 19:54:45 +00:00
Tobias Fella
405fd5841a Fix opening ban sheet 2023-08-26 19:54:17 +00:00
Tobias Fella
5da9bba844 Force plain text user display name in user detail sheet 2023-08-26 19:54:06 +00:00
Tobias Fella
84373712ef Fix opening about page 2023-08-26 21:51:40 +02:00
James Graham
50f4f96341 Fix maximize component menu
Should be plainText not plainMessage
2023-08-26 19:29:11 +00:00
Tobias Fella
1b27b1a4e2 Make sure we don't load link previews when they're not enabled
Fixes #591
2023-08-26 19:28:29 +00:00
Tobias Fella
33811a4c49 Remove unused kitemmodels import 2023-08-26 21:25:09 +02:00
James Graham
43715486e5 Room Drawer Mobile
Push the room drawer as a page rather than a drawer when on mobile or the window is thin.

This also lays some foundations for having multiple different pages in the drawer by separating the room information into it's own component which could be switched out to show other things like highlights or media.
2023-08-26 08:42:39 +00:00
l10n daemon script
5c72bd4ab7 GIT_SILENT Sync po/docbooks with svn 2023-08-26 01:47:31 +00:00
James Graham
092f1be99b Further mark messages read fixes
Add a timer to lockout the scroll trigger for marking messages as read in the first second after changing room. This is because the scrollview can't differentiate between mouse scrolling and the multiple changes in scrollview that happen as the room is changed over.
2023-08-25 22:27:20 +00:00
Carl Schwan
550d55cb1a Add release note for last two version 2023-08-25 14:25:34 +02:00
l10n daemon script
e9edb61245 GIT_SILENT Sync po/docbooks with svn 2023-08-25 01:47:19 +00:00
Tobias Fella
89aae665b1 Fix reaction string 2023-08-24 17:38:17 +00:00
Carl Schwan
1e3c3dd1f4 Remove debuging value
Otherwise like of connectivity never disapear
2023-08-24 16:10:01 +02:00
Ingo Klöcker
6f4d2c0216 Add Windows-specific screenshots as screenshots with custom attribute
Instead of using translated custom values (which are not accepted by
AppStream validation) this adds the Windows-specific screenshots as
normal screenshots with custom attribute x-kde-os="windows". AppStream
validation accepts custom attributes (in fact, it ignores any attributes
other than "type").

Upstream appstream tooling will treat those screenshots as any other
extra screenshots (screenshots without type="default").
2023-08-23 15:31:47 +02:00
l10n daemon script
803cd2b4e4 GIT_SILENT Sync po/docbooks with svn 2023-08-23 01:47:24 +00:00
109 changed files with 18441 additions and 11908 deletions

View File

@@ -77,7 +77,7 @@ ecm_setup_version(${PROJECT_VERSION}
VERSION_HEADER ${CMAKE_CURRENT_BINARY_DIR}/neochat-version.h
)
find_package(Qt${QT_MAJOR_VERSION} ${QT_MIN_VERSION} NO_MODULE COMPONENTS Core Quick Gui QuickControls2 Multimedia Svg)
find_package(Qt${QT_MAJOR_VERSION} ${QT_MIN_VERSION} NO_MODULE COMPONENTS Core Quick Gui QuickControls2 Multimedia Svg WebView)
set_package_properties(Qt${QT_MAJOR_VERSION} PROPERTIES
TYPE REQUIRED
PURPOSE "Basic application components"

View File

@@ -9,7 +9,7 @@
android:versionName="${versionName}"
android:versionCode="${versionCode}"
android:installLocation="auto">
<application android:name="org.qtproject.qt5.android.bindings.QtApplication" android:label="NeoChat" android:icon="@drawable/neochat">
<application android:name="org.qtproject.qt5.android.bindings.QtApplication" android:label="NeoChat" android:icon="@drawable/neochat" android:usesCleartextTraffic="true">
<activity android:configChanges="orientation|uiMode|screenLayout|screenSize|smallestScreenSize|layoutDirection|locale|fontScale|keyboard|keyboardHidden|navigation"
android:name="org.qtproject.qt5.android.bindings.QtActivity"
android:label="NeoChat"

View File

@@ -57,6 +57,7 @@
<summary xml:lang="gl">Charle coas súas amizades en Matrix.</summary>
<summary xml:lang="it">Conversa con i tuoi contatti su matrix</summary>
<summary xml:lang="ka">ესაუბრეთ მეგობრებს Matrix-ზე</summary>
<summary xml:lang="ko">Matrix를 사용하여 친구들과 대화하기</summary>
<summary xml:lang="nl">Met uw vrienden chatten op matrix</summary>
<summary xml:lang="nn">Prat med vennar på Matrix</summary>
<summary xml:lang="sl">Klepet z vašimi prijatelji na matrixu</summary>
@@ -267,52 +268,6 @@ to provide a convergent experience across multiple platforms.</p>
<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::windows_store::screenshots::1::image">https://cdn.kde.org/screenshots/neochat/NeoChat-Windows-Timeline.png</value>
<value key="KDE::windows_store::screenshots::1::caption">Main view with room list, chat, and room information</value>
<value key="KDE::windows_store::screenshots::1::caption" xml:lang="ar">العرض الرئيسة مع قائمة الغرف والدردشات و معلومات الغرفة</value>
<value key="KDE::windows_store::screenshots::1::caption" xml:lang="ca">Vista principal amb la llista de sales, xats i informació de les sales</value>
<value key="KDE::windows_store::screenshots::1::caption" xml:lang="ca-valencia">Vista principal amb la llista de sales, xats i informació de les sales</value>
<value key="KDE::windows_store::screenshots::1::caption" xml:lang="eo">Ĉefa vido kun ĉambra listo, babilejo kaj ĉambra informo</value>
<value key="KDE::windows_store::screenshots::1::caption" xml:lang="es">Vista principal con la lista de salas, chat e información de la sala</value>
<value key="KDE::windows_store::screenshots::1::caption" xml:lang="eu">Ikuspegi nagusia gela-zerrenda, berriketa, eta gelako informazioarekin</value>
<value key="KDE::windows_store::screenshots::1::caption" xml:lang="fi">Päänäkymä, jossa huoneluettelo, keskustelu ja huoneen tiedot</value>
<value key="KDE::windows_store::screenshots::1::caption" xml:lang="fr">Vue principale avec la liste des salons ainsi que des informations sur les salons et forums de discussions</value>
<value key="KDE::windows_store::screenshots::1::caption" xml:lang="gl">Vista principal coa lista de salas, a charla, e información da sala.</value>
<value key="KDE::windows_store::screenshots::1::caption" xml:lang="it">Vista principale con elenco delle stanze, chat e informazioni sulla stanza</value>
<value key="KDE::windows_store::screenshots::1::caption" xml:lang="ka">მთავარი ხედი სურათების სიით, ჩატით და ოთახის ინფორმაციით</value>
<value key="KDE::windows_store::screenshots::1::caption" xml:lang="ko">대화방 목록, 채팅, 대화방 정보가 표시된 주 보기</value>
<value key="KDE::windows_store::screenshots::1::caption" xml:lang="nl">Hoofdweergave met lijst met rooms, chat en roominformatie</value>
<value key="KDE::windows_store::screenshots::1::caption" xml:lang="nn">Hovudvising med romliste, pratevindauge og rominformasjon</value>
<value key="KDE::windows_store::screenshots::1::caption" xml:lang="pt">A área principal com a lista de salas e com informações sobre a conversa e a sala</value>
<value key="KDE::windows_store::screenshots::1::caption" xml:lang="sl">Glavni pogled s seznamom sob, klepetom in informacijami o sobah</value>
<value key="KDE::windows_store::screenshots::1::caption" xml:lang="sv">Huvudvy med rumslista, chatt, och rumsinformation</value>
<value key="KDE::windows_store::screenshots::1::caption" xml:lang="ta">அரங்குப்பட்டியல், உரையாடல், மற்றும் அரங்குவிவரங்களைக் கொண்டுள்ள பிரதான காட்சி</value>
<value key="KDE::windows_store::screenshots::1::caption" xml:lang="tr">Oda listesini, sohbet penceresini ve oda bilgisini gösteren ana görünüm</value>
<value key="KDE::windows_store::screenshots::1::caption" xml:lang="uk">Головна панель із списком кімнат, спілкуванням та даними щодо кімнати</value>
<value key="KDE::windows_store::screenshots::1::caption" xml:lang="x-test">xxMain view with room list, chat, and room informationxx</value>
<value key="KDE::windows_store::screenshots::2::image">https://cdn.kde.org/screenshots/neochat/NeoChat-Windows-Login.png</value>
<value key="KDE::windows_store::screenshots::2::caption">Login screen</value>
<value key="KDE::windows_store::screenshots::2::caption" xml:lang="ar">شاشة الدخول</value>
<value key="KDE::windows_store::screenshots::2::caption" xml:lang="ca">Pantalla d'inici de sessió</value>
<value key="KDE::windows_store::screenshots::2::caption" xml:lang="ca-valencia">Pantalla d'inici de sessió</value>
<value key="KDE::windows_store::screenshots::2::caption" xml:lang="eo">Ensaluta ekrano</value>
<value key="KDE::windows_store::screenshots::2::caption" xml:lang="es">Pantalla de inicio de sesión</value>
<value key="KDE::windows_store::screenshots::2::caption" xml:lang="eu">Saio-hasteko pantaila</value>
<value key="KDE::windows_store::screenshots::2::caption" xml:lang="fi">Kirjautumisnäkymä</value>
<value key="KDE::windows_store::screenshots::2::caption" xml:lang="fr">Écran de connexion</value>
<value key="KDE::windows_store::screenshots::2::caption" xml:lang="gl">Pantalla de identificación.</value>
<value key="KDE::windows_store::screenshots::2::caption" xml:lang="it">Schermata di accesso</value>
<value key="KDE::windows_store::screenshots::2::caption" xml:lang="ka">შესვლის ეკრანი</value>
<value key="KDE::windows_store::screenshots::2::caption" xml:lang="ko">로그인 화면</value>
<value key="KDE::windows_store::screenshots::2::caption" xml:lang="nl">Aanmeldscherm</value>
<value key="KDE::windows_store::screenshots::2::caption" xml:lang="nn">Innloggingsbilete</value>
<value key="KDE::windows_store::screenshots::2::caption" xml:lang="pt">Ecrã de autenticação</value>
<value key="KDE::windows_store::screenshots::2::caption" xml:lang="sl">Prijavni zaslon</value>
<value key="KDE::windows_store::screenshots::2::caption" xml:lang="sv">Inloggningsfönster</value>
<value key="KDE::windows_store::screenshots::2::caption" xml:lang="ta">நுழைவுத் திரை</value>
<value key="KDE::windows_store::screenshots::2::caption" xml:lang="tr">Oturum açma ekranı</value>
<value key="KDE::windows_store::screenshots::2::caption" xml:lang="uk">Вікно входу</value>
<value key="KDE::windows_store::screenshots::2::caption" xml:lang="x-test">xxLogin screenxx</value>
</custom>
<launchable type="desktop-id">org.kde.neochat.desktop</launchable>
<screenshots>
@@ -322,16 +277,77 @@ to provide a convergent experience across multiple platforms.</p>
<screenshot type="default">
<image>https://cdn.kde.org/screenshots/neochat/application.png</image>
</screenshot>
<screenshot x-kde-os="windows">
<image>https://cdn.kde.org/screenshots/neochat/NeoChat-Windows-Timeline.png</image>
<caption>Main view with room list, chat, and room information</caption>
<caption xml:lang="ar">العرض الرئيسة مع قائمة الغرف والدردشات و معلومات الغرفة</caption>
<caption xml:lang="ca">Vista principal amb la llista de sales, xats i informació de les sales</caption>
<caption xml:lang="ca-valencia">Vista principal amb la llista de sales, xats i informació de les sales</caption>
<caption xml:lang="eo">Ĉefa vido kun ĉambra listo, babilejo kaj ĉambra informo</caption>
<caption xml:lang="es">Vista principal con la lista de salas, chat e información de la sala</caption>
<caption xml:lang="eu">Ikuspegi nagusia gela-zerrenda, berriketa, eta gelako informazioarekin</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="gl">Vista principal coa lista de salas, a charla, e información da sala.</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="ko">대화방 목록, 채팅, 대화방 정보가 표시된 주 보기</caption>
<caption xml:lang="nl">Hoofdweergave met lijst met rooms, chat en roominformatie</caption>
<caption xml:lang="nn">Hovudvising med romliste, pratevindauge og rominformasjon</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="sl">Glavni pogled s seznamom sob, klepetom in informacijami o sobah</caption>
<caption xml:lang="sv">Huvudvy med rumslista, chatt, och rumsinformation</caption>
<caption xml:lang="ta">அரங்குப்பட்டியல், உரையாடல், மற்றும் அரங்குவிவரங்களைக் கொண்டுள்ள பிரதான காட்சி</caption>
<caption xml:lang="tr">Oda listesini, sohbet penceresini ve oda bilgisini gösteren ana görünüm</caption>
<caption xml:lang="uk">Головна панель із списком кімнат, спілкуванням та даними щодо кімнати</caption>
<caption xml:lang="x-test">xxMain view with room list, chat, and room informationxx</caption>
</screenshot>
<screenshot x-kde-os="windows">
<image>https://cdn.kde.org/screenshots/neochat/NeoChat-Windows-Login.png</image>
<caption>Login screen</caption>
<caption xml:lang="ar">شاشة الدخول</caption>
<caption xml:lang="ca">Pantalla d'inici de sessió</caption>
<caption xml:lang="ca-valencia">Pantalla d'inici de sessió</caption>
<caption xml:lang="eo">Ensaluta ekrano</caption>
<caption xml:lang="es">Pantalla de inicio de sesión</caption>
<caption xml:lang="eu">Saio-hasteko pantaila</caption>
<caption xml:lang="fi">Kirjautumisnäkymä</caption>
<caption xml:lang="fr">Écran de connexion</caption>
<caption xml:lang="gl">Pantalla de identificación.</caption>
<caption xml:lang="it">Schermata di accesso</caption>
<caption xml:lang="ka">შესვლის ეკრანი</caption>
<caption xml:lang="ko">로그인 화면</caption>
<caption xml:lang="nl">Aanmeldscherm</caption>
<caption xml:lang="nn">Innloggingsbilete</caption>
<caption xml:lang="pt">Ecrã de autenticação</caption>
<caption xml:lang="sl">Prijavni zaslon</caption>
<caption xml:lang="sv">Inloggningsfönster</caption>
<caption xml:lang="ta">நுழைவுத் திரை</caption>
<caption xml:lang="tr">Oturum açma ekranı</caption>
<caption xml:lang="uk">Вікно входу</caption>
<caption xml:lang="x-test">xxLogin screenxx</caption>
</screenshot>
</screenshots>
<content_rating type="oars-1.1">
<content_attribute id="social-chat">intense</content_attribute>
</content_rating>
<releases>
<release version="23.08.0" date="2023-08-24"/>
<release version="23.08.0" date="2023-08-24">
<url>https://kde.org/announcements/gear/23.08.0/#neochathttpsappskdeorgneochat</url>
<description>
<p>Apart from a visual overhaul, NeoChat can now display location events and also a map with the location of all the users currently broadcasting their location using Itineray's Matrix integration. Great for locating where your friends are.</p>
</description>
</release>
<release version="23.04.3" date="2023-07-06"/>
<release version="23.04.2" date="2023-06-08"/>
<release version="23.04.1" date="2023-05-11"/>
<release version="23.04.0" date="2023-04-20">
<url>https://kde.org/announcements/gear/23.04.0/#neochathttpsappskdeorgneochat</url>
<description>
<p>NeoChat improves its design with tweaks that provide a more compact layout and a simpler menu which works better for the collapsed room list.</p>
<p>We have also improved the video controls, added a new command /knock &lt;room-id&gt; to send a knock event to a room, and you can now edit a prior message inline, within the chat pane.</p>
<p>Other usability improvements include an overhaul of the keyboard navigation and shortcuts like Ctrl+PgUp/PgDn that allow you to skip from room to room.</p>
</description>
<artifacts>
<artifact type="binary" platform="x86_64-windows-msvc">
<location>https://download.kde.org/stable/release-service/23.04.0/windows/neochat-23.04.0-512-windows-cl-msvc2019-x86_64.exe</location>

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

@@ -6,8 +6,8 @@ msgid ""
msgstr ""
"Project-Id-Version: neochat\n"
"Report-Msgid-Bugs-To: https://bugs.kde.org\n"
"POT-Creation-Date: 2023-08-21 00:46+0000\n"
"PO-Revision-Date: 2023-08-10 18:43+0200\n"
"POT-Creation-Date: 2023-08-27 00:46+0000\n"
"PO-Revision-Date: 2023-08-24 21:25+0200\n"
"Last-Translator: Karl Ove Hufthammer <karl@huftis.org>\n"
"Language-Team: Norwegian Nynorsk <l10n-no@lister.huftis.org>\n"
"Language: nn\n"
@@ -20,77 +20,77 @@ msgstr ""
"X-Accelerator-Marker: &\n"
"X-Text-Markup: kde4\n"
#: src/controller.cpp:233
#: src/controller.cpp:245
#, kde-format
msgid "Login Failed: Access Token invalid or revoked"
msgstr "Feil ved innlogging: Ugyldig eller tilbaketrekt tilgangspollett"
#: src/controller.cpp:236 src/controller.cpp:241 src/login.cpp:90
#: src/controller.cpp:248 src/controller.cpp:253 src/login.cpp:90
#, kde-format
msgid "Login Failed: %1"
msgstr "Feil ved innlogging: %1"
#: src/controller.cpp:247
#: src/controller.cpp:259
#, kde-format
msgid "Network Error: %1"
msgstr "Nettverksfeil: %1"
#: src/controller.cpp:272
#: src/controller.cpp:284
#, kde-format
msgid "Access token wasn't found"
msgstr "Fann ikkje tilgangspollett"
#: src/controller.cpp:272
#: src/controller.cpp:284
#, kde-format
msgid "Maybe it was deleted?"
msgstr "Kanskje han er sletta?"
#: src/controller.cpp:276
#: src/controller.cpp:288
#, kde-format
msgid "Access to keychain was denied."
msgstr "Vart nekta tilgang til nøkkelring."
#: src/controller.cpp:276
#: src/controller.cpp:288
#, kde-format
msgid "Please allow NeoChat to read the access token"
msgstr "Gje NeoChat løyve til å lesa tilgangspolletten"
#: src/controller.cpp:279
#: src/controller.cpp:291
#, kde-format
msgid "No keychain available."
msgstr "Ingen nøkkelring er tilgjengeleg."
#: src/controller.cpp:279
#: src/controller.cpp:291
#, kde-format
msgid "Please install a keychain, e.g. KWallet or GNOME keyring on Linux"
msgstr "Installer ein nøkkelring, for eksempel KWallet eller GNOME Keyring"
#: src/controller.cpp:282
#: src/controller.cpp:294
#, kde-format
msgid "Unable to read access token"
msgstr "Klarte ikkje lesa tilgangspollett"
#: src/controller.cpp:452
#: src/controller.cpp:464
#, kde-format
msgid "File too large to download."
msgstr "Fila er for stor til å kunna lastast ned."
#: src/controller.cpp:452
#: src/controller.cpp:464
#, kde-format
msgid "Contact your matrix server administrator for support."
msgstr "Ta kontakt med administratoren av Matrix-tenaren for brukarstøtte."
#: src/controller.cpp:491
#: src/controller.cpp:503
#, kde-format
msgid "Room creation failed: %1"
msgstr "Feil ved romregistrering: %1"
#: src/controller.cpp:512
#: src/controller.cpp:524
#, kde-format
msgid "Space creation failed: %1"
msgstr "Feil ved registrering av område: %1"
#: src/controller.cpp:526
#: src/controller.cpp:538
#, kde-format
msgid "The room id you are trying to join is not valid"
msgstr "Rom-ID-en du prøver å bruka, er ikkje gyldig"
@@ -202,17 +202,17 @@ msgctxt "<version number> (built against <possibly different version number>)"
msgid "%1 (built against %2)"
msgstr "%1 (bygd mot %2)"
#: src/main.cpp:333
#: src/main.cpp:334
#, kde-format
msgid "Client for the matrix communication protocol"
msgstr "Lynmeldings­klient for Matrix-protokollen"
#: src/main.cpp:334
#: src/main.cpp:335
#, kde-format
msgid "Supports matrix: url scheme"
msgstr "Støttar «matrix:»-adresser"
#: src/main.cpp:335
#: src/main.cpp:336
#, kde-format
msgid "Ignore all SSL Errors, e.g., unsigned certificates."
msgstr "Ignorer alle SSL-feil, for eksempel usignerte sertifikat."
@@ -227,312 +227,312 @@ msgstr "Medie-ID-en «%1» følgjer ikkje mønsteret «tenar/medie-ID»"
msgid "Image request has been cancelled"
msgstr "Biletførespurnaden vart avbroten"
#: src/models/actionsmodel.cpp:23
#: src/models/actionsmodel.cpp:24
#, kde-format
msgid "Leaving this room."
msgstr "Forlèt rommet."
#: src/models/actionsmodel.cpp:30 src/models/actionsmodel.cpp:227
#: src/models/actionsmodel.cpp:253 src/models/actionsmodel.cpp:283
#: src/models/actionsmodel.cpp:31 src/models/actionsmodel.cpp:228
#: src/models/actionsmodel.cpp:254 src/models/actionsmodel.cpp:284
#, kde-format
msgctxt "'<text>' does not look like a room id or alias."
msgid "'%1' does not look like a room id or alias."
msgstr "«%1» ser ikkje ut til å vera ein rom-ID eller eit alias."
#: src/models/actionsmodel.cpp:38
#: src/models/actionsmodel.cpp:39
#, kde-format
msgctxt "Leaving room <roomname>."
msgid "Leaving room %1."
msgstr "Forlèt rom %1."
#: src/models/actionsmodel.cpp:41
#: src/models/actionsmodel.cpp:42
#, kde-format
msgctxt "Room <roomname> not found"
msgid "Room %1 not found."
msgstr "Fann ikkje rommet %1."
#: src/models/actionsmodel.cpp:49 src/models/actionsmodel.cpp:319
#: src/models/actionsmodel.cpp:50 src/models/actionsmodel.cpp:320
#, kde-format
msgid "No new nickname provided, no changes will happen."
msgstr "Nytt kallenamn ikkje oppgjeve. Ingen endringar vert gjort."
#: src/models/actionsmodel.cpp:64 src/models/actionsmodel.cpp:74
#: src/models/actionsmodel.cpp:84 src/models/actionsmodel.cpp:94
#: src/models/actionsmodel.cpp:114 src/models/actionsmodel.cpp:134
#: src/models/actionsmodel.cpp:145 src/models/actionsmodel.cpp:161
#: src/models/actionsmodel.cpp:171 src/models/actionsmodel.cpp:181
#: src/models/actionsmodel.cpp:65 src/models/actionsmodel.cpp:75
#: src/models/actionsmodel.cpp:85 src/models/actionsmodel.cpp:95
#: src/models/actionsmodel.cpp:115 src/models/actionsmodel.cpp:135
#: src/models/actionsmodel.cpp:146 src/models/actionsmodel.cpp:162
#: src/models/actionsmodel.cpp:172 src/models/actionsmodel.cpp:182
msgid "<message>"
msgstr "<melding>"
#: src/models/actionsmodel.cpp:65
#: src/models/actionsmodel.cpp:66
msgid "Prepends ¯\\_(ツ)_/¯ to a plain-text message"
msgstr "Legg til ¯\\_(ツ)_/¯ i starten av ei reintekstmelding"
#: src/models/actionsmodel.cpp:75
#: src/models/actionsmodel.cpp:76
msgid "Prepends ( ͡° ͜ʖ ͡°) to a plain-text message"
msgstr "Legg til ( ͡° ͜ʖ ͡°) i starten av ei reintekstmelding"
#: src/models/actionsmodel.cpp:85
#: src/models/actionsmodel.cpp:86
msgid "Prepends (╯°□°)╯︵ ┻━┻ to a plain-text message"
msgstr "Legg til (╯°□°)╯︵ ┻━┻ i starten av ei reintekstmelding"
#: src/models/actionsmodel.cpp:95
#: src/models/actionsmodel.cpp:96
msgid "Prepends ┬──┬ ( ゜-゜ノ) to a plain-text message"
msgstr "Legg til ┬──┬ ( ゜-゜ノ) i starten av ei reintekstmelding"
#: src/models/actionsmodel.cpp:115
#: src/models/actionsmodel.cpp:116
msgid "Sends the given message colored as a rainbow"
msgstr "Sender oppgjeven melding fargelagd som ein regnboge"
#: src/models/actionsmodel.cpp:135
#: src/models/actionsmodel.cpp:136
msgid "Sends the given emote colored as a rainbow"
msgstr "Sender oppgjeve uttrykk fargelagd som ein regnboge"
#: src/models/actionsmodel.cpp:146
#: src/models/actionsmodel.cpp:147
msgid "Sends the given message as plain text"
msgstr "Sender oppgjeven melding som reintekst"
#: src/models/actionsmodel.cpp:162
#: src/models/actionsmodel.cpp:163
msgid "Sends the given message as a spoiler"
msgstr "Sender oppgjeven melding med røpealarm"
#: src/models/actionsmodel.cpp:172
#: src/models/actionsmodel.cpp:173
msgid "Sends the given emote"
msgstr "Sender oppgjeve uttrykk"
#: src/models/actionsmodel.cpp:182
#: src/models/actionsmodel.cpp:183
msgid "Sends the given message as a notice"
msgstr "Sender oppgjeven melding som ei varsling"
#: src/models/actionsmodel.cpp:191 src/models/actionsmodel.cpp:353
#: src/models/actionsmodel.cpp:381 src/models/actionsmodel.cpp:431
#: src/models/actionsmodel.cpp:469 src/models/actionsmodel.cpp:504
#: src/models/actionsmodel.cpp:192 src/models/actionsmodel.cpp:354
#: src/models/actionsmodel.cpp:382 src/models/actionsmodel.cpp:432
#: src/models/actionsmodel.cpp:470 src/models/actionsmodel.cpp:505
#, kde-format
msgctxt "'<text>' does not look like a matrix id."
msgid "'%1' does not look like a matrix id."
msgstr "«%1» ser ikkje ut til å vera ein Matrix-ID."
#: src/models/actionsmodel.cpp:196
#: src/models/actionsmodel.cpp:197
#, kde-format
msgctxt "<user> is already invited to this room."
msgid "%1 is already invited to this room."
msgstr "%1 er alt invitert til dette rommet."
#: src/models/actionsmodel.cpp:200
#: src/models/actionsmodel.cpp:201
#, kde-format
msgctxt "<user> is banned from this room."
msgid "%1 is banned from this room."
msgstr "%1 er utestengd frå rommet."
#: src/models/actionsmodel.cpp:204
#: src/models/actionsmodel.cpp:205
#, kde-format
msgid "You are already in this room."
msgstr "Du er alt i dette rommet."
#: src/models/actionsmodel.cpp:208
#: src/models/actionsmodel.cpp:209
#, kde-format
msgctxt "<user> is already in this room."
msgid "%1 is already in this room."
msgstr "%1 er alt i dette rommet."
#: src/models/actionsmodel.cpp:212
#: src/models/actionsmodel.cpp:213
#, kde-format
msgctxt "<username> was invited into this room"
msgid "%1 was invited into this room"
msgstr "%1 vart invitert til rommet"
#: src/models/actionsmodel.cpp:217 src/models/actionsmodel.cpp:371
#: src/models/actionsmodel.cpp:399 src/models/actionsmodel.cpp:492
#: src/models/actionsmodel.cpp:218 src/models/actionsmodel.cpp:372
#: src/models/actionsmodel.cpp:400 src/models/actionsmodel.cpp:493
msgid "<user id>"
msgstr "<brukar-ID>"
#: src/models/actionsmodel.cpp:218
#: src/models/actionsmodel.cpp:219
msgid "Invites the user to this room"
msgstr "Inviterer brukaren til rommet"
#: src/models/actionsmodel.cpp:235 src/models/actionsmodel.cpp:290
#: src/models/actionsmodel.cpp:236 src/models/actionsmodel.cpp:291
#, kde-format
msgctxt "Joining room <roomname>."
msgid "Joining room %1."
msgstr "Vert med i rommet %1."
#: src/models/actionsmodel.cpp:241 src/models/actionsmodel.cpp:296
#: src/models/actionsmodel.cpp:242 src/models/actionsmodel.cpp:297
msgid "<room alias or id>"
msgstr "<rom-alias eller -ID>"
#: src/models/actionsmodel.cpp:242 src/models/actionsmodel.cpp:297
#: src/models/actionsmodel.cpp:243 src/models/actionsmodel.cpp:298
msgid "Joins the given room"
msgstr "Vert med i rommet"
#: src/models/actionsmodel.cpp:261
#: src/models/actionsmodel.cpp:262
#, kde-format
msgctxt "Knocking room <roomname>."
msgid "Knocking room %1."
msgstr "Bankar på rommet %1."
#: src/models/actionsmodel.cpp:273
#: src/models/actionsmodel.cpp:274
msgid "<room alias or id> [<reason>]"
msgstr "<rom-alias eller -id> [<grunngjeving>]"
#: src/models/actionsmodel.cpp:274
#: src/models/actionsmodel.cpp:275
msgid "Requests to join the given room"
msgstr "Førespurnad om å få verta med i rommet"
#: src/models/actionsmodel.cpp:287
#: src/models/actionsmodel.cpp:288
#, kde-format
msgctxt "You are already in room <roomname>."
msgid "You are already in room %1."
msgstr "Du er alt med i rom %1."
#: src/models/actionsmodel.cpp:304 src/models/actionsmodel.cpp:312
#: src/models/actionsmodel.cpp:305 src/models/actionsmodel.cpp:313
msgid "[<room alias or id>]"
msgstr "[<rom-alias eller -id>]"
#: src/models/actionsmodel.cpp:305 src/models/actionsmodel.cpp:313
#: src/models/actionsmodel.cpp:306 src/models/actionsmodel.cpp:314
msgid "Leaves the given room or this room, if there is none given"
msgstr "Forlèt valt eller (viss romnamn ikkje er oppgjeve) gjeldande rom"
#: src/models/actionsmodel.cpp:327 src/models/actionsmodel.cpp:335
#: src/models/actionsmodel.cpp:343
#: src/models/actionsmodel.cpp:328 src/models/actionsmodel.cpp:336
#: src/models/actionsmodel.cpp:344
msgid "<display name>"
msgstr "<visingsnamn>"
#: src/models/actionsmodel.cpp:328
#: src/models/actionsmodel.cpp:329
msgid "Changes your global display name"
msgstr "Byter ditt globale visingsnamn"
#: src/models/actionsmodel.cpp:336 src/models/actionsmodel.cpp:344
#: src/models/actionsmodel.cpp:337 src/models/actionsmodel.cpp:345
msgid "Changes your display name in this room"
msgstr "Byter visingsnamnet ditt i dette rommet"
#: src/models/actionsmodel.cpp:358
#: src/models/actionsmodel.cpp:359
#, kde-format
msgctxt "<username> is already ignored."
msgid "%1 is already ignored."
msgstr "%1 er ignorert frå før."
#: src/models/actionsmodel.cpp:363
#: src/models/actionsmodel.cpp:364
#, kde-format
msgctxt "<username> is now ignored"
msgid "%1 is now ignored."
msgstr "%1 er no ignorert."
#: src/models/actionsmodel.cpp:365 src/models/actionsmodel.cpp:393
#: src/models/actionsmodel.cpp:366 src/models/actionsmodel.cpp:394
#, kde-format
msgctxt "<username> is not a known user"
msgid "%1 is not a known user."
msgstr "%1 er ein ukjend brukar."
#: src/models/actionsmodel.cpp:372
#: src/models/actionsmodel.cpp:373
msgid "Ignores the given user"
msgstr "Ignorerer brukaren"
#: src/models/actionsmodel.cpp:387
#: src/models/actionsmodel.cpp:388
#, kde-format
msgctxt "<username> is not ignored."
msgid "%1 is not ignored."
msgstr "%1 er ikkje ignorert."
#: src/models/actionsmodel.cpp:391
#: src/models/actionsmodel.cpp:392
#, kde-format
msgctxt "<username> is no longer ignored."
msgid "%1 is no longer ignored."
msgstr "%1 er ikkje lenger ignorert."
#: src/models/actionsmodel.cpp:400
#: src/models/actionsmodel.cpp:401
msgid "Unignores the given user"
msgstr "Avignorerer brukaren"
#: src/models/actionsmodel.cpp:420
#: src/models/actionsmodel.cpp:421
msgid "<reaction text>"
msgstr "<reaksjonstekst>"
#: src/models/actionsmodel.cpp:421
#: src/models/actionsmodel.cpp:422
msgid "React to the message with the given text"
msgstr "Reager på meldinga med ein tekst"
#: src/models/actionsmodel.cpp:436
#: src/models/actionsmodel.cpp:437
#, kde-format
msgctxt "<user> is already banned from this room."
msgid "%1 is already banned from this room."
msgstr "%1 er alt utestengd frå rommet."
#: src/models/actionsmodel.cpp:444
#: src/models/actionsmodel.cpp:445
#, kde-format
msgid "You are not allowed to ban users from this room."
msgstr "Du har ikkje løyve til å utestengja brukarar frå rommet."
#: src/models/actionsmodel.cpp:450
#: src/models/actionsmodel.cpp:451
#, kde-format
msgctxt "You are not allowed to ban <username> from this room."
msgid "You are not allowed to ban %1 from this room."
msgstr "Du har ikkje løyve til å utestengja %1 frå rommet."
#: src/models/actionsmodel.cpp:454
#: src/models/actionsmodel.cpp:455
#, kde-format
msgctxt "<username> was banned from this room."
msgid "%1 was banned from this room."
msgstr "%1 vart utestengd frå rommet."
#: src/models/actionsmodel.cpp:459 src/models/actionsmodel.cpp:536
#: src/models/actionsmodel.cpp:460 src/models/actionsmodel.cpp:537
msgid "<user id> [<reason>]"
msgstr "<brukar-ID> [<grunngjeving>]"
#: src/models/actionsmodel.cpp:460
#: src/models/actionsmodel.cpp:461
msgid "Bans the given user"
msgstr "Utestengjer brukaren"
#: src/models/actionsmodel.cpp:477
#: src/models/actionsmodel.cpp:478
#, kde-format
msgid "You are not allowed to unban users from this room."
msgstr "Du har ikkje løyve til å oppheva utestenging frå rommet."
#: src/models/actionsmodel.cpp:482
#: src/models/actionsmodel.cpp:483
#, kde-format
msgctxt "<user> is not banned from this room."
msgid "%1 is not banned from this room."
msgstr "%1 er ikkje utestengd frå rommet."
#: src/models/actionsmodel.cpp:486
#: src/models/actionsmodel.cpp:487
#, kde-format
msgctxt "<username> was unbanned from this room."
msgid "%1 was unbanned from this room."
msgstr "%1 er ikkje lenger utestengd frå rommet."
#: src/models/actionsmodel.cpp:493
#: src/models/actionsmodel.cpp:494
msgid "Removes the ban of the given user"
msgstr "Opphevar utestenging av brukaren"
#: src/models/actionsmodel.cpp:508
#: src/models/actionsmodel.cpp:509
#, kde-format
msgid "You cannot kick yourself from the room."
msgstr "Du kan ikkje kasta deg sjølv ut av rommet."
#: src/models/actionsmodel.cpp:512
#: src/models/actionsmodel.cpp:513
#, kde-format
msgctxt "<username> is not in this room"
msgid "%1 is not in this room."
msgstr "%1 er ikkje med i rommet."
#: src/models/actionsmodel.cpp:521
#: src/models/actionsmodel.cpp:522
#, kde-format
msgid "You are not allowed to kick users from this room."
msgstr "Du har ikkje løyve til å kasta brukarar ut av rommet."
#: src/models/actionsmodel.cpp:527
#: src/models/actionsmodel.cpp:528
#, kde-format
msgctxt "You are not allowed to kick <username> from this room"
msgid "You are not allowed to kick %1 from this room."
msgstr "Du har ikkje løyve til å kasta %1 ut av rommet."
#: src/models/actionsmodel.cpp:531
#: src/models/actionsmodel.cpp:532
#, kde-format
msgctxt "<username> was kicked from this room."
msgid "%1 was kicked from this room."
msgstr "%1 vart kasta ut av rommet."
# Er dette noko anna enn å kasta brukaren ut?
#: src/models/actionsmodel.cpp:537
#: src/models/actionsmodel.cpp:538
msgid "Removes the user from the room"
msgstr "Fjernar brukaren frå rommet"
@@ -636,13 +636,13 @@ msgctxt "'Custom' is a category of emoji"
msgid "Custom"
msgstr "Tilpassa"
#: src/models/imagepacksmodel.cpp:86
#: src/models/imagepacksmodel.cpp:87
#, kde-format
msgctxt "As in 'The user's own Stickers'"
msgid "Own Stickers"
msgstr "Eigne klistremerke"
#: src/models/imagepacksmodel.cpp:86
#: src/models/imagepacksmodel.cpp:87
#, kde-format
msgctxt "As in 'The user's own emojis"
msgid "Own Emojis"
@@ -1079,12 +1079,12 @@ msgstr "oppdaterte tilstanden"
msgid "started a poll"
msgstr "starta ei avstemming"
#: src/neochatroom.cpp:1627 src/neochatroom.cpp:1628
#: src/neochatroom.cpp:1631 src/neochatroom.cpp:1632
#, kde-format
msgid "Report sent successfully."
msgstr "Rapporten er no send."
#: src/neochatroom.cpp:1924 src/neochatroom.cpp:1932
#: src/neochatroom.cpp:1928 src/neochatroom.cpp:1936
#, kde-format
msgctxt "'Lat' and 'Lon' as in Latitude and Longitude"
msgid "Lat: %1, Lon: %2"
@@ -1095,7 +1095,7 @@ msgstr "Breiddegr.: %1  lengdegr.: %2"
msgid "Encrypted Message"
msgstr "Kryptert melding"
#: src/notificationsmanager.cpp:201 src/qml/main.qml:253
#: src/notificationsmanager.cpp:201 src/qml/main.qml:257
#, kde-format
msgid "%1: %2"
msgstr "%1: %2"
@@ -1362,7 +1362,8 @@ msgstr "Avvis"
msgid "Accept"
msgstr "Godta"
#: src/qml/Component/LocationPage.qml:17 src/qml/Panel/RoomDrawer.qml:200
#: src/qml/Component/LocationPage.qml:17
#: src/qml/RoomDrawer/RoomInformation.qml:116
#, kde-format
msgctxt "Locations on a map"
msgid "Locations"
@@ -1608,22 +1609,22 @@ msgstr "Lydstyrke"
msgid "Maximize"
msgstr "Maksimer"
#: src/qml/Component/TimelineView.qml:153
#: src/qml/Component/TimelineView.qml:160
#, kde-format
msgid "Jump to first unread message"
msgstr "Gå til første ulesne melding"
#: src/qml/Component/TimelineView.qml:176
#: src/qml/Component/TimelineView.qml:183
#, kde-format
msgid "Jump to latest message"
msgstr "Gå til nyaste melding"
#: src/qml/Component/TimelineView.qml:202
#: src/qml/Component/TimelineView.qml:209
#, kde-format
msgid "Drag items here to share them"
msgstr "Dra element her for å dela dei"
#: src/qml/Component/TimelineView.qml:228
#: src/qml/Component/TimelineView.qml:235
#, kde-format
msgctxt "Message displayed when some users are typing"
msgid "%2 is typing"
@@ -1956,80 +1957,80 @@ msgctxt "@title:menu Account detail dialog"
msgid "Account detail"
msgstr "Konto­detaljar"
#: src/qml/Dialog/UserDetailDialog.qml:80
#: src/qml/Dialog/UserDetailDialog.qml:81
#, kde-format
msgid "Unignore this user"
msgstr "Avignorer brukaren"
#: src/qml/Dialog/UserDetailDialog.qml:80
#: src/qml/Dialog/UserDetailDialog.qml:81
#, kde-format
msgid "Ignore this user"
msgstr "Ignorer brukaren"
#: src/qml/Dialog/UserDetailDialog.qml:92
#: src/qml/Dialog/UserDetailDialog.qml:93
#, kde-format
msgid "Kick this user"
msgstr "Kast ut brukaren"
#: src/qml/Dialog/UserDetailDialog.qml:105
#: src/qml/Dialog/UserDetailDialog.qml:106
#, kde-format
msgid "Invite this user"
msgstr "Inviter brukaren"
#: src/qml/Dialog/UserDetailDialog.qml:117
#: src/qml/Dialog/UserDetailDialog.qml:118
#, kde-format
msgid "Ban this user"
msgstr "Utesteng brukaren"
#: src/qml/Dialog/UserDetailDialog.qml:122
#: src/qml/Dialog/UserDetailDialog.qml:123
#, kde-format
msgctxt "@title"
msgid "Ban User"
msgstr "Utesteng brukar"
#: src/qml/Dialog/UserDetailDialog.qml:133
#: src/qml/Dialog/UserDetailDialog.qml:134
#, kde-format
msgid "Unban this user"
msgstr "Opphev utestenging av brukaren"
#: src/qml/Dialog/UserDetailDialog.qml:145
#: src/qml/Dialog/UserDetailDialog.qml:146
#, kde-format
msgid "Set user power level"
msgstr "Vel maktnivå for brukar"
#: src/qml/Dialog/UserDetailDialog.qml:169
#: src/qml/Dialog/UserDetailDialog.qml:170
#, kde-format
msgid "Remove recent messages by this user"
msgstr "Fjern nylege meldingar frå brukaren"
#: src/qml/Dialog/UserDetailDialog.qml:174
#: src/qml/Dialog/UserDetailDialog.qml:175
#, kde-format
msgctxt "@title"
msgid "Remove Messages"
msgstr "Fjern meldingar"
#: src/qml/Dialog/UserDetailDialog.qml:184
#: src/qml/Dialog/UserDetailDialog.qml:185
#, kde-format
msgid "Open a private chat"
msgstr "Start privat prat"
#: src/qml/Dialog/UserDetailDialog.qml:194
#: src/qml/Dialog/UserDetailDialog.qml:195
#, kde-format
msgid "Copy link"
msgstr "Kopier lenkje"
#: src/qml/main.qml:293
#: src/qml/main.qml:297
#, kde-format
msgctxt "@title:window"
msgid "Session Verification"
msgstr "Øktstadfesting"
#: src/qml/main.qml:305
#: src/qml/main.qml:309
#, kde-format
msgid "User consent"
msgstr "Brukar­samtykke"
#: src/qml/main.qml:310
#: src/qml/main.qml:314
#, kde-format
msgid ""
"Your homeserver requires you to agree to its terms and conditions before "
@@ -2038,17 +2039,17 @@ msgstr ""
"Heimetenaren krev at du godtek brukarvilkåra før du kan ta han i bruk. Trykk "
"på knappen nedanfor for å lesa vilkåra."
#: src/qml/main.qml:315
#: src/qml/main.qml:319
#, kde-format
msgid "Open"
msgstr "Opna"
#: src/qml/main.qml:350
#: src/qml/main.qml:354
#, kde-format
msgid "Start a chat"
msgstr "Start prat"
#: src/qml/main.qml:352
#: src/qml/main.qml:356
#, kde-format
msgid "Do you want to start a chat with %1?"
msgstr "Vil du starta ein prat med %1?"
@@ -2226,25 +2227,25 @@ msgstr "Feil ved deling"
msgid "Shared url for image is <a href='%1'>%1</a>"
msgstr "Delt adresse for biletet er <a href='%1'>%1</a>"
#: src/qml/Menu/Timeline/BanSheet.qml:16
#: src/qml/Menu/Timeline/BanSheet.qml:18
#, kde-format
msgid "Ban User"
msgstr "Utesteng brukar"
#: src/qml/Menu/Timeline/BanSheet.qml:20
#: src/qml/Menu/Timeline/BanSheet.qml:22
#, kde-format
msgid "Reason for banning this user"
msgstr "Grunngjeving for utestenging av brukaren"
#: src/qml/Menu/Timeline/BanSheet.qml:32
#: src/qml/Menu/Timeline/BanSheet.qml:34
#, kde-format
msgctxt "@action:button 'Ban' as in 'Ban this user'"
msgid "Ban"
msgstr "Utesteng"
#: src/qml/Menu/Timeline/BanSheet.qml:41
#: src/qml/Menu/Timeline/BanSheet.qml:43
#: src/qml/Menu/Timeline/RemoveSheet.qml:49
#: src/qml/Menu/Timeline/ReportSheet.qml:41 src/qml/Page/InviteUserPage.qml:24
#: src/qml/Menu/Timeline/ReportSheet.qml:43 src/qml/Page/InviteUserPage.qml:24
#, kde-format
msgctxt "@action"
msgid "Cancel"
@@ -2281,7 +2282,7 @@ msgstr "Fjern melding"
#: src/qml/Menu/Timeline/FileDelegateContextMenu.qml:83
#: src/qml/Menu/Timeline/MessageDelegateContextMenu.qml:59
#: src/qml/Menu/Timeline/ReportSheet.qml:32
#: src/qml/Menu/Timeline/ReportSheet.qml:34
#, kde-format
msgctxt ""
"@action:button 'Report' as in 'Report this event to the administrators'"
@@ -2348,17 +2349,17 @@ msgctxt "@action:button 'Remove' as in 'Remove this message'"
msgid "Remove"
msgstr "Fjern"
#: src/qml/Menu/Timeline/ReportSheet.qml:16
#: src/qml/Menu/Timeline/ReportSheet.qml:18
#, kde-format
msgid "Report Message"
msgstr "Rapporter melding"
#: src/qml/Menu/Timeline/ReportSheet.qml:20
#: src/qml/Menu/Timeline/ReportSheet.qml:22
#, kde-format
msgid "Reason for reporting this message"
msgstr "Grunngjeving for rapportering av meldinga"
#: src/qml/Page/DevtoolsPage.qml:17 src/qml/Panel/RoomDrawer.qml:159
#: src/qml/Page/DevtoolsPage.qml:17 src/qml/RoomDrawer/RoomInformation.qml:75
#, kde-format
msgid "Developer Tools"
msgstr "Utviklarverktøy"
@@ -2519,7 +2520,7 @@ msgstr "Vart med"
#, kde-format
msgctxt "@info:label"
msgid "No rooms found"
msgstr ""
msgstr "Fann ingen rom"
#: src/qml/Page/RoomList/AccountMenu.qml:18
#: src/qml/Page/RoomList/UserInfo.qml:175
@@ -2634,7 +2635,9 @@ msgstr "Av"
#: src/qml/Page/RoomList/ContextMenu.qml:117
#: src/qml/Page/RoomList/ContextMenu.qml:119
#: src/qml/Page/RoomList/ContextMenu.qml:186 src/qml/Panel/RoomDrawer.qml:105
#: src/qml/Page/RoomList/ContextMenu.qml:186
#: src/qml/RoomDrawer/RoomDrawer.qml:98
#: src/qml/RoomDrawer/RoomDrawerPage.qml:37
#, kde-format
msgid "Room Settings"
msgstr "Romval"
@@ -2646,7 +2649,7 @@ msgid "Leave Room"
msgstr "Forlat rommet"
#: src/qml/Page/RoomList/ExploreComponent.qml:19
#: src/qml/Page/RoomList/Page.qml:154
#: src/qml/Page/RoomList/Page.qml:153
#, kde-format
msgid "Explore rooms"
msgstr "Utforsk rom"
@@ -2657,28 +2660,28 @@ msgstr "Utforsk rom"
msgid "Create rooms and chats"
msgstr "Opprett rom og diskusjonar"
#: src/qml/Page/RoomList/Page.qml:151
#: src/qml/Page/RoomList/Page.qml:150
#, kde-format
msgid "No rooms found"
msgstr "Fann ingen rom"
#: src/qml/Page/RoomList/Page.qml:151
#: src/qml/Page/RoomList/Page.qml:150
#, kde-format
msgid "Join some rooms to get started"
msgstr "Start ved å verta med i nokre rom"
#: src/qml/Page/RoomList/Page.qml:154
#: src/qml/Page/RoomList/Page.qml:153
#, kde-format
msgid "Search in room directory"
msgstr "Søk i romkatalogen"
#: src/qml/Page/RoomList/Page.qml:196
#: src/qml/Page/RoomList/Page.qml:195
#, kde-format
msgctxt "Collapse <section name>"
msgid "Collapse %1"
msgstr "Fald saman %1"
#: src/qml/Page/RoomList/Page.qml:196
#: src/qml/Page/RoomList/Page.qml:195
#, kde-format
msgctxt "Expand <section name"
msgid "Expand %1"
@@ -2750,7 +2753,7 @@ msgstr "Byt brukar"
msgid "Open Settings"
msgstr "Opna innstillingar"
#: src/qml/Page/RoomPage.qml:42
#: src/qml/Page/RoomPage.qml:50
#, kde-format
msgid "NeoChat is offline. Please check your network connection."
msgstr "NeoChat er fråkopla. Sjå til at du er kopla til nettet."
@@ -2791,97 +2794,103 @@ msgstr "Start ny prat"
msgid "Welcome to Matrix"
msgstr "Velkommen til Matrix"
#: src/qml/Panel/GroupChatDrawerHeader.qml:62
#: src/qml/RoomDrawer/GroupChatDrawerHeader.qml:62
#, kde-format
msgid "No name"
msgstr "Namnlaus"
#: src/qml/Panel/GroupChatDrawerHeader.qml:71
#: src/qml/RoomDrawer/GroupChatDrawerHeader.qml:71
#, kde-format
msgid "No Canonical Alias"
msgstr "Manglar kanonisk alias"
#: src/qml/Panel/GroupChatDrawerHeader.qml:81
#: src/qml/RoomDrawer/GroupChatDrawerHeader.qml:81
#, kde-format
msgid "No Topic"
msgstr "Manglar emne"
#: src/qml/Panel/RoomDrawer.qml:95
#: src/qml/RoomDrawer/RoomDrawer.qml:88
#, kde-format
msgid "Room information"
msgstr "Rominformasjon"
#: src/qml/Panel/RoomDrawer.qml:102
#: src/qml/RoomDrawer/RoomDrawer.qml:95
#, kde-format
msgid "Room settings"
msgstr "Romval"
#: src/qml/RoomDrawer/RoomInformation.qml:38
#, kde-format
msgctxt "@action:title"
msgid "Room information"
msgstr ""
# Er ulike handlingar ein kan gjera, for eksempel gjera rommet til favoritt. «Handlingar» fungerer derfor betre enn «Val» eller «Alternativ».
#: src/qml/Panel/RoomDrawer.qml:143
#: src/qml/RoomDrawer/RoomInformation.qml:59
#, kde-format
msgid "Options"
msgstr "Handlingar"
#: src/qml/Panel/RoomDrawer.qml:153
#: src/qml/RoomDrawer/RoomInformation.qml:69
#, kde-format
msgid "Open developer tools"
msgstr "Opna utviklarverktøy"
#: src/qml/Panel/RoomDrawer.qml:167
#: src/qml/RoomDrawer/RoomInformation.qml:83
#, kde-format
msgid "Search in this room"
msgstr "Søk i rommet"
#: src/qml/Panel/RoomDrawer.qml:175
#: src/qml/RoomDrawer/RoomInformation.qml:91
#, kde-format
msgctxt "@action:title"
msgid "Search"
msgstr "Søk"
#: src/qml/Panel/RoomDrawer.qml:184
#: src/qml/RoomDrawer/RoomInformation.qml:100
#, kde-format
msgid "Remove room from favorites"
msgstr "Fjern rommet frå favorittar"
#: src/qml/Panel/RoomDrawer.qml:184
#: src/qml/RoomDrawer/RoomInformation.qml:100
#, kde-format
msgid "Make room favorite"
msgstr "Gjer rommet til favoritt"
#: src/qml/Panel/RoomDrawer.qml:195
#: src/qml/RoomDrawer/RoomInformation.qml:111
#, kde-format
msgid "Show locations for this room"
msgstr "Vis posisjonar i rommet"
#: src/qml/Panel/RoomDrawer.qml:207
#: src/qml/RoomDrawer/RoomInformation.qml:123
#, kde-format
msgid "Members"
msgstr "Medlemmar"
#: src/qml/Panel/RoomDrawer.qml:218
#: src/qml/RoomDrawer/RoomInformation.qml:134
#, kde-format
msgid "Search user in room"
msgstr "Søk etter brukarar i rommet"
#: src/qml/Panel/RoomDrawer.qml:231
#: src/qml/RoomDrawer/RoomInformation.qml:147
#, kde-format
msgctxt "@title"
msgid "Invite a User"
msgstr "Inviter ein brukar"
#: src/qml/Panel/RoomDrawer.qml:234
#: src/qml/RoomDrawer/RoomInformation.qml:150
#, kde-format
msgid "Invite user to room"
msgstr "Inviter brukar til rommet"
#: src/qml/Panel/RoomDrawer.qml:241
#: src/qml/RoomDrawer/RoomInformation.qml:157
#, kde-format
msgid "%1 member"
msgid_plural "%1 members"
msgstr[0] "%1 medlem"
msgstr[1] "%1 medlemmar"
#: src/qml/Panel/RoomDrawer.qml:241
#: src/qml/RoomDrawer/RoomInformation.qml:157
#, kde-format
msgid "No member count"
msgstr "Manglar medlemstal"

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

@@ -74,8 +74,6 @@ add_library(neochat STATIC
blurhash.h
blurhashimageprovider.cpp
blurhashimageprovider.h
models/collapsestateproxymodel.cpp
models/collapsestateproxymodel.h
models/mediamessagefiltermodel.cpp
models/mediamessagefiltermodel.h
urlhelper.cpp
@@ -125,6 +123,9 @@ add_library(neochat STATIC
events/pollevent.cpp
pollhandler.cpp
utils.h
registration.cpp
neochatconnection.cpp
neochatconnection.h
)
ecm_qt_declare_logging_category(neochat
@@ -141,6 +142,11 @@ add_executable(neochat-app
${CMAKE_CURRENT_SOURCE_DIR}/res.generated.qrc
)
if(TARGET Qt::WebView)
target_link_libraries(neochat-app PUBLIC Qt::WebView)
target_compile_definitions(neochat-app PUBLIC -DHAVE_WEBVIEW)
endif()
target_include_directories(neochat-app PRIVATE ${CMAKE_BINARY_DIR})
target_link_libraries(neochat-app PRIVATE
@@ -177,6 +183,7 @@ endif()
target_include_directories(neochat PRIVATE ${CMAKE_BINARY_DIR})
target_link_libraries(neochat PUBLIC Qt::Core Qt::Quick Qt::Qml Qt::Gui Qt::Multimedia Qt::Network Qt::QuickControls2 KF${QT_MAJOR_VERSION}::I18n KF${QT_MAJOR_VERSION}::Kirigami2 KF${QT_MAJOR_VERSION}::Notifications KF${QT_MAJOR_VERSION}::ConfigCore KF${QT_MAJOR_VERSION}::ConfigGui KF${QT_MAJOR_VERSION}::CoreAddons KF${QT_MAJOR_VERSION}::SonnetCore KF${QT_MAJOR_VERSION}::ItemModels Quotient${QUOTIENT_SUFFIX} cmark::cmark QCoro::Core)
kconfig_add_kcfg_files(neochat GENERATE_MOC neochatconfig.kcfgc)
if(NEOCHAT_FLATPAK)

View File

@@ -21,7 +21,6 @@
#include <QFile>
#include <QFileInfo>
#include <QGuiApplication>
#include <QImageReader>
#include <QNetworkProxy>
#include <QQuickTextDocument>
#include <QQuickWindow>
@@ -33,10 +32,8 @@
#include <Quotient/accountregistry.h>
#include <Quotient/connection.h>
#include <Quotient/csapi/content-repo.h>
#include <Quotient/csapi/logout.h>
#include <Quotient/csapi/notifications.h>
#include <Quotient/csapi/profile.h>
#include <Quotient/eventstats.h>
#include <Quotient/jobs/downloadfilejob.h>
#include <Quotient/qt_connection_util.h>
@@ -105,8 +102,8 @@ Controller::Controller(QObject *parent)
static int oldAccountCount = 0;
connect(&m_accountRegistry, &AccountRegistry::accountCountChanged, this, [this]() {
if (m_accountRegistry.size() > oldAccountCount) {
auto connection = m_accountRegistry.accounts()[m_accountRegistry.size() - 1];
connect(connection, &Connection::syncDone, this, [connection]() {
auto connection = dynamic_cast<NeoChatConnection *>(m_accountRegistry.accounts()[m_accountRegistry.size() - 1]);
connect(connection, &NeoChatConnection::syncDone, this, [connection]() {
NotificationsManager::instance().handleNotifications(connection);
});
}
@@ -141,38 +138,7 @@ void Controller::toggleWindow()
}
}
void Controller::logout(Connection *conn, bool serverSideLogout)
{
if (!conn) {
qCritical() << "Attempt to logout null connection";
return;
}
SettingsGroup("Accounts"_ls).remove(conn->userId());
QKeychain::DeletePasswordJob job(qAppName());
job.setAutoDelete(true);
job.setKey(conn->userId());
QEventLoop loop;
QKeychain::DeletePasswordJob::connect(&job, &QKeychain::Job::finished, &loop, &QEventLoop::quit);
job.start();
loop.exec();
if (m_accountRegistry.count() > 1) {
// Only set the connection if the the account being logged out is currently active
if (conn == activeConnection()) {
setActiveConnection(m_accountRegistry.accounts()[0]);
}
} else {
setActiveConnection(nullptr);
}
if (!serverSideLogout) {
return;
}
conn->logout();
}
void Controller::addConnection(Connection *c)
void Controller::addConnection(NeoChatConnection *c)
{
Q_ASSERT_X(c, __FUNCTION__, "Attempt to add a null connection");
@@ -180,17 +146,17 @@ void Controller::addConnection(Connection *c)
c->setLazyLoading(true);
connect(c, &Connection::syncDone, this, [this, c] {
connect(c, &NeoChatConnection::syncDone, this, [this, c] {
Q_EMIT syncDone();
c->sync(30000);
c->saveState();
});
connect(c, &Connection::loggedOut, this, [this, c] {
connect(c, &NeoChatConnection::loggedOut, this, [this, c] {
dropConnection(c);
});
connect(c, &Connection::requestFailed, this, [this](BaseJob *job) {
connect(c, &NeoChatConnection::requestFailed, this, [this](BaseJob *job) {
if (job->error() == BaseJob::UserConsentRequired) {
Q_EMIT userConsentRequired(job->errorUrl());
}
@@ -202,7 +168,7 @@ void Controller::addConnection(Connection *c)
Q_EMIT accountCountChanged();
}
void Controller::dropConnection(Connection *c)
void Controller::dropConnection(NeoChatConnection *c)
{
Q_ASSERT_X(c, __FUNCTION__, "Attempt to drop a null connection");
@@ -231,19 +197,19 @@ void Controller::invokeLogin()
return;
}
auto connection = new Connection(account.homeserver());
connect(connection, &Connection::connected, this, [this, connection, id] {
auto connection = new NeoChatConnection(account.homeserver());
connect(connection, &NeoChatConnection::connected, this, [this, connection, id] {
connection->loadState();
addConnection(connection);
if (connection->userId() == id) {
setActiveConnection(connection);
connectSingleShot(connection, &Connection::syncDone, this, &Controller::initiated);
connectSingleShot(connection, &NeoChatConnection::syncDone, this, &Controller::initiated);
}
});
connect(connection, &Connection::loginError, this, [this, connection](const QString &error, const QString &) {
connect(connection, &NeoChatConnection::loginError, this, [this, connection](const QString &error, const QString &) {
if (error == "Unrecognised access token"_ls) {
Q_EMIT errorOccured(i18n("Login Failed: Access Token invalid or revoked"));
logout(connection, false);
connection->logout(false);
} else if (error == "Connection closed"_ls) {
Q_EMIT errorOccured(i18n("Login Failed: %1", error));
// Failed due to network connection issue. This might happen when the homeserver is
@@ -251,11 +217,11 @@ void Controller::invokeLogin()
// connect to the homeserver. In this case, we don't want to do logout().
} else {
Q_EMIT errorOccured(i18n("Login Failed: %1", error));
logout(connection, true);
connection->logout(true);
}
Q_EMIT initiated();
});
connect(connection, &Connection::networkError, this, [this](const QString &error, const QString &, int, int) {
connect(connection, &NeoChatConnection::networkError, this, [this](const QString &error, const QString &, int, int) {
Q_EMIT errorOccured(i18n("Network Error: %1", error));
});
connection->assumeIdentity(account.userId(), accessToken);
@@ -321,22 +287,6 @@ bool Controller::saveAccessTokenToKeyChain(const AccountSettings &account, const
return true;
}
void Controller::changeAvatar(Connection *conn, const QUrl &localFile)
{
auto job = conn->uploadFile(localFile.toLocalFile());
connect(job, &BaseJob::success, this, [conn, job] {
conn->callApi<SetAvatarUrlJob>(conn->userId(), job->contentUri());
});
}
void Controller::markAllMessagesAsRead(Connection *conn)
{
const auto rooms = conn->allRooms();
for (auto room : rooms) {
room->markAllMessagesAsRead();
}
}
bool Controller::supportSystemTray() const
{
#ifdef Q_OS_ANDROID
@@ -347,49 +297,6 @@ bool Controller::supportSystemTray() const
#endif
}
void Controller::changePassword(Connection *connection, const QString &currentPassword, const QString &newPassword)
{
NeochatChangePasswordJob *job = connection->callApi<NeochatChangePasswordJob>(newPassword, false);
connect(job, &BaseJob::result, this, [this, job, currentPassword, newPassword, connection] {
if (job->error() == 103) {
QJsonObject replyData = job->jsonData();
QJsonObject authData;
authData["session"_ls] = replyData["session"_ls];
authData["password"_ls] = currentPassword;
authData["type"_ls] = "m.login.password"_ls;
authData["user"_ls] = connection->user()->id();
QJsonObject identifier = {{"type"_ls, "m.id.user"_ls}, {"user"_ls, connection->user()->id()}};
authData["identifier"_ls] = identifier;
NeochatChangePasswordJob *innerJob = connection->callApi<NeochatChangePasswordJob>(newPassword, false, authData);
connect(innerJob, &BaseJob::success, this, [this]() {
Q_EMIT passwordStatus(PasswordStatus::Success);
});
connect(innerJob, &BaseJob::failure, this, [innerJob, this]() {
if (innerJob->jsonData()["errcode"_ls] == "M_FORBIDDEN"_ls) {
Q_EMIT passwordStatus(PasswordStatus::Wrong);
} else {
Q_EMIT passwordStatus(PasswordStatus::Other);
}
});
}
});
}
bool Controller::setAvatar(Connection *connection, const QUrl &avatarSource)
{
User *localUser = connection->user();
QString decoded = avatarSource.path();
if (decoded.isEmpty()) {
connection->callApi<SetAvatarUrlJob>(localUser->id(), avatarSource);
return true;
}
if (QImageReader(decoded).read().isNull()) {
return false;
} else {
return localUser->setAvatar(decoded);
}
}
NeochatChangePasswordJob::NeochatChangePasswordJob(const QString &newPassword, bool logoutDevices, const Omittable<QJsonObject> &auth)
: BaseJob(HttpVerb::Post, QStringLiteral("ChangePasswordJob"), "/_matrix/client/r0/account/password")
{
@@ -400,6 +307,14 @@ NeochatChangePasswordJob::NeochatChangePasswordJob(const QString &newPassword, b
setRequestData(_data);
}
NeoChatDeactivateAccountJob::NeoChatDeactivateAccountJob(const Quotient::Omittable<QJsonObject> &auth)
: BaseJob(HttpVerb::Post, QStringLiteral("DisableDeviceJob"), "_matrix/client/v3/account/deactivate")
{
QJsonObject data;
addParam<IfNotEmpty>(data, QStringLiteral("auth"), auth);
setRequestData(data);
}
int Controller::accountCount() const
{
return m_accountRegistry.count();
@@ -425,7 +340,7 @@ void Controller::setQuitOnLastWindowClosed()
#endif
}
Connection *Controller::activeConnection() const
NeoChatConnection *Controller::activeConnection() const
{
if (m_connection.isNull()) {
return nullptr;
@@ -433,49 +348,43 @@ Connection *Controller::activeConnection() const
return m_connection;
}
void Controller::setActiveConnection(Connection *connection)
void Controller::setActiveConnection(NeoChatConnection *connection)
{
if (connection == m_connection) {
return;
}
if (m_connection != nullptr) {
disconnect(m_connection, &Connection::syncError, this, nullptr);
disconnect(m_connection, &Connection::accountDataChanged, this, nullptr);
disconnect(m_connection, &NeoChatConnection::syncError, this, nullptr);
disconnect(m_connection, &NeoChatConnection::accountDataChanged, this, nullptr);
}
m_connection = connection;
if (connection != nullptr) {
NeoChatConfig::self()->setActiveConnection(connection->userId());
connect(connection, &Connection::networkError, this, [this]() {
connect(connection, &NeoChatConnection::networkError, this, [this]() {
if (!m_isOnline) {
return;
}
m_isOnline = false;
Q_EMIT isOnlineChanged(false);
});
connect(connection, &Connection::syncDone, this, [this] {
connect(connection, &NeoChatConnection::syncDone, this, [this] {
if (m_isOnline) {
return;
}
m_isOnline = true;
Q_EMIT isOnlineChanged(true);
});
connect(connection, &Connection::requestFailed, this, [](BaseJob *job) {
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."));
}
});
connect(connection, &Connection::accountDataChanged, this, [this](const QString &type) {
if (type == QLatin1String("org.kde.neochat.account_label")) {
Q_EMIT activeAccountLabelChanged();
}
});
} else {
NeoChatConfig::self()->setActiveConnection(QString());
}
NeoChatConfig::self()->save();
Q_EMIT activeConnectionChanged();
Q_EMIT activeConnectionIndexChanged();
Q_EMIT activeAccountLabelChanged();
}
PushRuleModel *Controller::pushRuleModel() const
@@ -646,41 +555,6 @@ bool Controller::isFlatpak() const
#endif
}
void Controller::setActiveAccountLabel(const QString &label)
{
if (!m_connection) {
return;
}
QJsonObject json{
{"account_label"_ls, label},
};
m_connection->setAccountData("org.kde.neochat.account_label"_ls, json);
}
QString Controller::activeAccountLabel() const
{
if (!m_connection) {
return {};
}
return m_connection->accountDataJson("org.kde.neochat.account_label"_ls)["account_label"_ls].toString();
}
QVariantList Controller::getSupportedRoomVersions(Quotient::Connection *connection)
{
auto roomVersions = connection->availableRoomVersions();
QVariantList supportedRoomVersions;
for (const Quotient::Connection::SupportedRoomVersion &v : roomVersions) {
QVariantMap roomVersionMap;
roomVersionMap.insert("id"_ls, v.id);
roomVersionMap.insert("status"_ls, v.status);
roomVersionMap.insert("isStable"_ls, v.isStable());
supportedRoomVersions.append(roomVersionMap);
}
return supportedRoomVersions;
}
AccountRegistry &Controller::accounts()
{
return m_accountRegistry;

View File

@@ -9,6 +9,7 @@
#include <KFormat>
#include "neochatconnection.h"
#include <Quotient/accountregistry.h>
#include <Quotient/jobs/basejob.h>
#include <Quotient/settings.h>
@@ -20,7 +21,6 @@ class QQuickTextDocument;
namespace Quotient
{
class Connection;
class Room;
class User;
}
@@ -50,7 +50,7 @@ class Controller : public QObject
/**
* @brief The current connection for the rest of NeoChat to use.
*/
Q_PROPERTY(Quotient::Connection *activeConnection READ activeConnection WRITE setActiveConnection NOTIFY activeConnectionChanged)
Q_PROPERTY(NeoChatConnection *activeConnection READ activeConnection WRITE setActiveConnection NOTIFY activeConnectionChanged)
/**
* @brief The PushRuleModel that has the active connection's push rules.
@@ -62,16 +62,6 @@ class Controller : public QObject
*/
Q_PROPERTY(int activeConnectionIndex READ activeConnectionIndex NOTIFY activeConnectionIndexChanged)
/**
* @brief The account label for the active account.
*
* Account labels are a concept specific to NeoChat, allowing accounts to be
* labelled, e.g. for "Work", "Private", etc.
*
* Set to an empty string to remove the label.
*/
Q_PROPERTY(QString activeAccountLabel READ activeAccountLabel WRITE setActiveAccountLabel NOTIFY activeAccountLabelChanged)
/**
* @brief Whether the OS NeoChat is running on supports sytem tray icons.
*/
@@ -109,46 +99,28 @@ public:
[[nodiscard]] int accountCount() const;
void setActiveConnection(Quotient::Connection *connection);
[[nodiscard]] Quotient::Connection *activeConnection() const;
void setActiveConnection(NeoChatConnection *connection);
[[nodiscard]] NeoChatConnection *activeConnection() const;
[[nodiscard]] PushRuleModel *pushRuleModel() const;
/**
* @brief Add a new connection to the account registry.
*/
void addConnection(Quotient::Connection *c);
void addConnection(NeoChatConnection *c);
/**
* @brief Drop a connection from the account registry.
*/
void dropConnection(Quotient::Connection *c);
void dropConnection(NeoChatConnection *c);
int activeConnectionIndex() const;
[[nodiscard]] QString activeAccountLabel() const;
void setActiveAccountLabel(const QString &label);
/**
* @brief Save an access token to the keychain for the given account.
*/
bool saveAccessTokenToKeyChain(const Quotient::AccountSettings &account, const QByteArray &accessToken);
/**
* @brief Change the password for an account.
*
* The function emits a passwordStatus signal with a PasswordStatus value when
* complete.
*
* @sa PasswordStatus, passwordStatus
*/
Q_INVOKABLE void changePassword(Quotient::Connection *connection, const QString &currentPassword, const QString &newPassword);
/**
* @brief Change the avatar for an account.
*/
Q_INVOKABLE bool setAvatar(Quotient::Connection *connection, const QUrl &avatarSource);
/**
* @brief Create new room for a group chat.
*/
@@ -210,14 +182,12 @@ public:
*/
Q_INVOKABLE void forceRefreshTextDocument(QQuickTextDocument *textDocument, QQuickItem *item);
Q_INVOKABLE QVariantList getSupportedRoomVersions(Quotient::Connection *connection);
Quotient::AccountRegistry &accounts();
private:
explicit Controller(QObject *parent = nullptr);
QPointer<Quotient::Connection> m_connection;
QPointer<NeoChatConnection> m_connection;
TrayIcon *m_trayIcon = nullptr;
QKeychain::ReadPasswordJob *loadAccessTokenFromKeyChain(const Quotient::AccountSettings &account);
@@ -244,8 +214,8 @@ Q_SIGNALS:
/// Error occurred because of server or bug in NeoChat
void globalErrorOccured(QString error, QString detail);
void syncDone();
void connectionAdded(Quotient::Connection *_t1);
void connectionDropped(Quotient::Connection *_t1);
void connectionAdded(NeoChatConnection *connection);
void connectionDropped(NeoChatConnection *connection);
void accountCountChanged();
void initiated();
void notificationClicked(const QString &_t1, const QString &_t2);
@@ -256,18 +226,14 @@ Q_SIGNALS:
void userConsentRequired(QUrl url);
void testConnectionResult(const QString &connection, bool usable);
void isOnlineChanged(bool isOnline);
void keyVerificationRequest(int timeLeft, Quotient::Connection *connection, const QString &transactionId, const QString &deviceId);
void keyVerificationRequest(int timeLeft, NeoChatConnection *connection, const QString &transactionId, const QString &deviceId);
void keyVerificationStart();
void keyVerificationAccept(const QString &commitment);
void keyVerificationKey(const QString &sas);
void activeConnectionIndexChanged();
void roomAdded(NeoChatRoom *room);
void activeAccountLabelChanged();
public Q_SLOTS:
void logout(Quotient::Connection *conn, bool serverSideLogout);
void changeAvatar(Quotient::Connection *conn, const QUrl &localFile);
static void markAllMessagesAsRead(Quotient::Connection *conn);
void saveWindowGeometry();
};
@@ -283,3 +249,9 @@ class NeochatDeleteDeviceJob : public Quotient::BaseJob
public:
explicit NeochatDeleteDeviceJob(const QString &deviceId, const Quotient::Omittable<QJsonObject> &auth = Quotient::none);
};
class NeoChatDeactivateAccountJob : public Quotient::BaseJob
{
public:
explicit NeoChatDeactivateAccountJob(const Quotient::Omittable<QJsonObject> &auth = Quotient::none);
};

View File

@@ -124,7 +124,7 @@ int DelegateSizeHelper::calculateCurrentPercentageWidth() const
int maxPercentWidth = endPercentBigger ? m_endPercentWidth : m_startPercentWidth;
int minPercentWidth = endPercentBigger ? m_startPercentWidth : m_endPercentWidth;
int calcPercentWidth = std::ceil(m * m_parentWidth + c);
int calcPercentWidth = std::round(m * m_parentWidth + c);
return std::clamp(calcPercentWidth, minPercentWidth, maxPercentWidth);
}
@@ -146,9 +146,9 @@ qreal DelegateSizeHelper::currentWidth() const
qreal absoluteWidth = m_parentWidth * percentWidth * 0.01;
if (m_maxWidth < 0.0) {
return std::ceil(absoluteWidth);
return std::round(absoluteWidth);
} else {
return std::ceil(std::min(absoluteWidth, m_maxWidth));
return std::round(std::min(absoluteWidth, m_maxWidth));
}
}

View File

@@ -8,6 +8,7 @@
#include <Quotient/connection.h>
#include <Quotient/csapi/content-repo.h>
#include "neochatconfig.h"
#include "neochatroom.h"
using namespace Quotient;
@@ -19,6 +20,10 @@ LinkPreviewer::LinkPreviewer(QObject *parent, NeoChatRoom *room, const QUrl &url
, m_url(url)
{
loadUrlPreview();
if (m_currentRoom) {
connect(m_currentRoom, &NeoChatRoom::urlPreviewEnabledChanged, this, &LinkPreviewer::loadUrlPreview);
}
connect(NeoChatConfig::self(), &NeoChatConfig::ShowLinkPreviewChanged, this, &LinkPreviewer::loadUrlPreview);
}
bool LinkPreviewer::loaded() const
@@ -57,6 +62,9 @@ void LinkPreviewer::setUrl(QUrl url)
void LinkPreviewer::loadUrlPreview()
{
if (!m_currentRoom || !NeoChatConfig::showLinkPreview() || !m_currentRoom->urlPreviewEnabled()) {
return;
}
if (m_url.scheme() == QStringLiteral("https")) {
m_loaded = false;
Q_EMIT loadedChanged();

View File

@@ -22,7 +22,7 @@ Login::Login(QObject *parent)
void Login::init()
{
m_homeserverReachable = false;
m_connection = new Connection();
m_connection = new NeoChatConnection();
m_matrixId = QString();
m_password = QString();
m_deviceName = QStringLiteral("NeoChat %1 %2 %3 %4")
@@ -51,7 +51,7 @@ void Login::init()
m_testing = true;
Q_EMIT testingChanged();
if (!m_connection) {
m_connection = new Connection();
m_connection = new NeoChatConnection();
}
m_connection->resolveServer(m_matrixId);
connectSingleShot(m_connection, &Connection::loginFlowsChanged, this, [this]() {
@@ -87,7 +87,11 @@ void Login::init()
Q_EMIT isLoggingInChanged();
});
connect(m_connection, &Connection::loginError, this, [this](QString error, const QString &) {
Q_EMIT errorOccured(i18n("Login Failed: %1", error));
if (error == QStringLiteral("Invalid username or password")) {
setInvalidPassword(true);
} else {
Q_EMIT errorOccured(i18n("Login Failed: %1", error));
}
m_isLoggingIn = false;
Q_EMIT isLoggingInChanged();
});
@@ -133,6 +137,7 @@ QString Login::password() const
void Login::setPassword(const QString &password)
{
setInvalidPassword(false);
m_password = password;
Q_EMIT passwordChanged();
}
@@ -199,4 +204,15 @@ bool Login::isLoggedIn() const
return m_isLoggedIn;
}
void Login::setInvalidPassword(bool invalid)
{
m_invalidPassword = invalid;
Q_EMIT isInvalidPasswordChanged();
}
bool Login::isInvalidPassword() const
{
return m_invalidPassword;
}
#include "moc_login.cpp"

View File

@@ -6,10 +6,7 @@
#include <QObject>
#include <QUrl>
namespace Quotient
{
class Connection;
}
class NeoChatConnection;
/**
* @class Login
@@ -73,6 +70,11 @@ class Login : public QObject
*/
Q_PROPERTY(bool isLoggedIn READ isLoggedIn NOTIFY isLoggedInChanged)
/**
* @brief Whether the password (or the username) is invalid.
*/
Q_PROPERTY(bool isInvalidPassword READ isInvalidPassword NOTIFY isInvalidPasswordChanged)
public:
explicit Login(QObject *parent = nullptr);
@@ -100,6 +102,9 @@ public:
bool isLoggedIn() const;
bool isInvalidPassword() const;
void setInvalidPassword(bool invalid);
Q_INVOKABLE void login();
Q_INVOKABLE void loginWithSso();
@@ -116,6 +121,7 @@ Q_SIGNALS:
void testingChanged();
void isLoggingInChanged();
void isLoggedInChanged();
void isInvalidPasswordChanged();
private:
void setHomeserverReachable(bool reachable);
@@ -126,9 +132,10 @@ private:
QString m_deviceName;
bool m_supportsSso = false;
bool m_supportsPassword = false;
Quotient::Connection *m_connection = nullptr;
NeoChatConnection *m_connection = nullptr;
QUrl m_ssoUrl;
bool m_testing = false;
bool m_isLoggingIn = false;
bool m_isLoggedIn = false;
bool m_invalidPassword = false;
};

View File

@@ -18,6 +18,10 @@
#include <QApplication>
#endif
#ifdef HAVE_WEBVIEW
#include <QtWebView>
#endif
#include <KAboutData>
#ifdef HAVE_KDBUSADDONS
#include <KDBusService>
@@ -50,7 +54,6 @@
#include "login.h"
#include "matriximageprovider.h"
#include "models/accountemoticonmodel.h"
#include "models/collapsestateproxymodel.h"
#include "models/customemojimodel.h"
#include "models/devicesmodel.h"
#include "models/devicesproxymodel.h"
@@ -77,6 +80,7 @@
#include "models/userlistmodel.h"
#include "models/webshortcutmodel.h"
#include "neochatconfig.h"
#include "neochatconnection.h"
#include "neochatroom.h"
#include "notificationsmanager.h"
#include "pollhandler.h"
@@ -95,6 +99,7 @@
#include "runner.h"
#include <QDBusConnection>
#endif
#include "registration.h"
#ifdef Q_OS_WINDOWS
#include <Windows.h>
@@ -139,6 +144,10 @@ int main(int argc, char *argv[])
QNetworkProxyFactory::setUseSystemConfiguration(true);
#ifdef HAVE_WEBVIEW
QtWebView::initialize();
#endif
#ifdef Q_OS_ANDROID
QGuiApplication app(argc, argv);
QQuickStyle::setStyle(QStringLiteral("org.kde.breeze"));
@@ -233,6 +242,7 @@ int main(int argc, char *argv[])
qmlRegisterSingletonInstance("org.kde.neochat", 1, 0, "AccountRegistry", &Controller::instance().accounts());
qmlRegisterSingletonInstance("org.kde.neochat", 1, 0, "SpaceHierarchyCache", &SpaceHierarchyCache::instance());
qmlRegisterSingletonInstance("org.kde.neochat", 1, 0, "CustomEmojiModel", &CustomEmojiModel::instance());
qmlRegisterSingletonInstance("org.kde.neochat", 1, 0, "Registration", &Registration::instance());
qmlRegisterType<ActionsHandler>("org.kde.neochat", 1, 0, "ActionsHandler");
qmlRegisterType<ChatDocumentHandler>("org.kde.neochat", 1, 0, "ChatDocumentHandler");
qmlRegisterType<RoomListModel>("org.kde.neochat", 1, 0, "RoomListModel");
@@ -240,7 +250,6 @@ int main(int argc, char *argv[])
qmlRegisterType<UserListModel>("org.kde.neochat", 1, 0, "UserListModel");
qmlRegisterType<MessageEventModel>("org.kde.neochat", 1, 0, "MessageEventModel");
qmlRegisterType<ReactionModel>("org.kde.neochat", 1, 0, "ReactionModel");
qmlRegisterType<CollapseStateProxyModel>("org.kde.neochat", 1, 0, "CollapseStateProxyModel");
qmlRegisterType<MediaMessageFilterModel>("org.kde.neochat", 1, 0, "MediaMessageFilterModel");
qmlRegisterType<MessageFilterModel>("org.kde.neochat", 1, 0, "MessageFilterModel");
qmlRegisterType<UserFilterModel>("org.kde.neochat", 1, 0, "UserFilterModel");
@@ -273,12 +282,12 @@ int main(int argc, char *argv[])
qmlRegisterUncreatableType<NeoChatRoomType>("org.kde.neochat", 1, 0, "NeoChatRoomType", "ENUM"_ls);
qmlRegisterUncreatableType<User>("org.kde.neochat", 1, 0, "User", {});
qmlRegisterUncreatableType<NeoChatRoom>("org.kde.neochat", 1, 0, "NeoChatRoom", {});
qmlRegisterUncreatableType<NeoChatConnection>("org.kde.neochat", 1, 0, "NeoChatConnection", {});
qRegisterMetaType<User *>("User*");
qRegisterMetaType<User *>("const User*");
qRegisterMetaType<User *>("const Quotient::User*");
qRegisterMetaType<Room *>("Room*");
qRegisterMetaType<Connection *>("Connection*");
qRegisterMetaType<MessageEventType>("MessageEventType");
qRegisterMetaType<NeoChatRoom *>("NeoChatRoom*");
qRegisterMetaType<User *>("User*");

View File

@@ -1,177 +0,0 @@
// SPDX-FileCopyrightText: 2022 Tobias Fella <tobias.fella@kde.org>
// SPDX-License-Identifier: LGPL-2.0-or-later
#include "collapsestateproxymodel.h"
#include <KLocalizedString>
bool CollapseStateProxyModel::filterAcceptsRow(int source_row, const QModelIndex &source_parent) const
{
Q_UNUSED(source_parent);
return sourceModel()->data(sourceModel()->index(source_row, 0), MessageEventModel::DelegateTypeRole)
!= MessageEventModel::DelegateType::State // If this is not a state, show it
|| (source_row < sourceModel()->rowCount() - 1
&& sourceModel()->data(sourceModel()->index(source_row + 1, 0), MessageEventModel::DelegateTypeRole)
!= MessageEventModel::DelegateType::State) // If this is the first state in a block, show it. TODO hidden events?
|| sourceModel()->data(sourceModel()->index(source_row, 0), MessageEventModel::ShowSectionRole).toBool(); // If it's a new day, show it
}
QVariant CollapseStateProxyModel::data(const QModelIndex &index, int role) const
{
if (role == AggregateDisplayRole) {
return aggregateEventToString(mapToSource(index).row());
} else if (role == StateEventsRole) {
return stateEventsList(mapToSource(index).row());
} else if (role == AuthorListRole) {
return authorList(mapToSource(index).row());
} else if (role == ExcessAuthorsRole) {
return excessAuthors(mapToSource(index).row());
}
return sourceModel()->data(mapToSource(index), role);
}
QHash<int, QByteArray> CollapseStateProxyModel::roleNames() const
{
auto roles = sourceModel()->roleNames();
roles[AggregateDisplayRole] = "aggregateDisplay";
roles[StateEventsRole] = "stateEvents";
roles[AuthorListRole] = "authorList";
roles[ExcessAuthorsRole] = "excessAuthors";
return roles;
}
QString CollapseStateProxyModel::aggregateEventToString(int sourceRow) const
{
QStringList parts;
QVariantList uniqueAuthors;
for (int i = sourceRow; i >= 0; i--) {
parts += sourceModel()->data(sourceModel()->index(i, 0), MessageEventModel::GenericDisplayRole).toString();
QVariant nextAuthor = sourceModel()->data(sourceModel()->index(i, 0), MessageEventModel::AuthorRole);
if (!uniqueAuthors.contains(nextAuthor)) {
uniqueAuthors.append(nextAuthor);
}
if (i > 0
&& (sourceModel()->data(sourceModel()->index(i - 1, 0), MessageEventModel::DelegateTypeRole)
!= MessageEventModel::DelegateType::State // If it's not a state event
|| sourceModel()->data(sourceModel()->index(i - 1, 0), MessageEventModel::ShowSectionRole).toBool() // or the section needs to be visible
)) {
break;
}
}
parts.sort(); // Sort them so that all identical events can be collected.
if (!parts.isEmpty()) {
QStringList chunks;
while (!parts.isEmpty()) {
chunks += QString();
int count = 1;
auto part = parts.takeFirst();
chunks.last() += part;
while (!parts.isEmpty() && parts.first() == part) {
parts.removeFirst();
count++;
}
if (count > 1 && uniqueAuthors.length() == 1) {
chunks.last() += i18ncp("n times", " %1 time ", " %1 times ", count);
}
}
chunks.removeDuplicates();
QString text = QStringLiteral("<style>a {text-decoration: none;}</style>"); // There can be links in the event text so make sure all are styled.
// The author text is either "n users" if > 1 user or the matrix.to link to a single user.
QString userText = uniqueAuthors.length() > 1 ? i18ncp("n users", " %1 user ", " %1 users ", uniqueAuthors.length())
: QStringLiteral("<a href=\"https://matrix.to/#/%1\" style=\"color: %2\">%3</a> ")
.arg(uniqueAuthors[0].toMap()[QStringLiteral("id")].toString(),
uniqueAuthors[0].toMap()[QStringLiteral("color")].toString(),
uniqueAuthors[0].toMap()[QStringLiteral("displayName")].toString().toHtmlEscaped());
text += userText;
text += chunks.takeFirst();
if (chunks.size() > 0) {
while (chunks.size() > 1) {
text += i18nc("[action 1], [action 2 and/or action 3]", ", ");
text += chunks.takeFirst();
}
text += uniqueAuthors.length() > 1 ? i18nc("[action 1, action 2] or [action 3]", " or ") : i18nc("[action 1, action 2] and [action 3]", " and ");
text += chunks.takeFirst();
}
return text;
} else {
return {};
}
}
QVariantList CollapseStateProxyModel::stateEventsList(int sourceRow) const
{
QVariantList stateEvents;
for (int i = sourceRow; i >= 0; i--) {
auto nextState = QVariantMap{
{QStringLiteral("author"), sourceModel()->data(sourceModel()->index(i, 0), MessageEventModel::AuthorRole)},
{QStringLiteral("authorDisplayName"), sourceModel()->data(sourceModel()->index(i, 0), MessageEventModel::AuthorDisplayNameRole).toString()},
{QStringLiteral("text"), sourceModel()->data(sourceModel()->index(i, 0), Qt::DisplayRole).toString()},
};
stateEvents.append(nextState);
if (i > 0
&& (sourceModel()->data(sourceModel()->index(i - 1, 0), MessageEventModel::DelegateTypeRole)
!= MessageEventModel::DelegateType::State // If it's not a state event
|| sourceModel()->data(sourceModel()->index(i - 1, 0), MessageEventModel::ShowSectionRole).toBool() // or the section needs to be visible
)) {
break;
}
}
return stateEvents;
}
QVariantList CollapseStateProxyModel::authorList(int sourceRow) const
{
QVariantList uniqueAuthors;
for (int i = sourceRow; i >= 0; i--) {
QVariant nextAvatar = sourceModel()->data(sourceModel()->index(i, 0), MessageEventModel::AuthorRole);
if (!uniqueAuthors.contains(nextAvatar)) {
uniqueAuthors.append(nextAvatar);
}
if (i > 0
&& (sourceModel()->data(sourceModel()->index(i - 1, 0), MessageEventModel::DelegateTypeRole)
!= MessageEventModel::DelegateType::State // If it's not a state event
|| sourceModel()->data(sourceModel()->index(i - 1, 0), MessageEventModel::ShowSectionRole).toBool() // or the section needs to be visible
)) {
break;
}
}
if (uniqueAuthors.count() > 5) {
uniqueAuthors = uniqueAuthors.mid(0, 5);
}
return uniqueAuthors;
}
QString CollapseStateProxyModel::excessAuthors(int row) const
{
QVariantList uniqueAuthors;
for (int i = row; i >= 0; i--) {
QVariant nextAvatar = sourceModel()->data(sourceModel()->index(i, 0), MessageEventModel::AuthorRole);
if (!uniqueAuthors.contains(nextAvatar)) {
uniqueAuthors.append(nextAvatar);
}
if (i > 0
&& (sourceModel()->data(sourceModel()->index(i - 1, 0), MessageEventModel::DelegateTypeRole)
!= MessageEventModel::DelegateType::State // If it's not a state event
|| sourceModel()->data(sourceModel()->index(i - 1, 0), MessageEventModel::ShowSectionRole).toBool() // or the section needs to be visible
)) {
break;
}
}
int excessAuthors;
if (uniqueAuthors.count() > 5) {
excessAuthors = uniqueAuthors.count() - 5;
} else {
excessAuthors = 0;
}
QString excessAuthorsString;
if (excessAuthors == 0) {
return QString();
} else {
return QStringLiteral("+ %1").arg(excessAuthors);
}
}
#include "moc_collapsestateproxymodel.cpp"

View File

@@ -1,80 +0,0 @@
// SPDX-FileCopyrightText: 2022 Tobias Fella <tobias.fella@kde.org>
// SPDX-License-Identifier: LGPL-2.0-or-later
#pragma once
#include "messageeventmodel.h"
#include <QSortFilterProxyModel>
/**
* @class CollapseStateProxyModel
*
* This model aggregates multiple sequential state events into a single entry.
*
* Events are only aggregated if they happened on the same day.
*
* @sa MessageEventModel
*/
class CollapseStateProxyModel : public QSortFilterProxyModel
{
Q_OBJECT
public:
/**
* @brief Defines the model roles.
*/
enum Roles {
AggregateDisplayRole = MessageEventModel::LastRole + 1, /**< Single line aggregation of all the state events. */
StateEventsRole, /**< List of state events in the aggregated state. */
AuthorListRole, /**< List of the first 5 unique authors of the aggregated state event. */
ExcessAuthorsRole, /**< The number of unique authors beyond the first 5. */
LastRole, // Keep this last
};
/**
* @brief Whether a row should be shown out or not.
*
* @sa QSortFilterProxyModel::filterAcceptsRow
*/
[[nodiscard]] bool filterAcceptsRow(int source_row, const QModelIndex &source_parent) const override;
/**
* @brief Get the given role value at the given index.
*
* @sa QSortFilterProxyModel::data
*/
[[nodiscard]] QVariant data(const QModelIndex &idx, int role = Qt::DisplayRole) const override;
/**
* @brief Returns a mapping from Role enum values to role names.
*
* @sa Roles, QAbstractProxyModel::roleNames()
*/
[[nodiscard]] QHash<int, QByteArray> roleNames() const override;
private:
/**
* @brief Aggregation of the text of consecutive state events starting at row.
*
* If state events happen on different days they will be split into two aggregate
* events.
*/
[[nodiscard]] QString aggregateEventToString(int row) const;
/**
* @brief Return a list of consecutive state events starting at row.
*
* If state events happen on different days they will be split into two aggregate
* events.
*/
[[nodiscard]] QVariantList stateEventsList(int row) const;
/**
* @brief List of the first 5 unique authors for the aggregate state events starting at row.
*/
[[nodiscard]] QVariantList authorList(int row) const;
/**
* @brief The number of unique authors beyond the first 5 for the aggregate state events starting at row.
*/
[[nodiscard]] QString excessAuthors(int row) const;
};

View File

@@ -145,7 +145,7 @@ void CompletionModel::updateCompletion()
m_filterModel->setFullText(m_fullText);
m_filterModel->setFilterText(m_text);
m_filterModel->invalidate();
} else if (text().startsWith(QLatin1Char(':')) && !text()[1].isUpper()
} else if (text().startsWith(QLatin1Char(':')) && text().size() > 1 && !text()[1].isUpper()
&& (m_fullText.indexOf(QLatin1Char(':'), 1) == -1
|| (m_fullText.indexOf(QLatin1Char(' ')) != -1 && m_fullText.indexOf(QLatin1Char(':'), 1) > m_fullText.indexOf(QLatin1Char(' '), 1)))) {
m_filterModel->setSourceModel(m_emojiModel);

View File

@@ -5,7 +5,7 @@
#include <QSortFilterProxyModel>
#include "models/collapsestateproxymodel.h"
#include "models/messagefiltermodel.h"
/**
* @class MediaMessageFilterModel
@@ -22,7 +22,7 @@ public:
* @brief Defines the model roles.
*/
enum Roles {
SourceRole = CollapseStateProxyModel::LastRole + 1, /**< The mxc source URL for the item. */
SourceRole = MessageFilterModel::LastRole + 1, /**< The mxc source URL for the item. */
TempSourceRole, /**< Source for the temporary content (either blurhash or mxc URL). */
TypeRole, /**< The type of the media (image or video). */
CaptionRole, /**< The caption for the item. */

View File

@@ -3,6 +3,8 @@
#include "messagefiltermodel.h"
#include <KLocalizedString>
#include "messageeventmodel.h"
#include "neochatconfig.h"
@@ -32,22 +34,192 @@ bool MessageFilterModel::filterAcceptsRow(int sourceRow, const QModelIndex &sour
{
const QModelIndex index = sourceModel()->index(sourceRow, 0, sourceParent);
// Don't show redacted (i.e. deleted) messages.
if (index.data(MessageEventModel::IsRedactedRole).toBool() && !NeoChatConfig::self()->showDeletedMessages()) {
return false;
}
// Don't show hidden or replaced messages.
const int specialMarks = index.data(MessageEventModel::SpecialMarksRole).toInt();
if (specialMarks == EventStatus::Hidden || specialMarks == EventStatus::Replaced) {
return false;
}
// Don't show events with an unknown type.
const auto eventType = index.data(MessageEventModel::DelegateTypeRole).toInt();
if (eventType == MessageEventModel::Other) {
return false;
}
// Don't show state events that are not the first in a consecutive group on the
// same day as they will be grouped as a single delegate.
const bool notLastRow = sourceRow < sourceModel()->rowCount() - 1;
const bool previousEventIsState =
sourceModel()->data(sourceModel()->index(sourceRow + 1, 0), MessageEventModel::DelegateTypeRole) == MessageEventModel::DelegateType::State;
const bool newDay = sourceModel()->data(sourceModel()->index(sourceRow, 0), MessageEventModel::ShowSectionRole).toBool();
if (eventType == MessageEventModel::State && notLastRow && previousEventIsState && !newDay) {
return false;
}
return true;
}
QVariant MessageFilterModel::data(const QModelIndex &index, int role) const
{
if (role == AggregateDisplayRole) {
return aggregateEventToString(mapToSource(index).row());
} else if (role == StateEventsRole) {
return stateEventsList(mapToSource(index).row());
} else if (role == AuthorListRole) {
return authorList(mapToSource(index).row());
} else if (role == ExcessAuthorsRole) {
return excessAuthors(mapToSource(index).row());
}
return sourceModel()->data(mapToSource(index), role);
}
QHash<int, QByteArray> MessageFilterModel::roleNames() const
{
auto roles = sourceModel()->roleNames();
roles[AggregateDisplayRole] = "aggregateDisplay";
roles[StateEventsRole] = "stateEvents";
roles[AuthorListRole] = "authorList";
roles[ExcessAuthorsRole] = "excessAuthors";
return roles;
}
QString MessageFilterModel::aggregateEventToString(int sourceRow) const
{
QStringList parts;
QVariantList uniqueAuthors;
for (int i = sourceRow; i >= 0; i--) {
parts += sourceModel()->data(sourceModel()->index(i, 0), MessageEventModel::GenericDisplayRole).toString();
QVariant nextAuthor = sourceModel()->data(sourceModel()->index(i, 0), MessageEventModel::AuthorRole);
if (!uniqueAuthors.contains(nextAuthor)) {
uniqueAuthors.append(nextAuthor);
}
if (i > 0
&& (sourceModel()->data(sourceModel()->index(i - 1, 0), MessageEventModel::DelegateTypeRole)
!= MessageEventModel::DelegateType::State // If it's not a state event
|| sourceModel()->data(sourceModel()->index(i - 1, 0), MessageEventModel::ShowSectionRole).toBool() // or the section needs to be visible
)) {
break;
}
}
parts.sort(); // Sort them so that all identical events can be collected.
if (!parts.isEmpty()) {
QStringList chunks;
while (!parts.isEmpty()) {
chunks += QString();
int count = 1;
auto part = parts.takeFirst();
chunks.last() += part;
while (!parts.isEmpty() && parts.first() == part) {
parts.removeFirst();
count++;
}
if (count > 1 && uniqueAuthors.length() == 1) {
chunks.last() += i18ncp("n times", " %1 time ", " %1 times ", count);
}
}
chunks.removeDuplicates();
QString text = QStringLiteral("<style>a {text-decoration: none;}</style>"); // There can be links in the event text so make sure all are styled.
// The author text is either "n users" if > 1 user or the matrix.to link to a single user.
QString userText = uniqueAuthors.length() > 1 ? i18ncp("n users", " %1 user ", " %1 users ", uniqueAuthors.length())
: QStringLiteral("<a href=\"https://matrix.to/#/%1\" style=\"color: %2\">%3</a> ")
.arg(uniqueAuthors[0].toMap()[QStringLiteral("id")].toString(),
uniqueAuthors[0].toMap()[QStringLiteral("color")].toString(),
uniqueAuthors[0].toMap()[QStringLiteral("displayName")].toString().toHtmlEscaped());
text += userText;
text += chunks.takeFirst();
if (chunks.size() > 0) {
while (chunks.size() > 1) {
text += i18nc("[action 1], [action 2 and/or action 3]", ", ");
text += chunks.takeFirst();
}
text += uniqueAuthors.length() > 1 ? i18nc("[action 1, action 2] or [action 3]", " or ") : i18nc("[action 1, action 2] and [action 3]", " and ");
text += chunks.takeFirst();
}
return text;
} else {
return {};
}
}
QVariantList MessageFilterModel::stateEventsList(int sourceRow) const
{
QVariantList stateEvents;
for (int i = sourceRow; i >= 0; i--) {
auto nextState = QVariantMap{
{QStringLiteral("author"), sourceModel()->data(sourceModel()->index(i, 0), MessageEventModel::AuthorRole)},
{QStringLiteral("authorDisplayName"), sourceModel()->data(sourceModel()->index(i, 0), MessageEventModel::AuthorDisplayNameRole).toString()},
{QStringLiteral("text"), sourceModel()->data(sourceModel()->index(i, 0), Qt::DisplayRole).toString()},
};
stateEvents.append(nextState);
if (i > 0
&& (sourceModel()->data(sourceModel()->index(i - 1, 0), MessageEventModel::DelegateTypeRole)
!= MessageEventModel::DelegateType::State // If it's not a state event
|| sourceModel()->data(sourceModel()->index(i - 1, 0), MessageEventModel::ShowSectionRole).toBool() // or the section needs to be visible
)) {
break;
}
}
return stateEvents;
}
QVariantList MessageFilterModel::authorList(int sourceRow) const
{
QVariantList uniqueAuthors;
for (int i = sourceRow; i >= 0; i--) {
QVariant nextAvatar = sourceModel()->data(sourceModel()->index(i, 0), MessageEventModel::AuthorRole);
if (!uniqueAuthors.contains(nextAvatar)) {
uniqueAuthors.append(nextAvatar);
}
if (i > 0
&& (sourceModel()->data(sourceModel()->index(i - 1, 0), MessageEventModel::DelegateTypeRole)
!= MessageEventModel::DelegateType::State // If it's not a state event
|| sourceModel()->data(sourceModel()->index(i - 1, 0), MessageEventModel::ShowSectionRole).toBool() // or the section needs to be visible
)) {
break;
}
}
if (uniqueAuthors.count() > 5) {
uniqueAuthors = uniqueAuthors.mid(0, 5);
}
return uniqueAuthors;
}
QString MessageFilterModel::excessAuthors(int row) const
{
QVariantList uniqueAuthors;
for (int i = row; i >= 0; i--) {
QVariant nextAvatar = sourceModel()->data(sourceModel()->index(i, 0), MessageEventModel::AuthorRole);
if (!uniqueAuthors.contains(nextAvatar)) {
uniqueAuthors.append(nextAvatar);
}
if (i > 0
&& (sourceModel()->data(sourceModel()->index(i - 1, 0), MessageEventModel::DelegateTypeRole)
!= MessageEventModel::DelegateType::State // If it's not a state event
|| sourceModel()->data(sourceModel()->index(i - 1, 0), MessageEventModel::ShowSectionRole).toBool() // or the section needs to be visible
)) {
break;
}
}
int excessAuthors;
if (uniqueAuthors.count() > 5) {
excessAuthors = uniqueAuthors.count() - 5;
} else {
excessAuthors = 0;
}
QString excessAuthorsString;
if (excessAuthors == 0) {
return QString();
} else {
return QStringLiteral("+ %1").arg(excessAuthors);
}
}
#include "moc_messagefiltermodel.cpp"

View File

@@ -5,21 +5,79 @@
#include <QSortFilterProxyModel>
#include "messageeventmodel.h"
/**
* @class MessageFilterModel
*
* This model filters out any messages that should be hidden.
*
* Deleted messages are only hidden if the user hasn't set them to be shown.
*
* The model also contains the roles and functions to support aggregating multiple
* consecutive state events into a single delegate. The state events must all happen
* on the same day to be aggregated.
*/
class MessageFilterModel : public QSortFilterProxyModel
{
Q_OBJECT
public:
/**
* @brief Defines the model roles.
*/
enum Roles {
AggregateDisplayRole = MessageEventModel::LastRole + 1, /**< Single line aggregation of all the state events. */
StateEventsRole, /**< List of state events in the aggregated state. */
AuthorListRole, /**< List of the first 5 unique authors of the aggregated state event. */
ExcessAuthorsRole, /**< The number of unique authors beyond the first 5. */
LastRole, // Keep this last
};
explicit MessageFilterModel(QObject *parent = nullptr);
/**
* @brief Custom filter function to remove hidden messages.
*/
bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const override;
/**
* @brief Get the given role value at the given index.
*
* @sa QSortFilterProxyModel::data
*/
[[nodiscard]] QVariant data(const QModelIndex &idx, int role = Qt::DisplayRole) const override;
/**
* @brief Returns a mapping from Role enum values to role names.
*
* @sa Roles, QAbstractProxyModel::roleNames()
*/
[[nodiscard]] QHash<int, QByteArray> roleNames() const override;
private:
/**
* @brief Aggregation of the text of consecutive state events starting at row.
*
* If state events happen on different days they will be split into two aggregate
* events.
*/
[[nodiscard]] QString aggregateEventToString(int row) const;
/**
* @brief Return a list of consecutive state events starting at row.
*
* If state events happen on different days they will be split into two aggregate
* events.
*/
[[nodiscard]] QVariantList stateEventsList(int row) const;
/**
* @brief List of the first 5 unique authors for the aggregate state events starting at row.
*/
[[nodiscard]] QVariantList authorList(int row) const;
/**
* @brief The number of unique authors beyond the first 5 for the aggregate state events starting at row.
*/
[[nodiscard]] QString excessAuthors(int row) const;
};

View File

@@ -31,7 +31,7 @@ QVariant ReactionModel::data(const QModelIndex &index, int role) const
if (role == TextRole) {
if (reaction.authors.count() > 1) {
return QStringLiteral("%1 %2").arg(reaction.reaction, reaction.authors.count());
return QStringLiteral("%1 %2").arg(reaction.reaction, QString::number(reaction.authors.count()));
} else {
return reaction.reaction;
}

155
src/neochatconnection.cpp Normal file
View File

@@ -0,0 +1,155 @@
// SPDX-FileCopyrightText: 2023 Tobias Fella <tobias.fella@kde.org>
// SPDX-License-Identifier: GPL-2.0-or-later
#include "neochatconnection.h"
#include <QImageReader>
#include "controller.h"
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
#include <qt5keychain/keychain.h>
#else
#include <qt6keychain/keychain.h>
#endif
#include <Quotient/csapi/content-repo.h>
#include <Quotient/csapi/profile.h>
#include <Quotient/settings.h>
#include <Quotient/user.h>
using namespace Quotient;
NeoChatConnection::NeoChatConnection(QObject *parent)
: Connection(parent)
{
connect(this, &NeoChatConnection::accountDataChanged, this, [this](const QString &type) {
if (type == QLatin1String("org.kde.neochat.account_label")) {
Q_EMIT labelChanged();
}
});
}
NeoChatConnection::NeoChatConnection(const QUrl &server, QObject *parent)
: Connection(server, parent)
{
connect(this, &NeoChatConnection::accountDataChanged, this, [this](const QString &type) {
if (type == QLatin1String("org.kde.neochat.account_label")) {
Q_EMIT labelChanged();
}
});
}
void NeoChatConnection::logout(bool serverSideLogout)
{
SettingsGroup(QStringLiteral("Accounts")).remove(userId());
QKeychain::DeletePasswordJob job(qAppName());
job.setAutoDelete(true);
job.setKey(userId());
QEventLoop loop;
QKeychain::DeletePasswordJob::connect(&job, &QKeychain::Job::finished, &loop, &QEventLoop::quit);
job.start();
loop.exec();
if (Controller::instance().accounts().count() > 1) {
// Only set the connection if the the account being logged out is currently active
if (this == Controller::instance().activeConnection()) {
Controller::instance().setActiveConnection(dynamic_cast<NeoChatConnection *>(Controller::instance().accounts().accounts()[0]));
}
} else {
Controller::instance().setActiveConnection(nullptr);
}
if (!serverSideLogout) {
return;
}
Connection::logout();
}
bool NeoChatConnection::setAvatar(const QUrl &avatarSource)
{
QString decoded = avatarSource.path();
if (decoded.isEmpty()) {
callApi<SetAvatarUrlJob>(user()->id(), avatarSource);
return true;
}
if (QImageReader(decoded).read().isNull()) {
return false;
} else {
return user()->setAvatar(decoded);
}
}
QVariantList NeoChatConnection::getSupportedRoomVersions() const
{
const auto &roomVersions = availableRoomVersions();
QVariantList supportedRoomVersions;
for (const auto &v : roomVersions) {
QVariantMap roomVersionMap;
roomVersionMap.insert("id"_ls, v.id);
roomVersionMap.insert("status"_ls, v.status);
roomVersionMap.insert("isStable"_ls, v.isStable());
supportedRoomVersions.append(roomVersionMap);
}
return supportedRoomVersions;
}
void NeoChatConnection::changePassword(const QString &currentPassword, const QString &newPassword)
{
auto job = callApi<NeochatChangePasswordJob>(newPassword, false);
connect(job, &BaseJob::result, this, [this, job, currentPassword, newPassword] {
if (job->error() == 103) {
QJsonObject replyData = job->jsonData();
QJsonObject authData;
authData["session"_ls] = replyData["session"_ls];
authData["password"_ls] = currentPassword;
authData["type"_ls] = "m.login.password"_ls;
authData["user"_ls] = user()->id();
QJsonObject identifier = {{"type"_ls, "m.id.user"_ls}, {"user"_ls, user()->id()}};
authData["identifier"_ls] = identifier;
NeochatChangePasswordJob *innerJob = callApi<NeochatChangePasswordJob>(newPassword, false, authData);
connect(innerJob, &BaseJob::success, this, []() {
Q_EMIT Controller::instance().passwordStatus(Controller::PasswordStatus::Success);
});
connect(innerJob, &BaseJob::failure, this, [innerJob]() {
Q_EMIT Controller::instance().passwordStatus(innerJob->jsonData()["errcode"_ls] == "M_FORBIDDEN"_ls ? Controller::PasswordStatus::Wrong
: Controller::PasswordStatus::Other);
});
}
});
}
void NeoChatConnection::setLabel(const QString &label)
{
QJsonObject json{
{"account_label"_ls, label},
};
setAccountData("org.kde.neochat.account_label"_ls, json);
Q_EMIT labelChanged();
}
QString NeoChatConnection::label() const
{
return accountDataJson("org.kde.neochat.account_label"_ls)["account_label"_ls].toString();
}
void NeoChatConnection::deactivateAccount(const QString &password)
{
auto job = callApi<NeoChatDeactivateAccountJob>();
connect(job, &BaseJob::result, this, [this, job, password] {
if (job->error() == 103) {
QJsonObject replyData = job->jsonData();
QJsonObject authData;
authData["session"_ls] = replyData["session"_ls];
authData["password"_ls] = password;
authData["type"_ls] = "m.login.password"_ls;
authData["user"_ls] = user()->id();
QJsonObject identifier = {{"type"_ls, "m.id.user"_ls}, {"user"_ls, user()->id()}};
authData["identifier"_ls] = identifier;
auto innerJob = callApi<NeoChatDeactivateAccountJob>(authData);
connect(innerJob, &BaseJob::success, this, [this]() {
logout(false);
});
}
});
}

53
src/neochatconnection.h Normal file
View File

@@ -0,0 +1,53 @@
// SPDX-FileCopyrightText: 2023 Tobias Fella <tobias.fella@kde.org>
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <QObject>
#include <Quotient/connection.h>
class NeoChatConnection : public Quotient::Connection
{
Q_OBJECT
/**
* @brief The account label for this account.
*
* Account labels are a concept specific to NeoChat, allowing accounts to be
* labelled, e.g. for "Work", "Private", etc.
*
* Set to an empty string to remove the label.
*/
Q_PROPERTY(QString label READ label WRITE setLabel NOTIFY labelChanged)
public:
NeoChatConnection(QObject *parent = nullptr);
NeoChatConnection(const QUrl &server, QObject *parent = nullptr);
Q_INVOKABLE void logout(bool serverSideLogout);
Q_INVOKABLE QVariantList getSupportedRoomVersions() const;
/**
* @brief Change the password for an account.
*
* The function emits a passwordStatus signal with a PasswordStatus value when
* complete.
*
* @sa PasswordStatus, passwordStatus
*/
Q_INVOKABLE void changePassword(const QString &currentPassword, const QString &newPassword);
/**
* @brief Change the avatar for an account.
*/
Q_INVOKABLE bool setAvatar(const QUrl &avatarSource);
[[nodiscard]] QString label() const;
void setLabel(const QString &label);
Q_INVOKABLE void deactivateAccount(const QString &password);
Q_SIGNALS:
void labelChanged();
};

View File

@@ -111,7 +111,7 @@ NeoChatRoom::NeoChatRoom(Connection *connection, QString roomId, JoinState joinS
qWarning() << "using this room's avatar";
avatar_image = avatar(128);
}
NotificationsManager::instance().postInviteNotification(this, htmlSafeDisplayName(), htmlSafeMemberName(senderId), avatar_image);
NotificationsManager::instance().postInviteNotification(this, displayNameForHtml(), htmlSafeMemberName(senderId), avatar_image);
});
connect(this, &Room::changed, this, [this] {
Q_EMIT canEncryptRoomChanged();
@@ -978,11 +978,6 @@ bool NeoChatRoom::isUserBanned(const QString &user) const
return roomMemberEvent->membership() == Membership::Ban;
}
QString NeoChatRoom::htmlSafeDisplayName() const
{
return displayName().toHtmlEscaped();
}
void NeoChatRoom::deleteMessagesByUser(const QString &user, const QString &reason)
{
doDeleteMessagesByUser(user, reason);

View File

@@ -111,11 +111,6 @@ class NeoChatRoom : public Quotient::Room
*/
Q_PROPERTY(bool readMarkerLoaded READ readMarkerLoaded NOTIFY readMarkerLoadedChanged)
/**
* @brief Display name with any html special characters escaped.
*/
Q_PROPERTY(QString htmlSafeDisplayName READ htmlSafeDisplayName NOTIFY displayNameChanged)
/**
* @brief The avatar image to be used for the room.
*/
@@ -591,8 +586,6 @@ public:
[[nodiscard]] bool readMarkerLoaded() const;
QString htmlSafeDisplayName() const;
/**
* @brief Get subtitle text for room
*

View File

@@ -13,12 +13,12 @@
#include <QPainter>
#include <Quotient/accountregistry.h>
#include <Quotient/connection.h>
#include <Quotient/csapi/pushrules.h>
#include <Quotient/user.h>
#include "controller.h"
#include "neochatconfig.h"
#include "neochatconnection.h"
#include "neochatroom.h"
#include "roommanager.h"
#include "texthandler.h"
@@ -37,7 +37,7 @@ NotificationsManager::NotificationsManager(QObject *parent)
{
}
void NotificationsManager::handleNotifications(QPointer<Connection> connection)
void NotificationsManager::handleNotifications(QPointer<NeoChatConnection> connection)
{
if (!m_connActiveJob.contains(connection->user()->id())) {
auto job = connection->callApi<GetNotificationsJob>();
@@ -49,7 +49,7 @@ void NotificationsManager::handleNotifications(QPointer<Connection> connection)
}
}
void NotificationsManager::processNotificationJob(QPointer<Quotient::Connection> connection, Quotient::GetNotificationsJob *job, bool initialization)
void NotificationsManager::processNotificationJob(QPointer<NeoChatConnection> connection, Quotient::GetNotificationsJob *job, bool initialization)
{
if (job == nullptr) {
return;
@@ -145,7 +145,7 @@ void NotificationsManager::processNotificationJob(QPointer<Quotient::Connection>
}
}
bool NotificationsManager::shouldPostNotification(QPointer<Quotient::Connection> connection, const QJsonValue &notification)
bool NotificationsManager::shouldPostNotification(QPointer<NeoChatConnection> connection, const QJsonValue &notification)
{
if (connection == nullptr) {
return false;
@@ -211,7 +211,7 @@ void NotificationsManager::postNotification(NeoChatRoom *room,
return;
}
if (room->localUser()->id() != Controller::instance().activeConnection()->userId()) {
Controller::instance().setActiveConnection(Controller::instance().accounts().get(room->localUser()->id()));
Controller::instance().setActiveConnection(dynamic_cast<NeoChatConnection *>(Controller::instance().accounts().get(room->localUser()->id())));
}
RoomManager::instance().enterRoom(room);
});

View File

@@ -12,11 +12,7 @@
#include <Quotient/csapi/notifications.h>
#include <Quotient/jobs/basejob.h>
namespace Quotient
{
class Connection;
}
class NeoChatConnection;
class KNotification;
class NeoChatRoom;
@@ -80,7 +76,7 @@ public:
/**
* @brief Handle the notifications for the given connection.
*/
void handleNotifications(QPointer<Quotient::Connection> connection);
void handleNotifications(QPointer<NeoChatConnection> connection);
private:
explicit NotificationsManager(QObject *parent = nullptr);
@@ -90,13 +86,13 @@ private:
QStringList m_connActiveJob;
bool shouldPostNotification(QPointer<Quotient::Connection> connection, const QJsonValue &notification);
bool shouldPostNotification(QPointer<NeoChatConnection> connection, const QJsonValue &notification);
QHash<QString, KNotification *> m_notifications;
QHash<QString, QPointer<KNotification>> m_invitations;
private Q_SLOTS:
void processNotificationJob(QPointer<Quotient::Connection> connection, Quotient::GetNotificationsJob *job, bool initialization);
void processNotificationJob(QPointer<NeoChatConnection> connection, Quotient::GetNotificationsJob *job, bool initialization);
private:
QPixmap createNotificationImage(const QImage &icon, NeoChatRoom *room);

View File

@@ -0,0 +1,49 @@
// SPDX-FileCopyrightText: 2023 Tobias Fella <tobias.fella@kde.org>
// SPDX-License-Identifier: GPL-2.0-or-later
import QtQuick 2.14
import QtQuick.Controls 2.14 as Controls
import QtQuick.Layouts 1.14
import QtWebView 1.15
import org.kde.kirigami 2.12 as Kirigami
import org.kde.kirigamiaddons.formcard 1.0 as FormCard
import org.kde.neochat 1.0
LoginStep {
id: root
FormCard.AbstractFormDelegate {
background: null
contentItem: WebView {
id: webview
url: "http://localhost:20847"
implicitHeight: 500
onLoadingChanged: {
webview.runJavaScript("document.body.style.background = '" + Kirigami.Theme.backgroundColor + "'")
}
Timer {
id: timer
repeat: true
running: true
interval: 300
onTriggered: {
if(!webview.visible) {
return
}
webview.runJavaScript("!!grecaptcha ? grecaptcha.getResponse() : \"\"", function(response){
if(!webview.visible || !response)
return
timer.running = false;
Registration.recaptchaResponse = response;
})
}
}
}
}
previousAction: Kirigami.Action {
onTriggered: root.processed("qrc:/Username.qml")
}
}

View File

@@ -0,0 +1,59 @@
// SPDX-FileCopyrightText: 2023 Tobias Fella <tobias.fella@kde.org>
// SPDX-License-Identifier: GPL-2.0-or-later
import QtQuick 2.15
import QtQuick.Controls 2.12 as QQC2
import QtQuick.Layouts 1.12
import org.kde.kirigami 2.12 as Kirigami
import org.kde.kirigamiaddons.formcard 1.0 as FormCard
import org.kde.neochat 1.0
LoginStep {
id: root
onActiveFocusChanged: if (activeFocus) emailField.forceActiveFocus()
FormCard.FormTextFieldDelegate {
id: emailField
label: i18n("Add an e-mail address:")
placeholderText: "user@example.com"
onTextChanged: Registration.email = text
Keys.onReturnPressed: {
if (root.nextAction.enabled) {
root.nextAction.trigger()
}
}
}
FormCard.FormTextDelegate {
id: confirmMessage
text: i18n("Confirm e-mail address")
visible: false
description: i18n("A confirmation e-mail has been sent to your address. Please continue here <b>after</b> clicking on the confirmation link in the e-mail")
}
FormCard.FormButtonDelegate {
id: resendButton
text: i18nc("@button", "Re-send confirmation e-mail")
onClicked: Registration.registerEmail()
visible: false
}
nextAction: Kirigami.Action {
enabled: emailField.text.length > 0
onTriggered: {
if (confirmMessage.visible) {
Registration.registerAccount()
} else {
Registration.registerEmail()
confirmMessage.visible = true
resendButton.visible = true
}
}
}
previousAction: Kirigami.Action {
onTriggered: root.processed("qrc:/Username.qml")
}
}

View File

@@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: 2020 Tobias Fella <tobias.fella@kde.org>
// SPDX-FileCopyrightText: 2023 Tobias Fella <tobias.fella@kde.org>
// SPDX-License-Identifier: GPL-2.0-or-later
import QtQuick 2.15
@@ -6,56 +6,42 @@ import QtQuick.Controls 2.15 as QQC2
import QtQuick.Layouts 1.15
import org.kde.kirigami 2.15 as Kirigami
import org.kde.kirigamiaddons.formcard 1.0 as FormCard
import org.kde.neochat 1.0
LoginStep {
id: root
readonly property var homeserver: customHomeserver.visible ? customHomeserver.text : serverCombo.currentText
property bool loading: false
onActiveFocusChanged: if (activeFocus) urlField.forceActiveFocus()
title: i18nc("@title", "Select a Homeserver")
action: Kirigami.Action {
enabled: LoginHelper.homeserverReachable && !customHomeserver.visible || customHomeserver.acceptableInput
onTriggered: {
// TODO
console.log("register todo")
FormCard.FormTextFieldDelegate {
id: urlField
label: i18n("Server Url:")
validator: RegularExpressionValidator {
regularExpression: /([a-zA-Z0-9\-]+\.)*[a-zA-Z0-9]+(:[0-9]+)?/
}
onTextChanged: timer.restart()
statusMessage: Registration.status === Registration.ServerNoRegistration ? i18n("Registration is disabled on this server.") : ""
Keys.onReturnPressed: {
if (root.nextAction.enabled) {
root.nextAction.trigger()
}
}
}
onHomeserverChanged: {
LoginHelper.testHomeserver("@user:" + homeserver)
Timer {
id: timer
interval: 500
onTriggered: Registration.homeserver = urlField.text
}
Kirigami.FormLayout {
Component.onCompleted: Controller.testHomeserver(homeserver)
QQC2.ComboBox {
id: serverCombo
Kirigami.FormData.label: i18n("Homeserver:")
model: ["matrix.org", "kde.org", "tchncs.de", i18n("Other...")]
}
QQC2.TextField {
id: customHomeserver
Kirigami.FormData.label: i18n("Url:")
visible: serverCombo.currentIndex === 3
onTextChanged: {
Controller.testHomeserver(text)
}
validator: RegularExpressionValidator {
regularExpression: /([a-zA-Z0-9\-]+\.)*[a-zA-Z0-9]+(:[0-9]+)?/
}
}
QQC2.Button {
id: continueButton
text: i18nc("@action:button", "Continue")
action: root.action
}
nextAction: Kirigami.Action {
text: Registration.testing ? i18n("Loading") : null
enabled: Registration.status > Registration.ServerNoRegistration
onTriggered: root.processed("qrc:/Username.qml");
}
previousAction: Kirigami.Action {
onTriggered: root.processed("qrc:/LoginRegister.qml")
}
}

View File

@@ -6,16 +6,19 @@ import QtQuick.Controls 2.15 as QQC2
import QtQuick.Layouts 1.15
import org.kde.kirigami 2.20 as Kirigami
import org.kde.kirigamiaddons.formcard 1.0 as FormCard
import org.kde.neochat 1.0
Kirigami.LoadingPlaceholder {
property var showContinueButton: false
property var showBackButton: false
QQC2.Label {
LoginStep {
id: root
FormCard.FormTextDelegate {
text: i18n("Please wait. This might take a little while.")
}
FormCard.AbstractFormDelegate {
contentItem: QQC2.BusyIndicator {}
background: null
}
Connections {
target: Controller

View File

@@ -7,43 +7,34 @@ import QtQuick.Controls 2.15 as QQC2
import QtQuick.Layouts 1.15
import org.kde.kirigami 2.15 as Kirigami
import org.kde.kirigamiaddons.formcard 1.0 as FormCard
import org.kde.neochat 1.0
LoginStep {
id: login
id: root
showContinueButton: true
showBackButton: false
title: i18nc("@title", "Login")
message: i18n("Enter your Matrix ID")
onActiveFocusChanged: if (activeFocus) matrixIdField.forceActiveFocus()
Component.onCompleted: {
LoginHelper.matrixId = ""
}
Kirigami.FormLayout {
QQC2.TextField {
id: matrixIdField
Kirigami.FormData.label: i18n("Matrix ID:")
placeholderText: "@user:matrix.org"
Accessible.name: i18n("Matrix ID")
onTextChanged: {
LoginHelper.matrixId = text
}
FormCard.FormTextFieldDelegate {
id: matrixIdField
label: i18n("Matrix ID:")
placeholderText: "@user:example.org"
Accessible.name: i18n("Matrix ID")
onTextChanged: {
LoginHelper.matrixId = text
}
Component.onCompleted: {
matrixIdField.forceActiveFocus()
}
Keys.onReturnPressed: {
login.action.trigger()
}
Keys.onReturnPressed: {
root.nextAction.trigger()
}
}
action: Kirigami.Action {
nextAction: Kirigami.Action {
text: LoginHelper.isLoggedIn ? i18n("Already logged in") : (LoginHelper.testing && matrixIdField.acceptableInput) ? i18n("Loading…") : i18nc("@action:button", "Continue")
onTriggered: {
if (LoginHelper.supportsSso && LoginHelper.supportsPassword) {
@@ -56,4 +47,9 @@ LoginStep {
}
enabled: LoginHelper.homeserverReachable
}
previousAction: Kirigami.Action {
onTriggered: {
root.processed("qrc:/LoginRegister.qml")
}
}
}

View File

@@ -4,28 +4,26 @@
import QtQuick 2.15
import QtQuick.Controls 2.15 as QQC2
import QtQuick.Layouts 1.15
import org.kde.kirigami 2.15 as Kirigami
import org.kde.kirigamiaddons.formcard 1.0 as FormCard
import org.kde.neochat 1.0
LoginStep {
id: loginMethod
id: root
title: i18n("Login Methods")
onActiveFocusChanged: if (activeFocus) loginPasswordButton.forceActiveFocus()
Layout.alignment: Qt.AlignHCenter
QQC2.Button {
Layout.alignment: Qt.AlignHCenter
text: i18n("Login with password")
Layout.preferredWidth: Kirigami.Units.gridUnit * 12
FormCard.FormButtonDelegate {
id: loginPasswordButton
text: i18nc("@action:button", "Login with password")
onClicked: processed("qrc:/Password.qml")
}
QQC2.Button {
Layout.alignment: Qt.AlignHCenter
text: i18n("Login with single sign-on")
Layout.preferredWidth: Kirigami.Units.gridUnit * 12
FormCard.FormButtonDelegate {
id: loginSsoButton
text: i18nc("@action:button", "Login with single sign-on")
onClicked: processed("qrc:/Sso.qml")
}
}

View File

@@ -6,25 +6,25 @@ import QtQuick.Controls 2.15 as QQC2
import QtQuick.Layouts 1.15
import org.kde.kirigami 2.15 as Kirigami
import org.kde.kirigamiaddons.formcard 1.0 as FormCard
import org.kde.neochat 1.0
LoginStep {
id: loginRegister
id: root
Layout.alignment: Qt.AlignHCenter
onActiveFocusChanged: if (activeFocus) loginButton.forceActiveFocus()
QQC2.Button {
Layout.alignment: Qt.AlignHCenter
text: i18n("Login")
Layout.preferredWidth: Kirigami.Units.gridUnit * 12
onClicked: processed("qrc:/Login.qml")
Layout.fillWidth: true
FormCard.FormButtonDelegate {
id: loginButton
text: i18nc("@action:button", "Login")
onClicked: root.processed("qrc:/Login.qml")
}
QQC2.Button {
Layout.alignment: Qt.AlignHCenter
text: i18n("Register")
Layout.preferredWidth: Kirigami.Units.gridUnit * 12
onClicked: processed("qrc:/Homeserver.qml")
FormCard.FormButtonDelegate {
text: i18nc("@action:button", "Register")
onClicked: root.processed("qrc:/Homeserver.qml")
}
}

View File

@@ -7,21 +7,25 @@ import QtQuick.Layouts 1.14
/// Step for the login/registration flow
ColumnLayout {
id: root
property string title: i18n("Welcome")
property string message: i18n("Welcome")
property bool showContinueButton: false
property bool showBackButton: false
property bool acceptable: false
property string previousUrl: ""
/// Set to true if the login step does not have any controls. This will ensure that the focus remains on the "continue" button
property bool noControls: false
/// Process this module, this is called by the continue button.
/// Should call \sa processed when it finish successfully.
property QQC2.Action action: null
property QQC2.Action nextAction: null
/// Go to the previous module. This is called by the "go back" button.
/// If no "go back" button should be shown, this should be null.
property QQC2.Action previousAction: null
/// Called when switching to the next step.
signal processed(url nextUrl)
/// Show a message in a banner at the top of the page.
signal showMessage(string message)
/// Clears any error messages currently being shown
signal clearError()
}

View File

@@ -6,25 +6,12 @@ import QtQuick.Controls 2.15 as QQC2
import QtQuick.Layouts 1.15
import org.kde.kirigami 2.15 as Kirigami
import org.kde.kirigamiaddons.formcard 1.0 as FormCard
import org.kde.neochat 1.0
LoginStep {
id: password
title: i18nc("@title", "Password")
message: i18n("Enter your password")
showContinueButton: true
showBackButton: true
previousUrl: LoginHelper.isLoggingIn ? "" : LoginHelper.supportsSso ? "qrc:/LoginMethod.qml" : "qrc:/Login.qml"
action: Kirigami.Action {
text: i18nc("@action:button", "Login")
enabled: passwordField.text.length > 0 && !LoginHelper.isLoggingIn
onTriggered: {
LoginHelper.login();
}
}
id: root
Connections {
target: LoginHelper
@@ -33,20 +20,32 @@ LoginStep {
}
}
Kirigami.FormLayout {
Kirigami.PasswordField {
id: passwordField
onTextChanged: LoginHelper.password = text
enabled: !LoginHelper.isLoggingIn
Accessible.name: i18n("Password")
onActiveFocusChanged: if(activeFocus) passwordField.forceActiveFocus()
Component.onCompleted: {
passwordField.forceActiveFocus()
}
FormCard.FormTextFieldDelegate {
id: passwordField
Keys.onReturnPressed: {
password.action.trigger()
}
label: i18n("Password:")
onTextChanged: LoginHelper.password = text
enabled: !LoginHelper.isLoggingIn
echoMode: TextInput.Password
Accessible.name: i18n("Password")
statusMessage: LoginHelper.isInvalidPassword ? i18n("Invalid username or password") : ""
Keys.onReturnPressed: {
root.nextAction.trigger()
}
}
nextAction: Kirigami.Action {
text: i18nc("@action:button", "Login")
enabled: passwordField.text.length > 0 && !LoginHelper.isLoggingIn
onTriggered: {
root.clearError()
LoginHelper.login();
}
}
previousAction: Kirigami.Action {
onTriggered: processed("qrc:/Login.qml")
}
}

View File

@@ -0,0 +1,51 @@
// SPDX-FileCopyrightText: 2023 Tobias Fella <tobias.fella@kde.org>
// SPDX-License-Identifier: GPL-2.0-or-later
import QtQuick 2.15
import QtQuick.Controls 2.12 as QQC2
import org.kde.kirigami 2.12 as Kirigami
import org.kde.kirigamiaddons.formcard 1.0 as FormCard
import org.kde.neochat 1.0
LoginStep {
id: root
onActiveFocusChanged: if (activeFocus) passwordField.forceActiveFocus()
FormCard.FormTextFieldDelegate {
id: passwordField
label: i18n("Password:")
echoMode: TextInput.Password
onTextChanged: Registration.password = text
Keys.onReturnPressed: {
confirmPasswordField.forceActiveFocus()
}
}
FormCard.FormTextFieldDelegate {
id: confirmPasswordField
label: i18n("Confirm Password:")
enabled: passwordField.enabled
echoMode: TextInput.Password
statusMessage: passwordField.text.length === confirmPasswordField.text.length && passwordField.text !== confirmPasswordField.text ? i18n("The passwords do not match.") : ""
Keys.onReturnPressed: {
if (root.nextAction.enabled) {
root.nextAction.trigger()
}
}
}
nextAction: Kirigami.Action {
onTriggered: {
passwordField.enabled = false
Registration.registerAccount()
}
enabled: passwordField.text === confirmPasswordField.text
}
previousAction: Kirigami.Action {
onTriggered: root.processed("qrc:/Username.qml")
}
}

View File

@@ -6,47 +6,38 @@ import QtQuick.Controls 2.15 as QQC2
import QtQuick.Layouts 1.15
import org.kde.kirigami 2.12 as Kirigami
import org.kde.kirigamiaddons.formcard 1.0 as FormCard
import org.kde.neochat 1.0
LoginStep {
id: root
title: i18nc("@title", "Login")
message: i18n("Login with single sign-on")
noControls: true
Kirigami.FormLayout {
Connections {
target: LoginHelper
function onSsoUrlChanged() {
UrlHelper.openUrl(LoginHelper.ssoUrl)
root.showMessage(i18n("Complete the authentication steps in your browser"))
loginButton.enabled = true
loginButton.text = i18n("Login")
}
function onConnected() {
processed("qrc:/Loading.qml")
}
Component.onCompleted: LoginHelper.loginWithSso()
Connections {
target: LoginHelper
function onSsoUrlChanged() {
UrlHelper.openUrl(LoginHelper.ssoUrl)
}
RowLayout {
QQC2.Button {
text: i18nc("@action:button", "Back")
onClicked: {
module.source = "qrc:/Login.qml"
}
}
QQC2.Button {
id: loginButton
text: i18n("Login")
onClicked: {
LoginHelper.loginWithSso()
loginButton.enabled = false
loginButton.text = i18n("Loading…")
}
Component.onCompleted: forceActiveFocus()
Keys.onReturnPressed: clicked()
}
function onConnected() {
processed("qrc:/Loading.qml")
}
}
FormCard.FormTextDelegate {
text: i18n("Continue the login process in your browser.")
}
previousAction: Kirigami.Action {
onTriggered: processed("qrc:/Login.qml")
}
nextAction: Kirigami.Action {
text: i18nc("@action:button", "Re-open SSO URL")
onTriggered: UrlHelper.openUrl(LoginHelper.ssoUrl)
}
}

View File

@@ -0,0 +1,39 @@
// SPDX-FileCopyrightText: 2023 Tobias Fella <tobias.fella@kde.org>
// SPDX-License-Identifier: GPL-2.0-or-later
import QtQuick 2.15
import QtQuick.Controls 2.12 as QQC2
import QtQuick.Layouts 1.12
import org.kde.kirigami 2.12 as Kirigami
import org.kde.kirigamiaddons.formcard 1.0 as FormCard
import org.kde.neochat 1.0
LoginStep {
id: root
noControls: true
FormCard.FormTextDelegate {
text: i18n("Terms & Conditions")
description: i18n("By continuing with the registration, you agree to the following terms and conditions:")
}
Repeater {
model: Registration.terms
delegate: FormCard.FormTextDelegate {
text: "<a href=\"" + modelData.url + "\">" + modelData.title + "</a>"
onLinkActivated: Qt.openUrlExternally(modelData.url)
}
}
nextAction: Kirigami.Action {
onTriggered: {
Registration.registerAccount()
}
}
previousAction: Kirigami.Action {
onTriggered: root.processed("qrc:/Username.qml")
}
}

View File

@@ -0,0 +1,46 @@
// SPDX-FileCopyrightText: 2023 Tobias Fella <tobias.fella@kde.org>
// SPDX-License-Identifier: GPL-2.0-or-later
import QtQuick 2.15
import QtQuick.Controls 2.12 as QQC2
import org.kde.kirigami 2.12 as Kirigami
import org.kde.kirigamiaddons.formcard 1.0 as FormCard
import org.kde.neochat 1.0
LoginStep {
id: root
onActiveFocusChanged: if (activeFocus) usernameField.forceActiveFocus()
FormCard.FormTextFieldDelegate {
id: usernameField
label: i18n("Username:")
placeholderText: "user"
onTextChanged: timer.restart()
statusMessage: Registration.status === Registration.UsernameTaken ? i18n("Username unavailable") : ""
Keys.onReturnPressed: {
if (root.nextAction.enabled) {
root.nextAction.trigger()
}
}
}
Timer {
id: timer
interval: 500
onTriggered: Registration.username = usernameField.text
}
nextAction: Kirigami.Action {
text: Registration.status === Registration.TestingUsername ? i18n("Loading") : null
onTriggered: root.processed("qrc:/RegisterPassword.qml")
enabled: Registration.status === Registration.Ready
}
previousAction: Kirigami.Action {
onTriggered: root.processed("qrc:/Homeserver.qml")
}
}

View File

@@ -28,8 +28,6 @@ Components.AlbumMaximizeComponent {
readonly property var currentJsonSource: model.data(model.index(content.currentIndex, 0), MessageEventModel.SourceRole)
autoLoad: false
downloadAction: Components.DownloadAction {
id: downloadAction
onTriggered: {
@@ -91,7 +89,7 @@ Components.AlbumMaximizeComponent {
file: parent,
mimeType: root.currentMimeType,
progressInfo: root.currentProgressInfo,
plainMessage: root.currentPlainText
plainText: root.currentPlainText
});
contextMenu.closeFullscreen.connect(root.close)
contextMenu.open();

View File

@@ -375,10 +375,7 @@ ColumnLayout {
MouseArea {
anchors.fill: parent
onClicked: {
userDetailDialog.createObject(QQC2.ApplicationWindow.overlay, {
room: currentRoom,
user: root.author
}).open();
RoomManager.visitUser(root.author.object, "mention")
}
cursorShape: Qt.PointingHandCursor
}
@@ -459,10 +456,7 @@ ColumnLayout {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: {
userDetailDialog.createObject(QQC2.ApplicationWindow.overlay, {
room: currentRoom,
user: root.author
}).open();
RoomManager.visitUser(root.author.object, "mention")
}
}
}
@@ -605,6 +599,7 @@ ColumnLayout {
source: root.jsonSource,
eventType: root.delegateType,
plainText: root.plainText,
htmlText: root.display,
});
contextMenu.open();
}

View File

@@ -177,13 +177,13 @@ TimelineContainer {
onDurationChanged: {
if (!duration) {
vid.supportStreaming = false;
root.supportStreaming = false;
}
}
onErrorChanged: {
if (error != MediaPlayer.NoError) {
vid.supportStreaming = false;
root.supportStreaming = false;
}
}
@@ -391,7 +391,7 @@ TimelineContainer {
TapHandler {
acceptedButtons: Qt.LeftButton
onTapped: if (vid.supportStreaming || root.progressInfo.completed) {
onTapped: if (root.supportStreaming || root.progressInfo.completed) {
if (vid.playbackState == MediaPlayer.PlayingState) {
vid.pause()
} else {

View File

@@ -17,10 +17,10 @@ QQC2.ScrollView {
required property NeoChatRoom currentRoom
onCurrentRoomChanged: {
roomChanging = true;
roomChangingTimer.restart()
applicationWindow().hoverLinkIndicator.text = "";
messageListView.positionViewAtBeginning();
hasScrolledUpBefore = false;
roomChanging = true;
}
property bool roomChanging: false
readonly property bool atYEnd: messageListView.atYEnd
@@ -47,19 +47,16 @@ QQC2.ScrollView {
interactive: Kirigami.Settings.isMobile
bottomMargin: Kirigami.Units.largeSpacing + Math.round(Kirigami.Theme.defaultFont.pointSize * 2)
model: collapseStateProxyModel
model: sortedMessageEventModel
MessageEventModel {
id: messageEventModel
room: root.currentRoom
}
CollapseStateProxyModel {
id: collapseStateProxyModel
sourceModel: MessageFilterModel {
id: sortedMessageEventModel
sourceModel: messageEventModel
}
MessageFilterModel {
id: sortedMessageEventModel
sourceModel: messageEventModel
}
Timer {
@@ -79,6 +76,13 @@ QQC2.ScrollView {
messageEventModel.fetchMore(messageEventModel.index(0, 0));
}
Timer {
id: roomChangingTimer
interval: 1000
onTriggered: {
root.roomChanging = false
}
}
onAtYEndChanged: if (!root.roomChanging) {
if (atYEnd && root.hasScrolledUpBefore) {
if (QQC2.ApplicationWindow.window && (QQC2.ApplicationWindow.window.visibility !== QQC2.ApplicationWindow.Hidden)) {
@@ -215,12 +219,6 @@ QQC2.ScrollView {
FileDelegateContextMenu {}
}
Component {
id: userDetailDialog
UserDetailDialog {}
}
TypingPane {
id: typingPane
visible: root.currentRoom && root.currentRoom.usersTyping.length > 0
@@ -348,7 +346,7 @@ QQC2.ScrollView {
MediaMessageFilterModel {
id: mediaMessageFilterModel
sourceModel: collapseStateProxyModel
sourceModel: sortedMessageEventModel
}
Component {
@@ -438,11 +436,4 @@ QQC2.ScrollView {
function positionViewAtBeginning() {
messageListView.positionViewAtBeginning()
}
function showUserDetail(user) {
userDetailDialog.createObject(QQC2.ApplicationWindow.overlay, {
room: root.currentRoom,
user: root.currentRoom.getUser(user.id),
}).open();
}
}

View File

@@ -0,0 +1,46 @@
// SPDX-FileCopyrightText: 2023 Tobias Fella <tobias.fella@kde.org>
// SPDX-License-Identifier: GPL-2.0-or-later
import QtQuick 2.15
import QtQuick.Controls 2.15 as QQC2
import QtQuick.Layouts 1.15
import org.kde.kirigami 2.20 as Kirigami
import org.kde.kirigamiaddons.formcard 1.0 as FormCard
import org.kde.neochat 1.0
FormCard.FormCardPage {
id: root
property var connection
title: i18nc("@title", "Deactivate Account")
FormCard.FormHeader {
title: i18nc("@title", "Deactivate Account")
}
FormCard.FormCard {
FormCard.FormTextDelegate {
text: i18nc("@title", "Warning")
description: i18n("Your account will be permanently disabled.\nThis cannot be undone.\nYour Matrix ID will not be available for new accounts.\nYour messages will stay available.")
}
FormCard.FormTextFieldDelegate {
id: passwordField
label: i18n("Password")
echoMode: TextInput.Password
}
FormCard.FormButtonDelegate {
text: i18n("Deactivate account")
icon.name: "emblem-warning"
enabled: passwordField.text.length > 0
onClicked: {
root.connection.deactivateAccount(passwordField.text)
root.closeDialog()
}
}
}
}

View File

@@ -37,7 +37,7 @@ QQC2.Dialog {
text: i18n("Sign out")
QQC2.DialogButtonBox.buttonRole: QQC2.DialogButtonBox.AcceptRole
onClicked: {
Controller.logout(root.connection, true);
root.connection.logout(true);
root.close();
root.accepted();
}

View File

@@ -61,6 +61,7 @@ Kirigami.Dialog {
elide: Text.ElideRight
wrapMode: Text.NoWrap
text: user.displayName
textFormat: Text.PlainText
}
Kirigami.SelectableLabel {

View File

@@ -24,7 +24,9 @@ Labs.MenuBar {
text: i18nc("menu", "Configure NeoChat...")
shortcut: StandardKey.Preferences
onTriggered: pageStack.pushDialogLayer("qrc:/SettingsPage.qml", {}, {
onTriggered: pageStack.pushDialogLayer("qrc:/SettingsPage.qml", {
connection: Controller.activeConnection
}, {
title: i18n("Configure")
})
}

View File

@@ -7,6 +7,8 @@ import QtQuick.Layouts 1.15
import org.kde.kirigami 2.20 as Kirigami
import org.kde.neochat 1.0
Kirigami.Page {
id: banSheet

View File

@@ -19,6 +19,7 @@ Loader {
required property string source
property string selectedText: ""
required property string plainText
property string htmlText: undefined
property list<Kirigami.Action> nestedActions
@@ -40,6 +41,20 @@ Loader {
currentRoom.chatBoxEditId = "";
}
},
Kirigami.Action {
text: i18nc("@action:inmenu As in 'Forward this message'", "Forward")
icon.name: "mail-forward-symbolic"
onTriggered: {
let page = applicationWindow().pageStack.pushDialogLayer("qrc:/ChooseRoomDialog.qml", {}, {
title: i18nc("@title", "Forward Message"),
width: Kirigami.Units.gridUnit * 25
})
page.chosen.connect(function(targetRoomId) {
Controller.activeConnection.room(targetRoomId).postHtmlMessage(loadRoot.plainText, loadRoot.htmlText ? loadRoot.htmlText : loadRoot.plainText)
page.closeDialog()
})
}
},
Kirigami.Action {
visible: author.id === currentRoom.localUser.id || currentRoom.canSendState("redact")
text: i18n("Remove")

View File

@@ -7,6 +7,8 @@ import QtQuick.Layouts 1.15
import org.kde.kirigami 2.20 as Kirigami
import org.kde.neochat 1.0
Kirigami.Page {
id: reportSheet

View File

@@ -0,0 +1,39 @@
// SPDX-FileCopyrightText: 2023 Tobias Fella <tobias.fella@kde.org>
// SPDX-License-Identifier: GPL-2.0-or-later
import QtQuick 2.15
import QtQuick.Controls 2.15
import org.kde.kirigami 2.20 as Kirigami
import org.kde.neochat 1.0
import "./RoomList"
Kirigami.ScrollablePage {
id: root
title: i18nc("@title", "Choose a Room")
signal chosen(string roomId)
header: Kirigami.SearchField {
onTextChanged: sortModel.filterText = text
}
ListView {
model: SortFilterRoomListModel {
id: sortModel
sourceModel: RoomListModel {
connection: Controller.activeConnection
}
}
delegate: RoomDelegate {
id: roomDelegate
filterText: ""
onClicked: {
root.chosen(roomDelegate.currentRoom.id)
}
}
}
}

View File

@@ -31,15 +31,15 @@ Kirigami.Page {
imageDoc.crop(selectionTool.selectionX / ratioX, selectionTool.selectionY / ratioY, selectionTool.selectionWidth / ratioX, selectionTool.selectionHeight / ratioY);
}
actions {
left: Kirigami.Action {
actions: [
Kirigami.Action {
id: undoAction
text: i18nc("@action:button Undo modification", "Undo")
icon.name: "edit-undo"
onTriggered: imageDoc.undo();
visible: imageDoc.edited
}
main: Kirigami.Action {
},
Kirigami.Action {
id: okAction
text: i18nc("@action:button Accept image modification", "Accept")
icon.name: "dialog-ok"
@@ -54,7 +54,7 @@ Kirigami.Page {
}
}
}
}
]

View File

@@ -122,10 +122,10 @@ Kirigami.ScrollablePage {
title: i18nc("@title:window", "Add server")
onSheetOpenChanged: if (!serverUrlField.isValidServer && !sheetOpen) {
onOpened: if (!serverUrlField.isValidServer && !opened) {
serverField.currentIndex = 0
server = serverField.currentValue
} else if (sheetOpen) {
} else if (opened) {
serverUrlField.forceActiveFocus()
}

View File

@@ -40,7 +40,7 @@ RowLayout {
text: i18n("Create a Space")
icon.name: "list-add"
onTriggered: {
let dialog = createSpaceDialog.createObject(root.overlay);
let dialog = createSpaceDialog.createObject(applicationWindow().overlay);
dialog.open()
}
}

View File

@@ -8,7 +8,6 @@ import QtQuick.Layouts 1.15
import QtQml.Models 2.15
import org.kde.kirigami 2.15 as Kirigami
import org.kde.kitemmodels 1.0
import org.kde.neochat 1.0

View File

@@ -72,7 +72,12 @@ QQC2.Control {
connection: Controller.activeConnection
}
}
onCountChanged: root.enabled = count > 0
onCountChanged: {
root.enabled = count > 0
if (!Controller.activeConnection.room(root.selectedSpaceId)) {
root.selectedSpaceId = ""
}
}
Component.onCompleted: root.enabled = count > 0
delegate: AvatarTabButton {

View File

@@ -193,7 +193,7 @@ QQC2.ToolBar {
Layout.fillWidth: true
}
QQC2.Label {
text: (Controller.activeAccountLabel.length > 0 ? (Controller.activeAccountLabel + " ") : "") + Controller.activeConnection.localUser.id
text: (Controller.activeConnection.label.length > 0 ? (Controller.activeConnection.label + " ") : "") + Controller.activeConnection.localUser.id
font.pointSize: displayNameLabel.font.pointSize * 0.8
opacity: 0.7
textFormat: Text.PlainText

View File

@@ -27,6 +27,14 @@ Kirigami.Page {
focus: true
padding: 0
actions: [
Kirigami.Action {
visible: Kirigami.Settings.isMobile || !applicationWindow().pageStack.wideMode
icon.name: "view-right-new"
onTriggered: applicationWindow().openRoomDrawer()
}
]
KeyNavigation.left: pageStack.get(0)
onCurrentRoomChanged: {
@@ -38,7 +46,7 @@ Kirigami.Page {
Connections {
target: Controller
function onIsOnlineChanged() {
if (true || !Controller.isOnline) {
if (!Controller.isOnline) {
banner.text = i18n("NeoChat is offline. Please check your network connection.");
banner.visible = true;
banner.type = Kirigami.MessageType.Error;
@@ -181,7 +189,22 @@ Kirigami.Page {
banner.visible = true;
}
Connections {
target: RoomManager
function onShowUserDetail(user) {
root.showUserDetail(user)
}
}
function showUserDetail(user) {
timelineViewLoader.item.showUserDetail(user)
userDetailDialog.createObject(QQC2.ApplicationWindow.overlay, {
room: root.currentRoom,
user: root.currentRoom.getUser(user.id),
}).open();
}
Component {
id: userDetailDialog
UserDetailDialog {}
}
}

View File

@@ -6,15 +6,16 @@ import QtQuick.Controls 2.15 as QQC2
import QtQuick.Layouts 1.15
import org.kde.kirigami 2.15 as Kirigami
import org.kde.kirigamiaddons.formcard 1.0 as FormCard
import org.kde.neochat 1.0
Kirigami.ScrollablePage {
id: welcomePage
FormCard.FormCardPage {
id: root
property alias currentStep: module.item
title: module.item.title ?? i18n("Welcome")
title: i18n("Welcome")
header: QQC2.Control {
contentItem: Kirigami.InlineMessage {
@@ -25,79 +26,111 @@ Kirigami.ScrollablePage {
}
}
Component.onCompleted: LoginHelper.init()
FormCard.FormCard {
id: contentCard
Connections {
target: LoginHelper
function onErrorOccured(message) {
headerMessage.text = message;
headerMessage.visible = true;
headerMessage.type = Kirigami.MessageType.Error;
FormCard.AbstractFormDelegate {
contentItem: Kirigami.Icon {
source: "org.kde.neochat"
Layout.fillWidth: true
Layout.preferredHeight: Kirigami.Units.gridUnit * 16
}
background: Item {}
onActiveFocusChanged: if (activeFocus) module.item.forceActiveFocus()
}
}
Connections {
target: Controller
function onInitiated() {
pageStack.layers.pop();
FormCard.FormTextDelegate {
id: welcomeMessage
text: AccountRegistry.accountCount > 0 ? i18n("Log in to a different account or create a new account.") : i18n("Welcome to NeoChat! Continue by logging in or creating a new account.")
}
}
ColumnLayout {
Kirigami.Icon {
source: "org.kde.neochat"
Layout.fillWidth: true
Layout.preferredHeight: Kirigami.Units.gridUnit * 16
}
QQC2.Label {
Layout.fillWidth: true
horizontalAlignment: Text.AlignHCenter
font.pixelSize: 25
text: module.item.message ?? module.item.title ?? i18n("Welcome to Matrix")
FormCard.FormDelegateSeparator {
above: welcomeMessage
}
Loader {
id: module
Layout.alignment: Qt.AlignHCenter
source: "qrc:/Login.qml"
onSourceChanged: {
headerMessage.visible = false
headerMessage.text = ""
}
}
RowLayout {
Layout.alignment: Qt.AlignHCenter
Layout.fillWidth: true
source: "qrc:/LoginRegister.qml"
QQC2.Button {
text: i18nc("@action:button", "Back")
Connections {
id: stepConnections
target: currentStep
enabled: welcomePage.currentStep.previousUrl !== ""
visible: welcomePage.currentStep.showBackButton
Layout.alignment: Qt.AlignHCenter
onClicked: {
module.source = welcomePage.currentStep.previousUrl
function onProcessed(nextUrl) {
module.source = nextUrl;
headerMessage.text = "";
headerMessage.visible = false;
if (!module.item.noControls) {
module.item.forceActiveFocus()
} else {
continueButton.forceActiveFocus()
}
}
function onShowMessage(message) {
headerMessage.text = message;
headerMessage.visible = true;
headerMessage.type = Kirigami.MessageType.Information;
}
function onClearError() {
headerMessage.text = "";
headerMessage.visible = false;
}
}
QQC2.Button {
id: continueButton
enabled: welcomePage.currentStep.acceptable
visible: welcomePage.currentStep.showContinueButton
action: welcomePage.currentStep.action
Connections {
target: Registration
function onNextStepChanged() {
if (Registration.nextStep === "m.login.recaptcha") {
stepConnections.onProcessed("qrc:/Captcha.qml")
}
if (Registration.nextStep === "m.login.terms") {
stepConnections.onProcessed("qrc:/Terms.qml")
}
if (Registration.nextStep === "m.login.email.identity") {
stepConnections.onProcessed("qrc:/Email.qml")
}
if (Registration.nextStep === "loading") {
stepConnections.onProcessed("qrc:/Loading.qml")
}
}
}
Connections {
target: LoginHelper
function onErrorOccured(message) {
headerMessage.text = message;
headerMessage.visible = message.length > 0;
headerMessage.type = Kirigami.MessageType.Error;
}
}
}
Connections {
target: currentStep
FormCard.FormDelegateSeparator {
below: continueButton
}
function onProcessed(nextUrl) {
module.source = nextUrl;
}
function onShowMessage(message) {
headerMessage.text = message;
headerMessage.visible = true;
headerMessage.type = Kirigami.MessageType.Information;
}
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()
icon.name: "arrow-right"
enabled: root.currentStep.nextAction ? root.currentStep.nextAction.enabled : false
}
FormCard.FormButtonDelegate {
text: i18nc("@action:button", "Go back")
visible: root.currentStep.previousAction
onClicked: root.currentStep.previousAction.trigger()
icon.name: "arrow-left"
enabled: root.currentStep.previousAction ? root.currentStep.previousAction.enabled : false
}
}
Component.onCompleted: {
LoginHelper.init()
module.item.forceActiveFocus()
Registration.username = ""
Registration.password = ""
Registration.email = ""
}
}

View File

@@ -1,367 +0,0 @@
// SPDX-FileCopyrightText: 2018-2019 Black Hat <bhat@encom.eu.org>
// SPDX-FileCopyrightText: 2020 Carl Schwan <carl@carlschwan.eu>
// SPDX-License-Identifier: GPL-3.0-only
import QtQuick 2.15
import QtQuick.Controls 2.15 as QQC2
import QtQuick.Layouts 1.15
import org.kde.kirigami 2.20 as Kirigami
import org.kde.kirigamiaddons.delegates 1.0 as Delegates
import org.kde.kirigamiaddons.labs.components 1.0 as KirigamiComponents
import org.kde.kitemmodels 1.0
import org.kde.neochat 1.0
Kirigami.OverlayDrawer {
id: roomDrawer
readonly property NeoChatRoom room: RoomManager.currentRoom
width: actualWidth
readonly property int minWidth: Kirigami.Units.gridUnit * 15
readonly property int maxWidth: Kirigami.Units.gridUnit * 25
readonly property int defaultWidth: Kirigami.Units.gridUnit * 20
property int actualWidth: {
if (Config.roomDrawerWidth === -1) {
return Kirigami.Units.gridUnit * 20;
} else {
return Config.roomDrawerWidth
}
}
MouseArea {
anchors.left: parent.left
anchors.top: parent.top
anchors.bottom: parent.bottom
anchors.right: undefined
width: 2
z: 500
cursorShape: !Kirigami.Settings.isMobile ? Qt.SplitHCursor : undefined
enabled: true
visible: true
onPressed: _lastX = mapToGlobal(mouseX, mouseY).x
onReleased: {
Config.roomDrawerWidth = roomDrawer.actualWidth;
Config.save();
}
property real _lastX: -1
onPositionChanged: {
if (_lastX === -1) {
return;
}
if (Qt.application.layoutDirection === Qt.RightToLeft) {
roomDrawer.actualWidth = Math.min(roomDrawer.maxWidth, Math.max(roomDrawer.minWidth, Config.roomDrawerWidth - _lastX + mapToGlobal(mouseX, mouseY).x))
} else {
roomDrawer.actualWidth = Math.min(roomDrawer.maxWidth, Math.max(roomDrawer.minWidth, Config.roomDrawerWidth + _lastX - mapToGlobal(mouseX, mouseY).x))
}
}
}
enabled: true
edge: Qt.application.layoutDirection == Qt.RightToLeft ? Qt.LeftEdge : Qt.RightEdge
// If modal has been changed and the drawer is closed automatically then dim on popup open will have been switched off in main.qml so switch it back on after the animation completes.
// This is to avoid dim being active for a split second when the drawer is switched to modal which looks terrible.
onAnimatingChanged: if (dim === false) dim = undefined
topPadding: 0
leftPadding: 0
rightPadding: 0
Kirigami.Theme.colorSet: Kirigami.Theme.View
contentItem: Loader {
id: loader
active: roomDrawer.drawerOpen
sourceComponent: ColumnLayout {
readonly property string userSearchText: userListView.headerItem ? userListView.headerItem.userListSearchField.text : ''
property alias highlightedUser: userListView.currentIndex
spacing: 0
function clearSearch() {
userListView.headerItem.userListSearchField.text = ""
}
QQC2.ToolBar {
Layout.fillWidth: true
Layout.preferredHeight: pageStack.globalToolBar.preferredHeight
contentItem: RowLayout {
Kirigami.Heading {
Layout.fillWidth: true
text: i18n("Room information")
}
QQC2.ToolButton {
id: settingsButton
icon.name: "settings-configure"
text: i18n("Room settings")
display: QQC2.AbstractButton.IconOnly
onClicked: QQC2.ApplicationWindow.window.pageStack.pushDialogLayer('qrc:/Categories.qml', {room: room}, { title: i18n("Room Settings") })
QQC2.ToolTip.text: text
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
QQC2.ToolTip.visible: hovered
}
}
}
QQC2.ScrollView {
Layout.fillWidth: true
Layout.fillHeight: true
// HACK: Hide unnecessary horizontal scrollbar (https://bugreports.qt.io/browse/QTBUG-83890)
QQC2.ScrollBar.horizontal.policy: QQC2.ScrollBar.AlwaysOff
ListView {
id: userListView
header: ColumnLayout {
id: columnLayout
property alias userListSearchField: userListSearchField
spacing: 0
width: userListView.width
Loader {
active: true
Layout.fillWidth: true
Layout.topMargin: Kirigami.Units.smallSpacing
sourceComponent: room.isDirectChat() ? directChatDrawerHeader : groupChatDrawerHeader
onItemChanged: if (item) {
userListView.positionViewAtBeginning();
}
}
Kirigami.ListSectionHeader {
label: i18n("Options")
activeFocusOnTab: false
Layout.fillWidth: true
}
Delegates.RoundedItemDelegate {
id: devtoolsButton
icon.name: "tools"
text: i18n("Open developer tools")
visible: Config.developerTools
Layout.fillWidth: true
onClicked: {
applicationWindow().pageStack.pushDialogLayer("qrc:/DevtoolsPage.qml", {room: room}, {title: i18n("Developer Tools")})
}
}
Delegates.RoundedItemDelegate {
id: searchButton
icon.name: "search"
text: i18n("Search in this room")
Layout.fillWidth: true
onClicked: {
pageStack.pushDialogLayer("qrc:/SearchPage.qml", {
currentRoom: room
}, {
title: i18nc("@action:title", "Search")
})
}
}
Delegates.RoundedItemDelegate {
id: favouriteButton
icon.name: room && room.isFavourite ? "rating" : "rating-unrated"
text: room && room.isFavourite ? i18n("Remove room from favorites") : i18n("Make room favorite")
onClicked: room.isFavourite ? room.removeTag("m.favourite") : room.addTag("m.favourite", 1.0)
Layout.fillWidth: true
}
Delegates.RoundedItemDelegate {
id: locationsButton
icon.name: "map-flat"
text: i18n("Show locations for this room")
onClicked: pageStack.pushDialogLayer("qrc:/LocationsPage.qml", {
room: room
}, {
title: i18nc("Locations on a map", "Locations")
})
Layout.fillWidth: true
}
Kirigami.ListSectionHeader {
label: i18n("Members")
activeFocusOnTab: false
spacing: 0
visible: !room.isDirectChat()
Layout.fillWidth: true
QQC2.ToolButton {
id: memberSearchToggle
checkable: true
icon.name: "search"
QQC2.ToolTip.text: i18n("Search user in room")
QQC2.ToolTip.visible: hovered
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
onToggled: {
userListSearchField.text = "";
}
}
QQC2.ToolButton {
visible: roomDrawer.room.canSendState("invite")
icon.name: "list-add-user"
onClicked: {
applicationWindow().pageStack.pushDialogLayer("qrc:/InviteUserPage.qml", {room: roomDrawer.room}, {title: i18nc("@title", "Invite a User")})
}
QQC2.ToolTip.text: i18n("Invite user to room")
QQC2.ToolTip.visible: hovered
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
}
QQC2.Label {
Layout.alignment: Qt.AlignRight
text: room ? i18np("%1 member", "%1 members", room.joinedCount) : i18n("No member count")
}
}
Kirigami.SearchField {
id: userListSearchField
visible: memberSearchToggle.checked
onVisibleChanged: if (visible) forceActiveFocus()
Layout.fillWidth: true
Layout.leftMargin: Kirigami.Units.largeSpacing - 1
Layout.rightMargin: Kirigami.Units.largeSpacing - 1
Layout.bottomMargin: Kirigami.Units.smallSpacing
focusSequence: "Ctrl+Shift+F"
onAccepted: sortedMessageEventModel.filterString = text;
}
}
KSortFilterProxyModel {
id: sortedMessageEventModel
sourceModel: UserListModel {
room: roomDrawer.room
}
sortRole: "powerLevel"
sortOrder: Qt.DescendingOrder
filterRole: "name"
filterCaseSensitivity: Qt.CaseInsensitive
}
model: room.isDirectChat() ? 0 : sortedMessageEventModel
clip: true
activeFocusOnTab: true
delegate: Delegates.RoundedItemDelegate {
id: userDelegate
required property string name
required property string userId
required property string avatar
required property int powerLevel
required property string powerLevelString
implicitHeight: Kirigami.Units.gridUnit * 2
text: name
onClicked: {
userDelegate.highlighted = true;
const popup = userDetailDialog.createObject(QQC2.ApplicationWindow.overlay, {
room: room,
user: room.getUser(userDelegate.userId)
});
popup.closed.connect(() => {
userDelegate.highlighted = false;
});
if (roomDrawer.modal) {
roomDrawer.close();
}
popup.open();
}
contentItem: RowLayout {
KirigamiComponents.Avatar {
implicitWidth: height
sourceSize {
height: Kirigami.Units.gridUnit + Kirigami.Units.smallSpacing * 2.5
width: Kirigami.Units.gridUnit + Kirigami.Units.smallSpacing * 2.5
}
source: userDelegate.avatar
name: userDelegate.userId
Layout.fillHeight: true
}
QQC2.Label {
text: userDelegate.name
textFormat: Text.PlainText
elide: Text.ElideRight
Layout.fillWidth: true
}
QQC2.Label {
visible: userDelegate.powerLevel > 0
text: userDelegate.powerLevelString
color: Kirigami.Theme.disabledTextColor
textFormat: Text.PlainText
}
}
}
}
}
}
}
onRoomChanged: {
if (loader.active) {
loader.item.clearSearch()
loader.item.highlightedUser = -1
}
if (room == null) {
close()
}
}
Component {
id: userDetailDialog
UserDetailDialog {}
}
Component {
id: groupChatDrawerHeader
GroupChatDrawerHeader {}
}
Component {
id: directChatDrawerHeader
DirectChatDrawerHeader {}
}
}

View File

@@ -26,17 +26,7 @@ ColumnLayout {
Layout.alignment: Qt.AlignHCenter
onClicked: {
const popup = userDetailDialog.createObject(QQC2.ApplicationWindow.overlay, {
room: room,
user: room.getUser(room.directChatRemoteUser.id),
})
popup.closed.connect(() => {
userListItem.highlighted = false
})
if (roomDrawer.modal) {
roomDrawer.close()
}
popup.open()
RoomManager.visitUser(room.getUser(room.directChatRemoteUser.id).object, "mention")
}
contentItem: KirigamiComponents.Avatar {

View File

@@ -68,7 +68,8 @@ ColumnLayout {
Layout.fillWidth: true
font: Kirigami.Theme.smallFont
textFormat: TextEdit.PlainText
text: room && room.canonicalAlias ? room.canonicalAlias : i18n("No Canonical Alias")
visible: room && room.canonicalAlias
text: room && room.canonicalAlias ? room.canonicalAlias : ""
}
}
}

View File

@@ -0,0 +1,121 @@
// SPDX-FileCopyrightText: 2018-2019 Black Hat <bhat@encom.eu.org>
// SPDX-FileCopyrightText: 2020 Carl Schwan <carl@carlschwan.eu>
// SPDX-License-Identifier: GPL-3.0-only
import QtQuick 2.15
import QtQuick.Controls 2.15 as QQC2
import QtQuick.Layouts 1.15
import org.kde.kirigami 2.20 as Kirigami
import org.kde.kirigamiaddons.delegates 1.0 as Delegates
import org.kde.kirigamiaddons.labs.components 1.0 as KirigamiComponents
import org.kde.kitemmodels 1.0
import org.kde.neochat 1.0
Kirigami.OverlayDrawer {
id: root
readonly property NeoChatRoom room: RoomManager.currentRoom
width: actualWidth
readonly property int minWidth: Kirigami.Units.gridUnit * 15
readonly property int maxWidth: Kirigami.Units.gridUnit * 25
readonly property int defaultWidth: Kirigami.Units.gridUnit * 20
property int actualWidth: {
if (Config.roomDrawerWidth === -1) {
return Kirigami.Units.gridUnit * 20;
} else {
return Config.roomDrawerWidth
}
}
MouseArea {
anchors.left: parent.left
anchors.top: parent.top
anchors.bottom: parent.bottom
anchors.right: undefined
width: 2
z: 500
cursorShape: !Kirigami.Settings.isMobile ? Qt.SplitHCursor : undefined
enabled: true
visible: true
onPressed: _lastX = mapToGlobal(mouseX, mouseY).x
onReleased: {
Config.roomDrawerWidth = root.actualWidth;
Config.save();
}
property real _lastX: -1
onPositionChanged: {
if (_lastX === -1) {
return;
}
if (Qt.application.layoutDirection === Qt.RightToLeft) {
root.actualWidth = Math.min(root.maxWidth, Math.max(root.minWidth, Config.roomDrawerWidth - _lastX + mapToGlobal(mouseX, mouseY).x))
} else {
root.actualWidth = Math.min(root.maxWidth, Math.max(root.minWidth, Config.roomDrawerWidth + _lastX - mapToGlobal(mouseX, mouseY).x))
}
}
}
enabled: true
edge: Qt.application.layoutDirection == Qt.RightToLeft ? Qt.LeftEdge : Qt.RightEdge
// If modal has been changed and the drawer is closed automatically then dim on popup open will have been switched off in main.qml so switch it back on after the animation completes.
// This is to avoid dim being active for a split second when the drawer is switched to modal which looks terrible.
onAnimatingChanged: if (dim === false) dim = undefined
topPadding: 0
leftPadding: 0
rightPadding: 0
Kirigami.Theme.colorSet: Kirigami.Theme.View
contentItem: Loader {
id: loader
active: root.drawerOpen
sourceComponent: ColumnLayout {
spacing: 0
QQC2.ToolBar {
Layout.fillWidth: true
Layout.preferredHeight: pageStack.globalToolBar.preferredHeight
contentItem: RowLayout {
Kirigami.Heading {
Layout.fillWidth: true
text: i18n("Room information")
}
QQC2.ToolButton {
id: settingsButton
icon.name: "settings-configure"
text: i18n("Room settings")
display: QQC2.AbstractButton.IconOnly
onClicked: QQC2.ApplicationWindow.window.pageStack.pushDialogLayer('qrc:/Categories.qml', {room: room}, { title: i18n("Room Settings") })
QQC2.ToolTip.text: text
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
QQC2.ToolTip.visible: hovered
}
}
}
QQC2.ScrollView {
Layout.fillWidth: true
Layout.fillHeight: true
// HACK: Hide unnecessary horizontal scrollbar (https://bugreports.qt.io/browse/QTBUG-83890)
QQC2.ScrollBar.horizontal.policy: QQC2.ScrollBar.AlwaysOff
RoomInformation {
id: roomInformation
room: root.room
}
}
}
}
}

View File

@@ -0,0 +1,60 @@
// 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
import QtQuick 2.15
import QtQuick.Controls 2.15 as QQC2
import QtQuick.Layouts 1.15
import org.kde.kirigami 2.20 as Kirigami
import org.kde.kirigamiaddons.delegates 1.0 as Delegates
import org.kde.kirigamiaddons.labs.components 1.0 as KirigamiComponents
import org.kde.kitemmodels 1.0
import org.kde.neochat 1.0
/**
* @brief Page for holding a room drawer component.
*
* This the companion component to RoomDrawer and is designed to be used on mobile
* where we want the room drawer to be pushed as a page as thin drawer doesn't
* look good.
*
* @sa RoomDrawer
*/
Kirigami.ScrollablePage {
id: root
/**
* @brief The current room that user is viewing.
*/
readonly property NeoChatRoom room: RoomManager.currentRoom
title: roomInformation.title
actions: [
Kirigami.Action {
icon.name: "settings-configure"
onTriggered: applicationWindow().pageStack.pushDialogLayer('qrc:/Categories.qml', {room: root.room}, { title: i18n("Room Settings") })
}
]
RoomInformation {
id: roomInformation
room: root.room
}
Connections {
target: applicationWindow().pageStack
onWideModeChanged: {
if (applicationWindow().pageStack.wideMode) {
console.log("widemode pop")
applicationWindow().pageStack.pop()
}
}
}
onBackRequested: event => {
event.accepted = true;
applicationWindow().pageStack.pop()
}
}

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