Compare commits

..

3 Commits

Author SHA1 Message Date
Carl Schwan
8177c1f1bc Port About page to FormCard 2023-08-22 22:03:35 +00:00
Carl Schwan
80171748d8 Port more MobileForm to FormCard
- DevTools
- Network settings
- Push notification
- Permissions
2023-08-22 22:03:35 +00:00
Carl Schwan
6aa2e586de Port to form card 2023-08-22 22:03:35 +00:00
119 changed files with 13128 additions and 19683 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 WebView)
find_package(Qt${QT_MAJOR_VERSION} ${QT_MIN_VERSION} NO_MODULE COMPONENTS Core Quick Gui QuickControls2 Multimedia Svg)
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" android:usesCleartextTraffic="true">
<application android:name="org.qtproject.qt5.android.bindings.QtApplication" android:label="NeoChat" android:icon="@drawable/neochat">
<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,7 +57,6 @@
<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>
@@ -268,6 +267,52 @@ 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>
@@ -277,77 +322,16 @@ 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">
<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.08.0" date="2023-08-24"/>
<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-27 00:46+0000\n"
"PO-Revision-Date: 2023-08-24 21:25+0200\n"
"POT-Creation-Date: 2023-08-21 00:46+0000\n"
"PO-Revision-Date: 2023-08-10 18:43+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:245
#: src/controller.cpp:233
#, kde-format
msgid "Login Failed: Access Token invalid or revoked"
msgstr "Feil ved innlogging: Ugyldig eller tilbaketrekt tilgangspollett"
#: src/controller.cpp:248 src/controller.cpp:253 src/login.cpp:90
#: src/controller.cpp:236 src/controller.cpp:241 src/login.cpp:90
#, kde-format
msgid "Login Failed: %1"
msgstr "Feil ved innlogging: %1"
#: src/controller.cpp:259
#: src/controller.cpp:247
#, kde-format
msgid "Network Error: %1"
msgstr "Nettverksfeil: %1"
#: src/controller.cpp:284
#: src/controller.cpp:272
#, kde-format
msgid "Access token wasn't found"
msgstr "Fann ikkje tilgangspollett"
#: src/controller.cpp:284
#: src/controller.cpp:272
#, kde-format
msgid "Maybe it was deleted?"
msgstr "Kanskje han er sletta?"
#: src/controller.cpp:288
#: src/controller.cpp:276
#, kde-format
msgid "Access to keychain was denied."
msgstr "Vart nekta tilgang til nøkkelring."
#: src/controller.cpp:288
#: src/controller.cpp:276
#, kde-format
msgid "Please allow NeoChat to read the access token"
msgstr "Gje NeoChat løyve til å lesa tilgangspolletten"
#: src/controller.cpp:291
#: src/controller.cpp:279
#, kde-format
msgid "No keychain available."
msgstr "Ingen nøkkelring er tilgjengeleg."
#: src/controller.cpp:291
#: src/controller.cpp:279
#, 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:294
#: src/controller.cpp:282
#, kde-format
msgid "Unable to read access token"
msgstr "Klarte ikkje lesa tilgangspollett"
#: src/controller.cpp:464
#: src/controller.cpp:452
#, kde-format
msgid "File too large to download."
msgstr "Fila er for stor til å kunna lastast ned."
#: src/controller.cpp:464
#: src/controller.cpp:452
#, kde-format
msgid "Contact your matrix server administrator for support."
msgstr "Ta kontakt med administratoren av Matrix-tenaren for brukarstøtte."
#: src/controller.cpp:503
#: src/controller.cpp:491
#, kde-format
msgid "Room creation failed: %1"
msgstr "Feil ved romregistrering: %1"
#: src/controller.cpp:524
#: src/controller.cpp:512
#, kde-format
msgid "Space creation failed: %1"
msgstr "Feil ved registrering av område: %1"
#: src/controller.cpp:538
#: src/controller.cpp:526
#, 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:334
#: src/main.cpp:333
#, kde-format
msgid "Client for the matrix communication protocol"
msgstr "Lynmeldings­klient for Matrix-protokollen"
#: src/main.cpp:335
#: src/main.cpp:334
#, kde-format
msgid "Supports matrix: url scheme"
msgstr "Støttar «matrix:»-adresser"
#: src/main.cpp:336
#: src/main.cpp:335
#, 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:24
#: src/models/actionsmodel.cpp:23
#, kde-format
msgid "Leaving this room."
msgstr "Forlèt rommet."
#: src/models/actionsmodel.cpp:31 src/models/actionsmodel.cpp:228
#: src/models/actionsmodel.cpp:254 src/models/actionsmodel.cpp:284
#: src/models/actionsmodel.cpp:30 src/models/actionsmodel.cpp:227
#: src/models/actionsmodel.cpp:253 src/models/actionsmodel.cpp:283
#, 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:39
#: src/models/actionsmodel.cpp:38
#, kde-format
msgctxt "Leaving room <roomname>."
msgid "Leaving room %1."
msgstr "Forlèt rom %1."
#: src/models/actionsmodel.cpp:42
#: src/models/actionsmodel.cpp:41
#, kde-format
msgctxt "Room <roomname> not found"
msgid "Room %1 not found."
msgstr "Fann ikkje rommet %1."
#: src/models/actionsmodel.cpp:50 src/models/actionsmodel.cpp:320
#: src/models/actionsmodel.cpp:49 src/models/actionsmodel.cpp:319
#, kde-format
msgid "No new nickname provided, no changes will happen."
msgstr "Nytt kallenamn ikkje oppgjeve. Ingen endringar vert gjort."
#: 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
#: 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
msgid "<message>"
msgstr "<melding>"
#: src/models/actionsmodel.cpp:66
#: src/models/actionsmodel.cpp:65
msgid "Prepends ¯\\_(ツ)_/¯ to a plain-text message"
msgstr "Legg til ¯\\_(ツ)_/¯ i starten av ei reintekstmelding"
#: src/models/actionsmodel.cpp:76
#: src/models/actionsmodel.cpp:75
msgid "Prepends ( ͡° ͜ʖ ͡°) to a plain-text message"
msgstr "Legg til ( ͡° ͜ʖ ͡°) i starten av ei reintekstmelding"
#: src/models/actionsmodel.cpp:86
#: src/models/actionsmodel.cpp:85
msgid "Prepends (╯°□°)╯︵ ┻━┻ to a plain-text message"
msgstr "Legg til (╯°□°)╯︵ ┻━┻ i starten av ei reintekstmelding"
#: src/models/actionsmodel.cpp:96
#: src/models/actionsmodel.cpp:95
msgid "Prepends ┬──┬ ( ゜-゜ノ) to a plain-text message"
msgstr "Legg til ┬──┬ ( ゜-゜ノ) i starten av ei reintekstmelding"
#: src/models/actionsmodel.cpp:116
#: src/models/actionsmodel.cpp:115
msgid "Sends the given message colored as a rainbow"
msgstr "Sender oppgjeven melding fargelagd som ein regnboge"
#: src/models/actionsmodel.cpp:136
#: src/models/actionsmodel.cpp:135
msgid "Sends the given emote colored as a rainbow"
msgstr "Sender oppgjeve uttrykk fargelagd som ein regnboge"
#: src/models/actionsmodel.cpp:147
#: src/models/actionsmodel.cpp:146
msgid "Sends the given message as plain text"
msgstr "Sender oppgjeven melding som reintekst"
#: src/models/actionsmodel.cpp:163
#: src/models/actionsmodel.cpp:162
msgid "Sends the given message as a spoiler"
msgstr "Sender oppgjeven melding med røpealarm"
#: src/models/actionsmodel.cpp:173
#: src/models/actionsmodel.cpp:172
msgid "Sends the given emote"
msgstr "Sender oppgjeve uttrykk"
#: src/models/actionsmodel.cpp:183
#: src/models/actionsmodel.cpp:182
msgid "Sends the given message as a notice"
msgstr "Sender oppgjeven melding som ei varsling"
#: 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
#: 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
#, 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:197
#: src/models/actionsmodel.cpp:196
#, 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:201
#: src/models/actionsmodel.cpp:200
#, 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:205
#: src/models/actionsmodel.cpp:204
#, kde-format
msgid "You are already in this room."
msgstr "Du er alt i dette rommet."
#: src/models/actionsmodel.cpp:209
#: src/models/actionsmodel.cpp:208
#, 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:213
#: src/models/actionsmodel.cpp:212
#, 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:218 src/models/actionsmodel.cpp:372
#: src/models/actionsmodel.cpp:400 src/models/actionsmodel.cpp:493
#: src/models/actionsmodel.cpp:217 src/models/actionsmodel.cpp:371
#: src/models/actionsmodel.cpp:399 src/models/actionsmodel.cpp:492
msgid "<user id>"
msgstr "<brukar-ID>"
#: src/models/actionsmodel.cpp:219
#: src/models/actionsmodel.cpp:218
msgid "Invites the user to this room"
msgstr "Inviterer brukaren til rommet"
#: src/models/actionsmodel.cpp:236 src/models/actionsmodel.cpp:291
#: src/models/actionsmodel.cpp:235 src/models/actionsmodel.cpp:290
#, kde-format
msgctxt "Joining room <roomname>."
msgid "Joining room %1."
msgstr "Vert med i rommet %1."
#: src/models/actionsmodel.cpp:242 src/models/actionsmodel.cpp:297
#: src/models/actionsmodel.cpp:241 src/models/actionsmodel.cpp:296
msgid "<room alias or id>"
msgstr "<rom-alias eller -ID>"
#: src/models/actionsmodel.cpp:243 src/models/actionsmodel.cpp:298
#: src/models/actionsmodel.cpp:242 src/models/actionsmodel.cpp:297
msgid "Joins the given room"
msgstr "Vert med i rommet"
#: src/models/actionsmodel.cpp:262
#: src/models/actionsmodel.cpp:261
#, kde-format
msgctxt "Knocking room <roomname>."
msgid "Knocking room %1."
msgstr "Bankar på rommet %1."
#: src/models/actionsmodel.cpp:274
#: src/models/actionsmodel.cpp:273
msgid "<room alias or id> [<reason>]"
msgstr "<rom-alias eller -id> [<grunngjeving>]"
#: src/models/actionsmodel.cpp:275
#: src/models/actionsmodel.cpp:274
msgid "Requests to join the given room"
msgstr "Førespurnad om å få verta med i rommet"
#: src/models/actionsmodel.cpp:288
#: src/models/actionsmodel.cpp:287
#, 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:305 src/models/actionsmodel.cpp:313
#: src/models/actionsmodel.cpp:304 src/models/actionsmodel.cpp:312
msgid "[<room alias or id>]"
msgstr "[<rom-alias eller -id>]"
#: src/models/actionsmodel.cpp:306 src/models/actionsmodel.cpp:314
#: src/models/actionsmodel.cpp:305 src/models/actionsmodel.cpp:313
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:328 src/models/actionsmodel.cpp:336
#: src/models/actionsmodel.cpp:344
#: src/models/actionsmodel.cpp:327 src/models/actionsmodel.cpp:335
#: src/models/actionsmodel.cpp:343
msgid "<display name>"
msgstr "<visingsnamn>"
#: src/models/actionsmodel.cpp:329
#: src/models/actionsmodel.cpp:328
msgid "Changes your global display name"
msgstr "Byter ditt globale visingsnamn"
#: src/models/actionsmodel.cpp:337 src/models/actionsmodel.cpp:345
#: src/models/actionsmodel.cpp:336 src/models/actionsmodel.cpp:344
msgid "Changes your display name in this room"
msgstr "Byter visingsnamnet ditt i dette rommet"
#: src/models/actionsmodel.cpp:359
#: src/models/actionsmodel.cpp:358
#, kde-format
msgctxt "<username> is already ignored."
msgid "%1 is already ignored."
msgstr "%1 er ignorert frå før."
#: src/models/actionsmodel.cpp:364
#: src/models/actionsmodel.cpp:363
#, kde-format
msgctxt "<username> is now ignored"
msgid "%1 is now ignored."
msgstr "%1 er no ignorert."
#: src/models/actionsmodel.cpp:366 src/models/actionsmodel.cpp:394
#: src/models/actionsmodel.cpp:365 src/models/actionsmodel.cpp:393
#, 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:373
#: src/models/actionsmodel.cpp:372
msgid "Ignores the given user"
msgstr "Ignorerer brukaren"
#: src/models/actionsmodel.cpp:388
#: src/models/actionsmodel.cpp:387
#, kde-format
msgctxt "<username> is not ignored."
msgid "%1 is not ignored."
msgstr "%1 er ikkje ignorert."
#: src/models/actionsmodel.cpp:392
#: src/models/actionsmodel.cpp:391
#, kde-format
msgctxt "<username> is no longer ignored."
msgid "%1 is no longer ignored."
msgstr "%1 er ikkje lenger ignorert."
#: src/models/actionsmodel.cpp:401
#: src/models/actionsmodel.cpp:400
msgid "Unignores the given user"
msgstr "Avignorerer brukaren"
#: src/models/actionsmodel.cpp:421
#: src/models/actionsmodel.cpp:420
msgid "<reaction text>"
msgstr "<reaksjonstekst>"
#: src/models/actionsmodel.cpp:422
#: src/models/actionsmodel.cpp:421
msgid "React to the message with the given text"
msgstr "Reager på meldinga med ein tekst"
#: src/models/actionsmodel.cpp:437
#: src/models/actionsmodel.cpp:436
#, 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:445
#: src/models/actionsmodel.cpp:444
#, 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:451
#: src/models/actionsmodel.cpp:450
#, 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:455
#: src/models/actionsmodel.cpp:454
#, 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:460 src/models/actionsmodel.cpp:537
#: src/models/actionsmodel.cpp:459 src/models/actionsmodel.cpp:536
msgid "<user id> [<reason>]"
msgstr "<brukar-ID> [<grunngjeving>]"
#: src/models/actionsmodel.cpp:461
#: src/models/actionsmodel.cpp:460
msgid "Bans the given user"
msgstr "Utestengjer brukaren"
#: src/models/actionsmodel.cpp:478
#: src/models/actionsmodel.cpp:477
#, 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:483
#: src/models/actionsmodel.cpp:482
#, 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:487
#: src/models/actionsmodel.cpp:486
#, 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:494
#: src/models/actionsmodel.cpp:493
msgid "Removes the ban of the given user"
msgstr "Opphevar utestenging av brukaren"
#: src/models/actionsmodel.cpp:509
#: src/models/actionsmodel.cpp:508
#, 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:513
#: src/models/actionsmodel.cpp:512
#, 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:522
#: src/models/actionsmodel.cpp:521
#, 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:528
#: src/models/actionsmodel.cpp:527
#, 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:532
#: src/models/actionsmodel.cpp:531
#, 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:538
#: src/models/actionsmodel.cpp:537
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:87
#: src/models/imagepacksmodel.cpp:86
#, kde-format
msgctxt "As in 'The user's own Stickers'"
msgid "Own Stickers"
msgstr "Eigne klistremerke"
#: src/models/imagepacksmodel.cpp:87
#: src/models/imagepacksmodel.cpp:86
#, 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:1631 src/neochatroom.cpp:1632
#: src/neochatroom.cpp:1627 src/neochatroom.cpp:1628
#, kde-format
msgid "Report sent successfully."
msgstr "Rapporten er no send."
#: src/neochatroom.cpp:1928 src/neochatroom.cpp:1936
#: src/neochatroom.cpp:1924 src/neochatroom.cpp:1932
#, 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:257
#: src/notificationsmanager.cpp:201 src/qml/main.qml:253
#, kde-format
msgid "%1: %2"
msgstr "%1: %2"
@@ -1362,8 +1362,7 @@ msgstr "Avvis"
msgid "Accept"
msgstr "Godta"
#: src/qml/Component/LocationPage.qml:17
#: src/qml/RoomDrawer/RoomInformation.qml:116
#: src/qml/Component/LocationPage.qml:17 src/qml/Panel/RoomDrawer.qml:200
#, kde-format
msgctxt "Locations on a map"
msgid "Locations"
@@ -1609,22 +1608,22 @@ msgstr "Lydstyrke"
msgid "Maximize"
msgstr "Maksimer"
#: src/qml/Component/TimelineView.qml:160
#: src/qml/Component/TimelineView.qml:153
#, kde-format
msgid "Jump to first unread message"
msgstr "Gå til første ulesne melding"
#: src/qml/Component/TimelineView.qml:183
#: src/qml/Component/TimelineView.qml:176
#, kde-format
msgid "Jump to latest message"
msgstr "Gå til nyaste melding"
#: src/qml/Component/TimelineView.qml:209
#: src/qml/Component/TimelineView.qml:202
#, kde-format
msgid "Drag items here to share them"
msgstr "Dra element her for å dela dei"
#: src/qml/Component/TimelineView.qml:235
#: src/qml/Component/TimelineView.qml:228
#, kde-format
msgctxt "Message displayed when some users are typing"
msgid "%2 is typing"
@@ -1957,80 +1956,80 @@ msgctxt "@title:menu Account detail dialog"
msgid "Account detail"
msgstr "Konto­detaljar"
#: src/qml/Dialog/UserDetailDialog.qml:81
#: src/qml/Dialog/UserDetailDialog.qml:80
#, kde-format
msgid "Unignore this user"
msgstr "Avignorer brukaren"
#: src/qml/Dialog/UserDetailDialog.qml:81
#: src/qml/Dialog/UserDetailDialog.qml:80
#, kde-format
msgid "Ignore this user"
msgstr "Ignorer brukaren"
#: src/qml/Dialog/UserDetailDialog.qml:93
#: src/qml/Dialog/UserDetailDialog.qml:92
#, kde-format
msgid "Kick this user"
msgstr "Kast ut brukaren"
#: src/qml/Dialog/UserDetailDialog.qml:106
#: src/qml/Dialog/UserDetailDialog.qml:105
#, kde-format
msgid "Invite this user"
msgstr "Inviter brukaren"
#: src/qml/Dialog/UserDetailDialog.qml:118
#: src/qml/Dialog/UserDetailDialog.qml:117
#, kde-format
msgid "Ban this user"
msgstr "Utesteng brukaren"
#: src/qml/Dialog/UserDetailDialog.qml:123
#: src/qml/Dialog/UserDetailDialog.qml:122
#, kde-format
msgctxt "@title"
msgid "Ban User"
msgstr "Utesteng brukar"
#: src/qml/Dialog/UserDetailDialog.qml:134
#: src/qml/Dialog/UserDetailDialog.qml:133
#, kde-format
msgid "Unban this user"
msgstr "Opphev utestenging av brukaren"
#: src/qml/Dialog/UserDetailDialog.qml:146
#: src/qml/Dialog/UserDetailDialog.qml:145
#, kde-format
msgid "Set user power level"
msgstr "Vel maktnivå for brukar"
#: src/qml/Dialog/UserDetailDialog.qml:170
#: src/qml/Dialog/UserDetailDialog.qml:169
#, kde-format
msgid "Remove recent messages by this user"
msgstr "Fjern nylege meldingar frå brukaren"
#: src/qml/Dialog/UserDetailDialog.qml:175
#: src/qml/Dialog/UserDetailDialog.qml:174
#, kde-format
msgctxt "@title"
msgid "Remove Messages"
msgstr "Fjern meldingar"
#: src/qml/Dialog/UserDetailDialog.qml:185
#: src/qml/Dialog/UserDetailDialog.qml:184
#, kde-format
msgid "Open a private chat"
msgstr "Start privat prat"
#: src/qml/Dialog/UserDetailDialog.qml:195
#: src/qml/Dialog/UserDetailDialog.qml:194
#, kde-format
msgid "Copy link"
msgstr "Kopier lenkje"
#: src/qml/main.qml:297
#: src/qml/main.qml:293
#, kde-format
msgctxt "@title:window"
msgid "Session Verification"
msgstr "Øktstadfesting"
#: src/qml/main.qml:309
#: src/qml/main.qml:305
#, kde-format
msgid "User consent"
msgstr "Brukar­samtykke"
#: src/qml/main.qml:314
#: src/qml/main.qml:310
#, kde-format
msgid ""
"Your homeserver requires you to agree to its terms and conditions before "
@@ -2039,17 +2038,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:319
#: src/qml/main.qml:315
#, kde-format
msgid "Open"
msgstr "Opna"
#: src/qml/main.qml:354
#: src/qml/main.qml:350
#, kde-format
msgid "Start a chat"
msgstr "Start prat"
#: src/qml/main.qml:356
#: src/qml/main.qml:352
#, kde-format
msgid "Do you want to start a chat with %1?"
msgstr "Vil du starta ein prat med %1?"
@@ -2227,25 +2226,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:18
#: src/qml/Menu/Timeline/BanSheet.qml:16
#, kde-format
msgid "Ban User"
msgstr "Utesteng brukar"
#: src/qml/Menu/Timeline/BanSheet.qml:22
#: src/qml/Menu/Timeline/BanSheet.qml:20
#, kde-format
msgid "Reason for banning this user"
msgstr "Grunngjeving for utestenging av brukaren"
#: src/qml/Menu/Timeline/BanSheet.qml:34
#: src/qml/Menu/Timeline/BanSheet.qml:32
#, kde-format
msgctxt "@action:button 'Ban' as in 'Ban this user'"
msgid "Ban"
msgstr "Utesteng"
#: src/qml/Menu/Timeline/BanSheet.qml:43
#: src/qml/Menu/Timeline/BanSheet.qml:41
#: src/qml/Menu/Timeline/RemoveSheet.qml:49
#: src/qml/Menu/Timeline/ReportSheet.qml:43 src/qml/Page/InviteUserPage.qml:24
#: src/qml/Menu/Timeline/ReportSheet.qml:41 src/qml/Page/InviteUserPage.qml:24
#, kde-format
msgctxt "@action"
msgid "Cancel"
@@ -2282,7 +2281,7 @@ msgstr "Fjern melding"
#: src/qml/Menu/Timeline/FileDelegateContextMenu.qml:83
#: src/qml/Menu/Timeline/MessageDelegateContextMenu.qml:59
#: src/qml/Menu/Timeline/ReportSheet.qml:34
#: src/qml/Menu/Timeline/ReportSheet.qml:32
#, kde-format
msgctxt ""
"@action:button 'Report' as in 'Report this event to the administrators'"
@@ -2349,17 +2348,17 @@ msgctxt "@action:button 'Remove' as in 'Remove this message'"
msgid "Remove"
msgstr "Fjern"
#: src/qml/Menu/Timeline/ReportSheet.qml:18
#: src/qml/Menu/Timeline/ReportSheet.qml:16
#, kde-format
msgid "Report Message"
msgstr "Rapporter melding"
#: src/qml/Menu/Timeline/ReportSheet.qml:22
#: src/qml/Menu/Timeline/ReportSheet.qml:20
#, kde-format
msgid "Reason for reporting this message"
msgstr "Grunngjeving for rapportering av meldinga"
#: src/qml/Page/DevtoolsPage.qml:17 src/qml/RoomDrawer/RoomInformation.qml:75
#: src/qml/Page/DevtoolsPage.qml:17 src/qml/Panel/RoomDrawer.qml:159
#, kde-format
msgid "Developer Tools"
msgstr "Utviklarverktøy"
@@ -2520,7 +2519,7 @@ msgstr "Vart med"
#, kde-format
msgctxt "@info:label"
msgid "No rooms found"
msgstr "Fann ingen rom"
msgstr ""
#: src/qml/Page/RoomList/AccountMenu.qml:18
#: src/qml/Page/RoomList/UserInfo.qml:175
@@ -2635,9 +2634,7 @@ 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/RoomDrawer/RoomDrawer.qml:98
#: src/qml/RoomDrawer/RoomDrawerPage.qml:37
#: src/qml/Page/RoomList/ContextMenu.qml:186 src/qml/Panel/RoomDrawer.qml:105
#, kde-format
msgid "Room Settings"
msgstr "Romval"
@@ -2649,7 +2646,7 @@ msgid "Leave Room"
msgstr "Forlat rommet"
#: src/qml/Page/RoomList/ExploreComponent.qml:19
#: src/qml/Page/RoomList/Page.qml:153
#: src/qml/Page/RoomList/Page.qml:154
#, kde-format
msgid "Explore rooms"
msgstr "Utforsk rom"
@@ -2660,28 +2657,28 @@ msgstr "Utforsk rom"
msgid "Create rooms and chats"
msgstr "Opprett rom og diskusjonar"
#: src/qml/Page/RoomList/Page.qml:150
#: src/qml/Page/RoomList/Page.qml:151
#, kde-format
msgid "No rooms found"
msgstr "Fann ingen rom"
#: src/qml/Page/RoomList/Page.qml:150
#: src/qml/Page/RoomList/Page.qml:151
#, kde-format
msgid "Join some rooms to get started"
msgstr "Start ved å verta med i nokre rom"
#: src/qml/Page/RoomList/Page.qml:153
#: src/qml/Page/RoomList/Page.qml:154
#, kde-format
msgid "Search in room directory"
msgstr "Søk i romkatalogen"
#: src/qml/Page/RoomList/Page.qml:195
#: src/qml/Page/RoomList/Page.qml:196
#, kde-format
msgctxt "Collapse <section name>"
msgid "Collapse %1"
msgstr "Fald saman %1"
#: src/qml/Page/RoomList/Page.qml:195
#: src/qml/Page/RoomList/Page.qml:196
#, kde-format
msgctxt "Expand <section name"
msgid "Expand %1"
@@ -2753,7 +2750,7 @@ msgstr "Byt brukar"
msgid "Open Settings"
msgstr "Opna innstillingar"
#: src/qml/Page/RoomPage.qml:50
#: src/qml/Page/RoomPage.qml:42
#, kde-format
msgid "NeoChat is offline. Please check your network connection."
msgstr "NeoChat er fråkopla. Sjå til at du er kopla til nettet."
@@ -2794,103 +2791,97 @@ msgstr "Start ny prat"
msgid "Welcome to Matrix"
msgstr "Velkommen til Matrix"
#: src/qml/RoomDrawer/GroupChatDrawerHeader.qml:62
#: src/qml/Panel/GroupChatDrawerHeader.qml:62
#, kde-format
msgid "No name"
msgstr "Namnlaus"
#: src/qml/RoomDrawer/GroupChatDrawerHeader.qml:71
#: src/qml/Panel/GroupChatDrawerHeader.qml:71
#, kde-format
msgid "No Canonical Alias"
msgstr "Manglar kanonisk alias"
#: src/qml/RoomDrawer/GroupChatDrawerHeader.qml:81
#: src/qml/Panel/GroupChatDrawerHeader.qml:81
#, kde-format
msgid "No Topic"
msgstr "Manglar emne"
#: src/qml/RoomDrawer/RoomDrawer.qml:88
#: src/qml/Panel/RoomDrawer.qml:95
#, kde-format
msgid "Room information"
msgstr "Rominformasjon"
#: src/qml/RoomDrawer/RoomDrawer.qml:95
#: src/qml/Panel/RoomDrawer.qml:102
#, 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/RoomDrawer/RoomInformation.qml:59
#: src/qml/Panel/RoomDrawer.qml:143
#, kde-format
msgid "Options"
msgstr "Handlingar"
#: src/qml/RoomDrawer/RoomInformation.qml:69
#: src/qml/Panel/RoomDrawer.qml:153
#, kde-format
msgid "Open developer tools"
msgstr "Opna utviklarverktøy"
#: src/qml/RoomDrawer/RoomInformation.qml:83
#: src/qml/Panel/RoomDrawer.qml:167
#, kde-format
msgid "Search in this room"
msgstr "Søk i rommet"
#: src/qml/RoomDrawer/RoomInformation.qml:91
#: src/qml/Panel/RoomDrawer.qml:175
#, kde-format
msgctxt "@action:title"
msgid "Search"
msgstr "Søk"
#: src/qml/RoomDrawer/RoomInformation.qml:100
#: src/qml/Panel/RoomDrawer.qml:184
#, kde-format
msgid "Remove room from favorites"
msgstr "Fjern rommet frå favorittar"
#: src/qml/RoomDrawer/RoomInformation.qml:100
#: src/qml/Panel/RoomDrawer.qml:184
#, kde-format
msgid "Make room favorite"
msgstr "Gjer rommet til favoritt"
#: src/qml/RoomDrawer/RoomInformation.qml:111
#: src/qml/Panel/RoomDrawer.qml:195
#, kde-format
msgid "Show locations for this room"
msgstr "Vis posisjonar i rommet"
#: src/qml/RoomDrawer/RoomInformation.qml:123
#: src/qml/Panel/RoomDrawer.qml:207
#, kde-format
msgid "Members"
msgstr "Medlemmar"
#: src/qml/RoomDrawer/RoomInformation.qml:134
#: src/qml/Panel/RoomDrawer.qml:218
#, kde-format
msgid "Search user in room"
msgstr "Søk etter brukarar i rommet"
#: src/qml/RoomDrawer/RoomInformation.qml:147
#: src/qml/Panel/RoomDrawer.qml:231
#, kde-format
msgctxt "@title"
msgid "Invite a User"
msgstr "Inviter ein brukar"
#: src/qml/RoomDrawer/RoomInformation.qml:150
#: src/qml/Panel/RoomDrawer.qml:234
#, kde-format
msgid "Invite user to room"
msgstr "Inviter brukar til rommet"
#: src/qml/RoomDrawer/RoomInformation.qml:157
#: src/qml/Panel/RoomDrawer.qml:241
#, kde-format
msgid "%1 member"
msgid_plural "%1 members"
msgstr[0] "%1 medlem"
msgstr[1] "%1 medlemmar"
#: src/qml/RoomDrawer/RoomInformation.qml:157
#: src/qml/Panel/RoomDrawer.qml:241
#, 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,6 +74,8 @@ add_library(neochat STATIC
blurhash.h
blurhashimageprovider.cpp
blurhashimageprovider.h
models/collapsestateproxymodel.cpp
models/collapsestateproxymodel.h
models/mediamessagefiltermodel.cpp
models/mediamessagefiltermodel.h
urlhelper.cpp
@@ -123,9 +125,6 @@ add_library(neochat STATIC
events/pollevent.cpp
pollhandler.cpp
utils.h
registration.cpp
neochatconnection.cpp
neochatconnection.h
)
ecm_qt_declare_logging_category(neochat
@@ -142,11 +141,6 @@ 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
@@ -183,7 +177,6 @@ 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,6 +21,7 @@
#include <QFile>
#include <QFileInfo>
#include <QGuiApplication>
#include <QImageReader>
#include <QNetworkProxy>
#include <QQuickTextDocument>
#include <QQuickWindow>
@@ -32,8 +33,10 @@
#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>
@@ -102,8 +105,8 @@ Controller::Controller(QObject *parent)
static int oldAccountCount = 0;
connect(&m_accountRegistry, &AccountRegistry::accountCountChanged, this, [this]() {
if (m_accountRegistry.size() > oldAccountCount) {
auto connection = dynamic_cast<NeoChatConnection *>(m_accountRegistry.accounts()[m_accountRegistry.size() - 1]);
connect(connection, &NeoChatConnection::syncDone, this, [connection]() {
auto connection = m_accountRegistry.accounts()[m_accountRegistry.size() - 1];
connect(connection, &Connection::syncDone, this, [connection]() {
NotificationsManager::instance().handleNotifications(connection);
});
}
@@ -138,7 +141,38 @@ void Controller::toggleWindow()
}
}
void Controller::addConnection(NeoChatConnection *c)
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)
{
Q_ASSERT_X(c, __FUNCTION__, "Attempt to add a null connection");
@@ -146,17 +180,17 @@ void Controller::addConnection(NeoChatConnection *c)
c->setLazyLoading(true);
connect(c, &NeoChatConnection::syncDone, this, [this, c] {
connect(c, &Connection::syncDone, this, [this, c] {
Q_EMIT syncDone();
c->sync(30000);
c->saveState();
});
connect(c, &NeoChatConnection::loggedOut, this, [this, c] {
connect(c, &Connection::loggedOut, this, [this, c] {
dropConnection(c);
});
connect(c, &NeoChatConnection::requestFailed, this, [this](BaseJob *job) {
connect(c, &Connection::requestFailed, this, [this](BaseJob *job) {
if (job->error() == BaseJob::UserConsentRequired) {
Q_EMIT userConsentRequired(job->errorUrl());
}
@@ -168,7 +202,7 @@ void Controller::addConnection(NeoChatConnection *c)
Q_EMIT accountCountChanged();
}
void Controller::dropConnection(NeoChatConnection *c)
void Controller::dropConnection(Connection *c)
{
Q_ASSERT_X(c, __FUNCTION__, "Attempt to drop a null connection");
@@ -197,19 +231,19 @@ void Controller::invokeLogin()
return;
}
auto connection = new NeoChatConnection(account.homeserver());
connect(connection, &NeoChatConnection::connected, this, [this, connection, id] {
auto connection = new Connection(account.homeserver());
connect(connection, &Connection::connected, this, [this, connection, id] {
connection->loadState();
addConnection(connection);
if (connection->userId() == id) {
setActiveConnection(connection);
connectSingleShot(connection, &NeoChatConnection::syncDone, this, &Controller::initiated);
connectSingleShot(connection, &Connection::syncDone, this, &Controller::initiated);
}
});
connect(connection, &NeoChatConnection::loginError, this, [this, connection](const QString &error, const QString &) {
connect(connection, &Connection::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"));
connection->logout(false);
logout(connection, 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
@@ -217,11 +251,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));
connection->logout(true);
logout(connection, true);
}
Q_EMIT initiated();
});
connect(connection, &NeoChatConnection::networkError, this, [this](const QString &error, const QString &, int, int) {
connect(connection, &Connection::networkError, this, [this](const QString &error, const QString &, int, int) {
Q_EMIT errorOccured(i18n("Network Error: %1", error));
});
connection->assumeIdentity(account.userId(), accessToken);
@@ -287,6 +321,22 @@ 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
@@ -297,6 +347,49 @@ 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")
{
@@ -307,14 +400,6 @@ 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();
@@ -340,7 +425,7 @@ void Controller::setQuitOnLastWindowClosed()
#endif
}
NeoChatConnection *Controller::activeConnection() const
Connection *Controller::activeConnection() const
{
if (m_connection.isNull()) {
return nullptr;
@@ -348,43 +433,49 @@ NeoChatConnection *Controller::activeConnection() const
return m_connection;
}
void Controller::setActiveConnection(NeoChatConnection *connection)
void Controller::setActiveConnection(Connection *connection)
{
if (connection == m_connection) {
return;
}
if (m_connection != nullptr) {
disconnect(m_connection, &NeoChatConnection::syncError, this, nullptr);
disconnect(m_connection, &NeoChatConnection::accountDataChanged, this, nullptr);
disconnect(m_connection, &Connection::syncError, this, nullptr);
disconnect(m_connection, &Connection::accountDataChanged, this, nullptr);
}
m_connection = connection;
if (connection != nullptr) {
NeoChatConfig::self()->setActiveConnection(connection->userId());
connect(connection, &NeoChatConnection::networkError, this, [this]() {
connect(connection, &Connection::networkError, this, [this]() {
if (!m_isOnline) {
return;
}
m_isOnline = false;
Q_EMIT isOnlineChanged(false);
});
connect(connection, &NeoChatConnection::syncDone, this, [this] {
connect(connection, &Connection::syncDone, this, [this] {
if (m_isOnline) {
return;
}
m_isOnline = true;
Q_EMIT isOnlineChanged(true);
});
connect(connection, &NeoChatConnection::requestFailed, this, [](BaseJob *job) {
connect(connection, &Connection::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
@@ -555,6 +646,41 @@ 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,7 +9,6 @@
#include <KFormat>
#include "neochatconnection.h"
#include <Quotient/accountregistry.h>
#include <Quotient/jobs/basejob.h>
#include <Quotient/settings.h>
@@ -21,6 +20,7 @@ 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(NeoChatConnection *activeConnection READ activeConnection WRITE setActiveConnection NOTIFY activeConnectionChanged)
Q_PROPERTY(Quotient::Connection *activeConnection READ activeConnection WRITE setActiveConnection NOTIFY activeConnectionChanged)
/**
* @brief The PushRuleModel that has the active connection's push rules.
@@ -62,6 +62,16 @@ 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.
*/
@@ -99,28 +109,46 @@ public:
[[nodiscard]] int accountCount() const;
void setActiveConnection(NeoChatConnection *connection);
[[nodiscard]] NeoChatConnection *activeConnection() const;
void setActiveConnection(Quotient::Connection *connection);
[[nodiscard]] Quotient::Connection *activeConnection() const;
[[nodiscard]] PushRuleModel *pushRuleModel() const;
/**
* @brief Add a new connection to the account registry.
*/
void addConnection(NeoChatConnection *c);
void addConnection(Quotient::Connection *c);
/**
* @brief Drop a connection from the account registry.
*/
void dropConnection(NeoChatConnection *c);
void dropConnection(Quotient::Connection *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.
*/
@@ -182,12 +210,14 @@ 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<NeoChatConnection> m_connection;
QPointer<Quotient::Connection> m_connection;
TrayIcon *m_trayIcon = nullptr;
QKeychain::ReadPasswordJob *loadAccessTokenFromKeyChain(const Quotient::AccountSettings &account);
@@ -214,8 +244,8 @@ Q_SIGNALS:
/// Error occurred because of server or bug in NeoChat
void globalErrorOccured(QString error, QString detail);
void syncDone();
void connectionAdded(NeoChatConnection *connection);
void connectionDropped(NeoChatConnection *connection);
void connectionAdded(Quotient::Connection *_t1);
void connectionDropped(Quotient::Connection *_t1);
void accountCountChanged();
void initiated();
void notificationClicked(const QString &_t1, const QString &_t2);
@@ -226,14 +256,18 @@ Q_SIGNALS:
void userConsentRequired(QUrl url);
void testConnectionResult(const QString &connection, bool usable);
void isOnlineChanged(bool isOnline);
void keyVerificationRequest(int timeLeft, NeoChatConnection *connection, const QString &transactionId, const QString &deviceId);
void keyVerificationRequest(int timeLeft, Quotient::Connection *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();
};
@@ -249,9 +283,3 @@ 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::round(m * m_parentWidth + c);
int calcPercentWidth = std::ceil(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::round(absoluteWidth);
return std::ceil(absoluteWidth);
} else {
return std::round(std::min(absoluteWidth, m_maxWidth));
return std::ceil(std::min(absoluteWidth, m_maxWidth));
}
}

View File

@@ -8,7 +8,6 @@
#include <Quotient/connection.h>
#include <Quotient/csapi/content-repo.h>
#include "neochatconfig.h"
#include "neochatroom.h"
using namespace Quotient;
@@ -20,10 +19,6 @@ 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
@@ -62,9 +57,6 @@ 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 NeoChatConnection();
m_connection = new Connection();
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 NeoChatConnection();
m_connection = new Connection();
}
m_connection->resolveServer(m_matrixId);
connectSingleShot(m_connection, &Connection::loginFlowsChanged, this, [this]() {
@@ -87,11 +87,7 @@ void Login::init()
Q_EMIT isLoggingInChanged();
});
connect(m_connection, &Connection::loginError, this, [this](QString error, const QString &) {
if (error == QStringLiteral("Invalid username or password")) {
setInvalidPassword(true);
} else {
Q_EMIT errorOccured(i18n("Login Failed: %1", error));
}
Q_EMIT errorOccured(i18n("Login Failed: %1", error));
m_isLoggingIn = false;
Q_EMIT isLoggingInChanged();
});
@@ -137,7 +133,6 @@ QString Login::password() const
void Login::setPassword(const QString &password)
{
setInvalidPassword(false);
m_password = password;
Q_EMIT passwordChanged();
}
@@ -204,15 +199,4 @@ 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,7 +6,10 @@
#include <QObject>
#include <QUrl>
class NeoChatConnection;
namespace Quotient
{
class Connection;
}
/**
* @class Login
@@ -70,11 +73,6 @@ 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);
@@ -102,9 +100,6 @@ public:
bool isLoggedIn() const;
bool isInvalidPassword() const;
void setInvalidPassword(bool invalid);
Q_INVOKABLE void login();
Q_INVOKABLE void loginWithSso();
@@ -121,7 +116,6 @@ Q_SIGNALS:
void testingChanged();
void isLoggingInChanged();
void isLoggedInChanged();
void isInvalidPasswordChanged();
private:
void setHomeserverReachable(bool reachable);
@@ -132,10 +126,9 @@ private:
QString m_deviceName;
bool m_supportsSso = false;
bool m_supportsPassword = false;
NeoChatConnection *m_connection = nullptr;
Quotient::Connection *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,10 +18,6 @@
#include <QApplication>
#endif
#ifdef HAVE_WEBVIEW
#include <QtWebView>
#endif
#include <KAboutData>
#ifdef HAVE_KDBUSADDONS
#include <KDBusService>
@@ -54,6 +50,7 @@
#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"
@@ -80,7 +77,6 @@
#include "models/userlistmodel.h"
#include "models/webshortcutmodel.h"
#include "neochatconfig.h"
#include "neochatconnection.h"
#include "neochatroom.h"
#include "notificationsmanager.h"
#include "pollhandler.h"
@@ -99,7 +95,6 @@
#include "runner.h"
#include <QDBusConnection>
#endif
#include "registration.h"
#ifdef Q_OS_WINDOWS
#include <Windows.h>
@@ -144,10 +139,6 @@ 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"));
@@ -242,7 +233,6 @@ 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");
@@ -250,6 +240,7 @@ 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");
@@ -282,12 +273,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

@@ -0,0 +1,177 @@
// 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

@@ -0,0 +1,80 @@
// 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().size() > 1 && !text()[1].isUpper()
} else if (text().startsWith(QLatin1Char(':')) && !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/messagefiltermodel.h"
#include "models/collapsestateproxymodel.h"
/**
* @class MediaMessageFilterModel
@@ -22,7 +22,7 @@ public:
* @brief Defines the model roles.
*/
enum Roles {
SourceRole = MessageFilterModel::LastRole + 1, /**< The mxc source URL for the item. */
SourceRole = CollapseStateProxyModel::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,8 +3,6 @@
#include "messagefiltermodel.h"
#include <KLocalizedString>
#include "messageeventmodel.h"
#include "neochatconfig.h"
@@ -34,192 +32,22 @@ 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) {
if (eventType == MessageEventModel::Other) {
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,79 +5,21 @@
#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, QString::number(reaction.authors.count()));
return QStringLiteral("%1 %2").arg(reaction.reaction, reaction.authors.count());
} else {
return reaction.reaction;
}

View File

@@ -26,8 +26,7 @@ QVariant StateModel::data(const QModelIndex &index, int role) const
int StateModel::rowCount(const QModelIndex &parent) const
{
Q_UNUSED(parent);
return m_room->currentState().events().size();
return !m_room || parent.isValid() ? 0 : m_room->currentState().events().size();
}
NeoChatRoom *StateModel::room() const
@@ -37,8 +36,12 @@ NeoChatRoom *StateModel::room() const
void StateModel::setRoom(NeoChatRoom *room)
{
if (m_room == room) {
return;
}
m_room = room;
Q_EMIT roomChanged();
beginResetModel();
m_stateEvents.clear();
m_stateEvents = m_room->currentState().events().keys();

View File

@@ -1,155 +0,0 @@
// 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);
});
}
});
}

View File

@@ -1,53 +0,0 @@
// 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, displayNameForHtml(), htmlSafeMemberName(senderId), avatar_image);
NotificationsManager::instance().postInviteNotification(this, htmlSafeDisplayName(), htmlSafeMemberName(senderId), avatar_image);
});
connect(this, &Room::changed, this, [this] {
Q_EMIT canEncryptRoomChanged();
@@ -978,6 +978,11 @@ 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,6 +111,11 @@ 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.
*/
@@ -586,6 +591,8 @@ 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<NeoChatConnection> connection)
void NotificationsManager::handleNotifications(QPointer<Connection> connection)
{
if (!m_connActiveJob.contains(connection->user()->id())) {
auto job = connection->callApi<GetNotificationsJob>();
@@ -49,7 +49,7 @@ void NotificationsManager::handleNotifications(QPointer<NeoChatConnection> conne
}
}
void NotificationsManager::processNotificationJob(QPointer<NeoChatConnection> connection, Quotient::GetNotificationsJob *job, bool initialization)
void NotificationsManager::processNotificationJob(QPointer<Quotient::Connection> connection, Quotient::GetNotificationsJob *job, bool initialization)
{
if (job == nullptr) {
return;
@@ -145,7 +145,7 @@ void NotificationsManager::processNotificationJob(QPointer<NeoChatConnection> co
}
}
bool NotificationsManager::shouldPostNotification(QPointer<NeoChatConnection> connection, const QJsonValue &notification)
bool NotificationsManager::shouldPostNotification(QPointer<Quotient::Connection> 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(dynamic_cast<NeoChatConnection *>(Controller::instance().accounts().get(room->localUser()->id())));
Controller::instance().setActiveConnection(Controller::instance().accounts().get(room->localUser()->id()));
}
RoomManager::instance().enterRoom(room);
});

View File

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

View File

@@ -6,107 +6,122 @@ import QtQuick.Controls 2.15 as QQC2
import QtQuick.Layouts 1.15
import org.kde.kirigami 2.15 as Kirigami
import org.kde.kirigamiaddons.labs.mobileform 0.1 as MobileForm
import org.kde.kirigamiaddons.formcard 1.0 as FormCard
import org.kde.kitemmodels 1.0
import org.kde.neochat 1.0
ColumnLayout {
MobileForm.FormCard {
Layout.fillWidth: true
contentItem: ColumnLayout {
spacing: 0
MobileForm.FormComboBoxDelegate {
text: i18n("Room")
textRole: "displayName"
valueRole: "id"
model: RoomListModel {
id: roomListModel
connection: Controller.activeConnection
}
Component.onCompleted: currentIndex = indexOfValue(room.id)
onCurrentValueChanged: room = roomListModel.roomByAliasOrId(currentValue)
FormCard.FormCardPage {
id: root
required property var room
FormCard.FormCard {
Layout.topMargin: Kirigami.Units.gridUnit
FormCard.FormComboBoxDelegate {
id: roomChooser
text: i18n("Room")
textRole: "displayName"
valueRole: "roomId"
model: RoomListModel {
id: roomListModel
connection: Controller.activeConnection
}
MobileForm.FormCheckDelegate {
text: i18n("Show m.room.member events")
checked: true
onToggled: {
if (checked) {
stateEventFilterModel.removeStateEventTypeFiltered("m.room.member");
} else {
stateEventFilterModel.addStateEventTypeFiltered("m.room.member");
}
onCurrentValueChanged: room = roomListModel.roomByAliasOrId(currentValue)
Component.onCompleted: currentIndex = indexOfValue(room.id)
}
FormCard.FormDelegateSeparator { above: showRoomMember }
FormCard.FormCheckDelegate {
id: showRoomMember
text: i18n("Show m.room.member events")
checked: true
onToggled: {
if (checked) {
stateEventFilterModel.removeStateEventTypeFiltered("m.room.member");
} else {
stateEventFilterModel.addStateEventTypeFiltered("m.room.member");
}
}
MobileForm.FormCheckDelegate {
id: roomAccoutnDataVisibleCheck
text: i18n("Show room account data")
checked: false
}
}
FormCard.FormDelegateSeparator { above: roomAccoutnDataVisibleCheck; below: showRoomMember }
FormCard.FormCheckDelegate {
id: roomAccoutnDataVisibleCheck
text: i18n("Show room account data")
checked: false
}
FormCard.FormDelegateSeparator { below: roomAccoutnDataVisibleCheck }
FormCard.FormTextDelegate {
text: i18n("Room id")
description: room.id
}
}
MobileForm.FormCard {
Layout.fillWidth: true
FormCard.FormHeader {
title: i18n("Room Account Data for %1", room.displayName)
visible: roomAccoutnDataVisibleCheck.checked
contentItem: ColumnLayout {
spacing: 0
MobileForm.FormCardHeader {
title: i18n("Room Account Data for %1 - %2", room.displayName, room.id)
}
}
Repeater {
model: room.accountDataEventTypes
delegate: MobileForm.FormTextDelegate {
text: modelData
onClicked: applicationWindow().pageStack.pushDialogLayer("qrc:/MessageSourceSheet.qml", {
"sourceText": room.roomAcountDataJson(text)
}, {
"title": i18n("Event Source"),
"width": Kirigami.Units.gridUnit * 25
})
}
FormCard.FormCard {
visible: roomAccoutnDataVisibleCheck.checked
Repeater {
model: room.accountDataEventTypes
delegate: FormCard.FormTextDelegate {
text: modelData
onClicked: applicationWindow().pageStack.pushDialogLayer("qrc:/MessageSourceSheet.qml", {
"sourceText": room.roomAcountDataJson(text)
}, {
"title": i18n("Event Source"),
"width": Kirigami.Units.gridUnit * 25
})
}
}
}
MobileForm.FormCard {
Layout.fillWidth: true
FormCard.FormHeader {
id: stateEventListHeader
title: i18n("Room State for %1", room.displayName)
}
FormCard.FormCard {
Layout.fillHeight: true
contentItem: ColumnLayout {
spacing: 0
MobileForm.FormCardHeader {
id: stateEventListHeader
title: i18n("Room State for %1", room.displayName)
subtitle: room.id
}
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
QQC2.ScrollView {
Layout.fillWidth: true
Layout.fillHeight: true
Layout.maximumHeight: Kirigami.Units.gridUnit * 20
ListView {
id: stateEventListView
clip: true
// HACK: Hide unnecessary horizontal scrollbar (https://bugreports.qt.io/browse/QTBUG-83890)
QQC2.ScrollBar.horizontal.policy: QQC2.ScrollBar.AlwaysOff
model: StateFilterModel {
id: stateEventFilterModel
sourceModel: StateModel {
id: stateModel
room: devtoolsPage.room
}
ListView {
id: stateEventListView
clip: true
model: StateFilterModel {
id: stateEventFilterModel
sourceModel: StateModel {
id: stateModel
room: root.room
}
}
delegate: MobileForm.FormTextDelegate {
text: model.type
description: model.stateKey
onClicked: applicationWindow().pageStack.pushDialogLayer('qrc:/MessageSourceSheet.qml', {
sourceText: stateModel.stateEventJson(stateEventFilterModel.mapToSource(stateEventFilterModel.index(model.index, 0)))
}, {
title: i18n("Event Source"),
width: Kirigami.Units.gridUnit * 25
});
}
delegate: FormCard.FormTextDelegate {
text: model.type
description: model.stateKey
onClicked: applicationWindow().pageStack.pushDialogLayer('qrc:/MessageSourceSheet.qml', {
sourceText: stateModel.stateEventJson(stateEventFilterModel.mapToSource(stateEventFilterModel.index(model.index, 0)))
}, {
title: i18n("Event Source"),
width: Kirigami.Units.gridUnit * 25
});
}
}
}

View File

@@ -6,58 +6,50 @@ import QtQuick.Controls 2.15 as QQC2
import QtQuick.Layouts 1.15
import org.kde.kirigami 2.15 as Kirigami
import org.kde.kirigamiaddons.labs.mobileform 0.1 as MobileForm
import org.kde.kirigamiaddons.formcard 1.0 as FormCard
import org.kde.kitemmodels 1.0
import org.kde.neochat 1.0
ColumnLayout {
MobileForm.FormCard {
Layout.fillWidth: true
contentItem: ColumnLayout {
spacing: 0
MobileForm.FormCardHeader {
title: i18n("Server Capabilities")
}
MobileForm.FormTextDelegate {
text: i18n("Can change password")
description: Controller.activeConnection.canChangePassword
}
}
}
MobileForm.FormCard {
Layout.fillWidth: true
contentItem: ColumnLayout {
spacing: 0
MobileForm.FormCardHeader {
title: i18n("Default Room Version")
}
MobileForm.FormTextDelegate {
text: Controller.activeConnection.defaultRoomVersion
}
}
}
MobileForm.FormCard {
Layout.fillWidth: true
contentItem: ColumnLayout {
spacing: 0
MobileForm.FormCardHeader {
title: i18n("Available Room Versions")
}
Repeater {
model: Controller.getSupportedRoomVersions(room.connection)
FormCard.FormCardPage {
id: root
delegate: MobileForm.FormTextDelegate {
text: modelData.id
contentItem.children: QQC2.Label {
text: modelData.status
color: Kirigami.Theme.disabledTextColor
}
FormCard.FormHeader {
title: i18n("Server Capabilities")
}
FormCard.FormCard {
FormCard.FormTextDelegate {
text: i18n("Can change password")
description: Controller.activeConnection.canChangePassword
}
}
FormCard.FormHeader {
title: i18n("Default Room Version")
}
FormCard.FormCard {
FormCard.FormTextDelegate {
text: Controller.activeConnection.defaultRoomVersion
}
}
FormCard.FormHeader {
title: i18n("Available Room Versions")
}
FormCard.FormCard {
Repeater {
model: Controller.getSupportedRoomVersions(room.connection)
delegate: FormCard.FormTextDelegate {
text: modelData.id
contentItem.children: QQC2.Label {
text: modelData.status
color: Kirigami.Theme.disabledTextColor
}
}
}
}
Item {
Layout.fillHeight: true
}
}

View File

@@ -1,49 +0,0 @@
// 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

@@ -1,59 +0,0 @@
// 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: 2023 Tobias Fella <tobias.fella@kde.org>
// SPDX-FileCopyrightText: 2020 Tobias Fella <tobias.fella@kde.org>
// SPDX-License-Identifier: GPL-2.0-or-later
import QtQuick 2.15
@@ -6,42 +6,56 @@ 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
onActiveFocusChanged: if (activeFocus) urlField.forceActiveFocus()
readonly property var homeserver: customHomeserver.visible ? customHomeserver.text : serverCombo.currentText
property bool loading: false
FormCard.FormTextFieldDelegate {
id: urlField
label: i18n("Server Url:")
validator: RegularExpressionValidator {
regularExpression: /([a-zA-Z0-9\-]+\.)*[a-zA-Z0-9]+(:[0-9]+)?/
title: i18nc("@title", "Select a Homeserver")
action: Kirigami.Action {
enabled: LoginHelper.homeserverReachable && !customHomeserver.visible || customHomeserver.acceptableInput
onTriggered: {
// TODO
console.log("register todo")
}
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)
}
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]+)?/
}
}
}
Timer {
id: timer
interval: 500
onTriggered: Registration.homeserver = urlField.text
}
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")
QQC2.Button {
id: continueButton
text: i18nc("@action:button", "Continue")
action: root.action
}
}
}

View File

@@ -6,19 +6,16 @@ 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
LoginStep {
id: root
FormCard.FormTextDelegate {
Kirigami.LoadingPlaceholder {
property var showContinueButton: false
property var showBackButton: false
QQC2.Label {
text: i18n("Please wait. This might take a little while.")
}
FormCard.AbstractFormDelegate {
contentItem: QQC2.BusyIndicator {}
background: null
}
Connections {
target: Controller

View File

@@ -7,34 +7,43 @@ 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
id: login
onActiveFocusChanged: if (activeFocus) matrixIdField.forceActiveFocus()
showContinueButton: true
showBackButton: false
title: i18nc("@title", "Login")
message: i18n("Enter your Matrix ID")
Component.onCompleted: {
LoginHelper.matrixId = ""
}
FormCard.FormTextFieldDelegate {
id: matrixIdField
label: i18n("Matrix ID:")
placeholderText: "@user:example.org"
Accessible.name: i18n("Matrix ID")
onTextChanged: {
LoginHelper.matrixId = text
}
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
}
Keys.onReturnPressed: {
root.nextAction.trigger()
Component.onCompleted: {
matrixIdField.forceActiveFocus()
}
Keys.onReturnPressed: {
login.action.trigger()
}
}
}
nextAction: Kirigami.Action {
action: 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) {
@@ -47,9 +56,4 @@ LoginStep {
}
enabled: LoginHelper.homeserverReachable
}
previousAction: Kirigami.Action {
onTriggered: {
root.processed("qrc:/LoginRegister.qml")
}
}
}

View File

@@ -4,26 +4,28 @@
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: root
id: loginMethod
onActiveFocusChanged: if (activeFocus) loginPasswordButton.forceActiveFocus()
title: i18n("Login Methods")
FormCard.FormButtonDelegate {
id: loginPasswordButton
text: i18nc("@action:button", "Login with password")
Layout.alignment: Qt.AlignHCenter
QQC2.Button {
Layout.alignment: Qt.AlignHCenter
text: i18n("Login with password")
Layout.preferredWidth: Kirigami.Units.gridUnit * 12
onClicked: processed("qrc:/Password.qml")
}
FormCard.FormButtonDelegate {
id: loginSsoButton
text: i18nc("@action:button", "Login with single sign-on")
QQC2.Button {
Layout.alignment: Qt.AlignHCenter
text: i18n("Login with single sign-on")
Layout.preferredWidth: Kirigami.Units.gridUnit * 12
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: root
id: loginRegister
onActiveFocusChanged: if (activeFocus) loginButton.forceActiveFocus()
Layout.alignment: Qt.AlignHCenter
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("Login")
Layout.preferredWidth: Kirigami.Units.gridUnit * 12
onClicked: processed("qrc:/Login.qml")
}
FormCard.FormButtonDelegate {
text: i18nc("@action:button", "Register")
onClicked: root.processed("qrc:/Homeserver.qml")
QQC2.Button {
Layout.alignment: Qt.AlignHCenter
text: i18n("Register")
Layout.preferredWidth: Kirigami.Units.gridUnit * 12
onClicked: processed("qrc:/Homeserver.qml")
}
}

View File

@@ -7,25 +7,21 @@ import QtQuick.Layouts 1.14
/// Step for the login/registration flow
ColumnLayout {
id: root
/// 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
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: ""
/// Process this module, this is called by the continue button.
/// Should call \sa processed when it finish successfully.
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
property QQC2.Action action: 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,12 +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: root
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();
}
}
Connections {
target: LoginHelper
@@ -20,32 +33,20 @@ LoginStep {
}
}
onActiveFocusChanged: if(activeFocus) passwordField.forceActiveFocus()
Kirigami.FormLayout {
Kirigami.PasswordField {
id: passwordField
onTextChanged: LoginHelper.password = text
enabled: !LoginHelper.isLoggingIn
Accessible.name: i18n("Password")
FormCard.FormTextFieldDelegate {
id: passwordField
Component.onCompleted: {
passwordField.forceActiveFocus()
}
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()
Keys.onReturnPressed: {
password.action.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

@@ -1,51 +0,0 @@
// 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,38 +6,47 @@ 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
noControls: true
title: i18nc("@title", "Login")
message: i18n("Login with single sign-on")
Component.onCompleted: LoginHelper.loginWithSso()
Connections {
target: LoginHelper
function onSsoUrlChanged() {
UrlHelper.openUrl(LoginHelper.ssoUrl)
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")
}
}
function onConnected() {
processed("qrc:/Loading.qml")
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()
}
}
}
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

@@ -1,39 +0,0 @@
// 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

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

View File

@@ -375,7 +375,10 @@ ColumnLayout {
MouseArea {
anchors.fill: parent
onClicked: {
RoomManager.visitUser(root.author.object, "mention")
userDetailDialog.createObject(QQC2.ApplicationWindow.overlay, {
room: currentRoom,
user: root.author
}).open();
}
cursorShape: Qt.PointingHandCursor
}
@@ -456,7 +459,10 @@ ColumnLayout {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: {
RoomManager.visitUser(root.author.object, "mention")
userDetailDialog.createObject(QQC2.ApplicationWindow.overlay, {
room: currentRoom,
user: root.author
}).open();
}
}
}
@@ -599,7 +605,6 @@ 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) {
root.supportStreaming = false;
vid.supportStreaming = false;
}
}
onErrorChanged: {
if (error != MediaPlayer.NoError) {
root.supportStreaming = false;
vid.supportStreaming = false;
}
}
@@ -391,7 +391,7 @@ TimelineContainer {
TapHandler {
acceptedButtons: Qt.LeftButton
onTapped: if (root.supportStreaming || root.progressInfo.completed) {
onTapped: if (vid.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,16 +47,19 @@ QQC2.ScrollView {
interactive: Kirigami.Settings.isMobile
bottomMargin: Kirigami.Units.largeSpacing + Math.round(Kirigami.Theme.defaultFont.pointSize * 2)
model: sortedMessageEventModel
model: collapseStateProxyModel
MessageEventModel {
id: messageEventModel
room: root.currentRoom
}
MessageFilterModel {
id: sortedMessageEventModel
sourceModel: messageEventModel
CollapseStateProxyModel {
id: collapseStateProxyModel
sourceModel: MessageFilterModel {
id: sortedMessageEventModel
sourceModel: messageEventModel
}
}
Timer {
@@ -76,13 +79,6 @@ 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)) {
@@ -219,6 +215,12 @@ QQC2.ScrollView {
FileDelegateContextMenu {}
}
Component {
id: userDetailDialog
UserDetailDialog {}
}
TypingPane {
id: typingPane
visible: root.currentRoom && root.currentRoom.usersTyping.length > 0
@@ -346,7 +348,7 @@ QQC2.ScrollView {
MediaMessageFilterModel {
id: mediaMessageFilterModel
sourceModel: sortedMessageEventModel
sourceModel: collapseStateProxyModel
}
Component {
@@ -436,4 +438,11 @@ 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

@@ -1,46 +0,0 @@
// 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: {
root.connection.logout(true);
Controller.logout(root.connection, true);
root.close();
root.accepted();
}

View File

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

View File

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

View File

@@ -7,8 +7,6 @@ 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,7 +19,6 @@ Loader {
required property string source
property string selectedText: ""
required property string plainText
property string htmlText: undefined
property list<Kirigami.Action> nestedActions
@@ -41,20 +40,6 @@ 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,8 +7,6 @@ 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

@@ -1,39 +0,0 @@
// 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

@@ -2,41 +2,34 @@
// 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.settings 1.0 as KirigamiSettings
import org.kde.neochat 1.0
Kirigami.Page {
id: devtoolsPage
KirigamiSettings.CategorizedSettings {
id: root
property NeoChatRoom room
title: i18n("Developer Tools")
leftPadding: 0
rightPadding: 0
header: QQC2.TabBar {
id: tabBar
QQC2.TabButton {
text: qsTr("Room Data")
actions: [
KirigamiSettings.SettingAction {
actionName: "roomData"
text: i18n("Room Data")
icon.name: "datatype"
page: "qrc:/RoomData.qml"
initialProperties: {
return {
room: root.room
}
}
},
KirigamiSettings.SettingAction {
actionName: "serverData"
text: i18n("Server Info")
icon.name: "network-server-symbolic"
page: "qrc:/ServerData.qml"
}
QQC2.TabButton {
text: qsTr("Server Info")
}
}
StackLayout {
id: swipeView
anchors.fill: parent
currentIndex: tabBar.currentIndex
RoomData {}
ServerData {}
}
]
}

View File

@@ -31,15 +31,15 @@ Kirigami.Page {
imageDoc.crop(selectionTool.selectionX / ratioX, selectionTool.selectionY / ratioY, selectionTool.selectionWidth / ratioX, selectionTool.selectionHeight / ratioY);
}
actions: [
Kirigami.Action {
actions {
left: Kirigami.Action {
id: undoAction
text: i18nc("@action:button Undo modification", "Undo")
icon.name: "edit-undo"
onTriggered: imageDoc.undo();
visible: imageDoc.edited
},
Kirigami.Action {
}
main: 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")
onOpened: if (!serverUrlField.isValidServer && !opened) {
onSheetOpenChanged: if (!serverUrlField.isValidServer && !sheetOpen) {
serverField.currentIndex = 0
server = serverField.currentValue
} else if (opened) {
} else if (sheetOpen) {
serverUrlField.forceActiveFocus()
}

View File

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

View File

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

View File

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

View File

@@ -27,14 +27,6 @@ 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: {
@@ -46,7 +38,7 @@ Kirigami.Page {
Connections {
target: Controller
function onIsOnlineChanged() {
if (!Controller.isOnline) {
if (true || !Controller.isOnline) {
banner.text = i18n("NeoChat is offline. Please check your network connection.");
banner.visible = true;
banner.type = Kirigami.MessageType.Error;
@@ -189,22 +181,7 @@ Kirigami.Page {
banner.visible = true;
}
Connections {
target: RoomManager
function onShowUserDetail(user) {
root.showUserDetail(user)
}
}
function showUserDetail(user) {
userDetailDialog.createObject(QQC2.ApplicationWindow.overlay, {
room: root.currentRoom,
user: root.currentRoom.getUser(user.id),
}).open();
}
Component {
id: userDetailDialog
UserDetailDialog {}
timelineViewLoader.item.showUserDetail(user)
}
}

View File

@@ -6,16 +6,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
FormCard.FormCardPage {
id: root
Kirigami.ScrollablePage {
id: welcomePage
property alias currentStep: module.item
title: i18n("Welcome")
title: module.item.title ?? i18n("Welcome")
header: QQC2.Control {
contentItem: Kirigami.InlineMessage {
@@ -26,111 +25,79 @@ FormCard.FormCardPage {
}
}
FormCard.FormCard {
id: contentCard
Component.onCompleted: LoginHelper.init()
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: LoginHelper
function onErrorOccured(message) {
headerMessage.text = message;
headerMessage.visible = true;
headerMessage.type = Kirigami.MessageType.Error;
}
}
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.")
Connections {
target: Controller
function onInitiated() {
pageStack.layers.pop();
}
}
FormCard.FormDelegateSeparator {
above: welcomeMessage
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")
}
Loader {
id: module
Layout.fillWidth: true
source: "qrc:/LoginRegister.qml"
Layout.alignment: Qt.AlignHCenter
source: "qrc:/Login.qml"
onSourceChanged: {
headerMessage.visible = false
headerMessage.text = ""
}
}
RowLayout {
Layout.alignment: Qt.AlignHCenter
Connections {
id: stepConnections
target: currentStep
QQC2.Button {
text: i18nc("@action:button", "Back")
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;
enabled: welcomePage.currentStep.previousUrl !== ""
visible: welcomePage.currentStep.showBackButton
Layout.alignment: Qt.AlignHCenter
onClicked: {
module.source = welcomePage.currentStep.previousUrl
}
}
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;
}
QQC2.Button {
id: continueButton
enabled: welcomePage.currentStep.acceptable
visible: welcomePage.currentStep.showContinueButton
action: welcomePage.currentStep.action
}
}
FormCard.FormDelegateSeparator {
below: continueButton
}
Connections {
target: currentStep
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
function onProcessed(nextUrl) {
module.source = nextUrl;
}
function onShowMessage(message) {
headerMessage.text = message;
headerMessage.visible = true;
headerMessage.type = Kirigami.MessageType.Information;
}
}
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

@@ -26,7 +26,17 @@ ColumnLayout {
Layout.alignment: Qt.AlignHCenter
onClicked: {
RoomManager.visitUser(room.getUser(room.directChatRemoteUser.id).object, "mention")
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()
}
contentItem: KirigamiComponents.Avatar {

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