Compare commits

...

55 Commits

Author SHA1 Message Date
Laurent Montel
574f25884a As we have a kf6 branch not necessary to have a CI for it in master 2023-09-01 12:10:15 +02:00
Laurent Montel
167349b9d2 Remove unused forward declaration 2023-09-01 11:51:36 +02:00
l10n daemon script
296aa9ddad GIT_SILENT Sync po/docbooks with svn 2023-09-01 01:45:50 +00:00
Tobias Fella
94c1b67ec9 Port account editor to formcard 2023-08-31 22:02:30 +00:00
Tobias Fella
a9965acdd3 Fix compilation warning in searchmodel 2023-08-31 21:52:23 +00:00
Tobias Fella
35313271a3 Cleanup registration.h 2023-08-31 21:36:56 +00:00
Tobias Fella
e40179f641 Don't load data from invalid indices in MessageFilterModel 2023-08-31 21:28:20 +00:00
Tobias Fella
dacf3ff8a0 Fix compilation warnings in registration.cpp 2023-08-31 21:21:22 +00:00
Tobias Fella
0274d70f71 Fix connection property initialization 2023-08-31 21:16:57 +00:00
Tobias Fella
0c19052f32 Port PushNotification settings page to FormCard 2023-08-31 20:40:59 +00:00
Laurent Montel
a3423e5724 Fix warning about missing argument qml signal 2023-08-31 10:02:32 +00:00
l10n daemon script
e09f31f891 GIT_SILENT Sync po/docbooks with svn 2023-08-31 01:46:27 +00:00
Tobias Fella
e05e0f5489 Improve QML types 2023-08-30 23:04:18 +00:00
Tobias Fella
002961f638 Move jobs to individual files 2023-08-30 21:10:56 +00:00
Laurent Montel
e28419ef88 Use isEmpty() here 2023-08-30 19:29:02 +02:00
Laurent Montel
0e4df58fc9 Add missing include moc file 2023-08-30 19:20:04 +02:00
Tobias Fella
21a55885b3 Only read role names from source model when there is a source model 2023-08-30 15:21:24 +00:00
l10n daemon script
6a8ac8f6e5 GIT_SILENT Sync po/docbooks with svn 2023-08-30 01:46:13 +00:00
Tobias Fella
9fcfad7058 Fix supportStreaming in VideoDelegate 2023-08-29 20:48:02 +00:00
Tobias Fella
cfcc1756dd Implement deactivating accounts 2023-08-29 22:38:32 +02:00
Tobias Fella
e15bec2295 Introduce NeoChatConnection
Previously, some functions that conceptually belong to the connection needed to be in the Controller, since we didn't have a place to put them.
This fixes that by extending the Connection class in a similar way as we extend the Room class.
2023-08-29 20:14:44 +00:00
James Graham
9071cf827f Make sure that the event after a state event is also a state event before filtering it out. 2023-08-29 17:41:20 +01:00
Yuri Chornoivan
c7deaaba84 Fix minor typo 2023-08-29 09:52:45 +03:00
l10n daemon script
414035de8b GIT_SILENT Sync po/docbooks with svn 2023-08-29 01:44:33 +00:00
James Graham
442612d31d Merge the functionality of CollapseStateProxyModel into MessageFilterModel
Merge the functionality of CollapseStateProxyModel into MessageFilterModel there is no need for a whole separate model the filters can be combined trivialy.
2023-08-28 10:31:18 +00:00
Tobias Fella
59164d3bb2 Implement account registration
Implements #7
2023-08-28 10:05:09 +00:00
Tobias Fella
cc60dde62d Deselect space when leaving it
BUG: 473271
2023-08-28 11:47:39 +02:00
l10n daemon script
cbed8148a3 GIT_SILENT Sync po/docbooks with svn 2023-08-28 02:09:20 +00:00
James Graham
50e8b9ebf6 Visit User
Move to using visit user in all cases.
2023-08-27 19:05:55 +00:00
Nicolas Fella
f5ad2ad162 Fix enter key naviation in login flow 2023-08-27 19:32:41 +02:00
Tobias Fella
d11d6c74b3 Fix opening settings
BUG: 473789
2023-08-27 17:17:12 +00:00
James Graham
63ed69a5d4 Use updated AlbumMaximizeDelegate
Autoload is being removed so don't use it.

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

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

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

View File

@@ -4,11 +4,7 @@
include:
- https://invent.kde.org/sysadmin/ci-utilities/raw/master/gitlab-templates/reuse-lint.yml
- https://invent.kde.org/sysadmin/ci-utilities/raw/master/gitlab-templates/android.yml
# - https://invent.kde.org/sysadmin/ci-utilities/raw/master/gitlab-templates/android-qt6.yml
# - https://invent.kde.org/sysadmin/ci-utilities/raw/master/gitlab-templates/linux.yml
- https://invent.kde.org/sysadmin/ci-utilities/raw/master/gitlab-templates/linux-qt6.yml
- https://invent.kde.org/sysadmin/ci-utilities/raw/master/gitlab-templates/windows.yml
# - https://invent.kde.org/sysadmin/ci-utilities/raw/master/gitlab-templates/windows-qt6.yml
# - https://invent.kde.org/sysadmin/ci-utilities/raw/master/gitlab-templates/freebsd.yml
# - https://invent.kde.org/sysadmin/ci-utilities/raw/master/gitlab-templates/freebsd-qt6.yml
- https://invent.kde.org/sysadmin/ci-utilities/raw/master/gitlab-templates/flatpak.yml

View File

@@ -30,36 +30,7 @@ Dependencies:
'require':
'frameworks/kdbusaddons': '@stable'
- 'on': ['Linux/Qt6', 'Android/Qt6', 'FreeBSD/Qt6', 'Windows/Qt6']
'require':
'frameworks/extra-cmake-modules': '@latest-kf6'
'frameworks/kcoreaddons': '@latest-kf6'
'frameworks/kirigami': '@latest-kf6'
'frameworks/ki18n': '@latest-kf6'
'frameworks/kconfig': '@latest-kf6'
'frameworks/syntax-highlighting': '@latest-kf6'
'frameworks/kitemmodels': '@latest-kf6'
'frameworks/kquickcharts': '@latest-kf6'
'frameworks/knotifications': '@latest-kf6'
'libraries/kquickimageeditor': '@latest-kf6'
'frameworks/sonnet': '@latest-kf6'
'libraries/kirigami-addons': '@latest-kf6'
'third-party/libquotient': '@latest'
'third-party/qtkeychain': '@latest'
'third-party/cmark': '@latest'
'third-party/qcoro': '@latest'
- 'on': ['Windows/Qt6', 'Linux/Qt6', 'FreeBSD/Qt6']
'require':
'frameworks/qqc2-desktop-style': '@latest-kf6'
'frameworks/kio': '@latest-kf6'
'frameworks/kwindowsystem': '@latest-kf6'
'frameworks/kconfigwidgets': '@latest-kf6'
- 'on': ['Linux/Qt6', 'FreeBSD/Qt6']
'require':
'frameworks/kdbusaddons': '@latest-kf6'
'frameworks/kstatusnotifieritem': '@latest-kf6'
- 'on': ['Linux/Qt6', 'Linux/Qt5']
- 'on': ['Linux/Qt5']
'require':
'sdk/selenium-webdriver-at-spi': '@latest-kf6'

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

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

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

View File

@@ -9,7 +9,6 @@
#include "neochatroom.h"
class CustomEmojiModel;
class NeoChatRoom;
/**

View File

@@ -10,7 +10,6 @@
#include "models/completionmodel.h"
#include "neochatroom.h"
class QTextDocument;
class NeoChatRoom;
class SyntaxHighlighter;

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,16 @@
// SPDX-FileCopyrightText: 2023 Tobias Fella <tobias.fella@kde.org>
// SPDX-License-Identifier: GPL-2.0-or-later
#include "neochatchangepasswordjob.h"
using namespace Quotient;
NeochatChangePasswordJob::NeochatChangePasswordJob(const QString &newPassword, bool logoutDevices, const Omittable<QJsonObject> &auth)
: BaseJob(HttpVerb::Post, QStringLiteral("ChangePasswordJob"), "/_matrix/client/r0/account/password")
{
QJsonObject _data;
addParam<>(_data, QStringLiteral("new_password"), newPassword);
addParam<IfNotEmpty>(_data, QStringLiteral("logout_devices"), logoutDevices);
addParam<IfNotEmpty>(_data, QStringLiteral("auth"), auth);
setRequestData(_data);
}

View File

@@ -0,0 +1,13 @@
// SPDX-FileCopyrightText: 2023 Tobias Fella <tobias.fella@kde.org>
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <Quotient/jobs/basejob.h>
#include <Quotient/omittable.h>
class NeochatChangePasswordJob : public Quotient::BaseJob
{
public:
explicit NeochatChangePasswordJob(const QString &newPassword, bool logoutDevices, const Quotient::Omittable<QJsonObject> &auth = Quotient::none);
};

View File

@@ -0,0 +1,14 @@
// SPDX-FileCopyrightText: 2023 Tobias Fella <tobias.fella@kde.org>
// SPDX-License-Identifier: GPL-2.0-or-later
#include "neochatdeactivateaccountjob.h"
using namespace Quotient;
NeoChatDeactivateAccountJob::NeoChatDeactivateAccountJob(const Omittable<QJsonObject> &auth)
: BaseJob(HttpVerb::Post, QStringLiteral("DisableDeviceJob"), "_matrix/client/v3/account/deactivate")
{
QJsonObject data;
addParam<IfNotEmpty>(data, QStringLiteral("auth"), auth);
setRequestData(data);
}

View File

@@ -0,0 +1,13 @@
// SPDX-FileCopyrightText: 2023 Tobias Fella <tobias.fella@kde.org>
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <Quotient/jobs/basejob.h>
#include <Quotient/omittable.h>
class NeoChatDeactivateAccountJob : public Quotient::BaseJob
{
public:
explicit NeoChatDeactivateAccountJob(const Quotient::Omittable<QJsonObject> &auth = Quotient::none);
};

View File

@@ -0,0 +1,14 @@
// SPDX-FileCopyrightText: 2023 Tobias Fella <tobias.fella@kde.org>
// SPDX-License-Identifier: GPL-2.0-or-later
#include "neochatdeletedevicejob.h"
using namespace Quotient;
NeochatDeleteDeviceJob::NeochatDeleteDeviceJob(const QString &deviceId, const Omittable<QJsonObject> &auth)
: BaseJob(HttpVerb::Delete, QStringLiteral("DeleteDeviceJob"), QStringLiteral("/_matrix/client/r0/devices/%1").arg(deviceId).toLatin1())
{
QJsonObject _data;
addParam<IfNotEmpty>(_data, QStringLiteral("auth"), auth);
setRequestData(std::move(_data));
}

View File

@@ -0,0 +1,13 @@
// SPDX-FileCopyrightText: 2023 Tobias Fella <tobias.fella@kde.org>
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <Quotient/jobs/basejob.h>
#include <Quotient/omittable.h>
class NeochatDeleteDeviceJob : public Quotient::BaseJob
{
public:
explicit NeochatDeleteDeviceJob(const QString &deviceId, const Quotient::Omittable<QJsonObject> &auth = Quotient::none);
};

View File

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

View File

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

View File

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

View File

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

View File

@@ -10,11 +10,6 @@
#include <QReadWriteLock>
namespace Quotient
{
class Connection;
}
/**
* @class ThumbnailResponse
*

View File

@@ -13,8 +13,6 @@
#include <Quotient/connection.h>
class ImagePacksModel;
/**
* @class AccountEmoticonModel
*

View File

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

View File

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

View File

@@ -4,6 +4,7 @@
#include "devicesmodel.h"
#include "controller.h"
#include "jobs/neochatdeletedevicejob.h"
#include <QDateTime>
#include <QLocale>

View File

@@ -10,11 +10,6 @@
#include <QPointer>
#include <QRectF>
namespace Quotient
{
class RoomMessageEvent;
}
struct LiveLocationData {
QString eventId;
QString senderId;

View File

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

View File

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

View File

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

View File

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

View File

@@ -118,12 +118,7 @@ QVariant SearchModel::data(const QModelIndex &index, int role) const
return m_room->getUser(nullptr);
}
case ReplyRole:
if (role == ReplyRole) {
auto replyPtr = m_room->getReplyForEvent(event);
if (!replyPtr) {
return {};
}
if (auto replyPtr = m_room->getReplyForEvent(event)) {
MessageEventModel::DelegateType type;
if (auto e = eventCast<const RoomMessageEvent>(replyPtr)) {
switch (e->msgtype()) {
@@ -155,12 +150,12 @@ QVariant SearchModel::data(const QModelIndex &index, int role) const
} else {
type = MessageEventModel::DelegateType::Other;
}
return QVariantMap{
{"display"_ls, m_room->eventToString(*replyPtr, Qt::RichText)},
{"type"_ls, type},
};
}
break;
case IsPendingRole:
return false;
case ShowLinkPreviewRole:

159
src/neochatconnection.cpp Normal file
View File

@@ -0,0 +1,159 @@
// 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"
#include "jobs/neochatchangepasswordjob.h"
#include "jobs/neochatdeactivateaccountjob.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);
});
}
});
}
#include "moc_neochatconnection.cpp"

53
src/neochatconnection.h Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -411,7 +411,7 @@ QQC2.Control {
currentRoom: root.currentRoom
onChosen: insertText(emoji)
onChosen: emoji => insertText(emoji)
onClosed: if (emojiAction.checked) emojiAction.checked = false
}
}

View File

@@ -116,7 +116,7 @@ ColumnLayout {
Layout.fillWidth: true
Layout.fillHeight: true
withCustom: root.includeCustom
onChosen: root.chosen(unicode)
onChosen: unicode => root.chosen(unicode)
header: categories
Keys.forwardTo: searchField
stickers: root.selectedType === 1

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,46 @@
// SPDX-FileCopyrightText: 2023 Tobias Fella <tobias.fella@kde.org>
// SPDX-License-Identifier: GPL-2.0-or-later
import QtQuick 2.15
import QtQuick.Controls 2.15 as QQC2
import QtQuick.Layouts 1.15
import org.kde.kirigami 2.20 as Kirigami
import org.kde.kirigamiaddons.formcard 1.0 as FormCard
import org.kde.neochat 1.0
FormCard.FormCardPage {
id: root
required property NeoChatConnection 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

@@ -11,7 +11,7 @@ import org.kde.neochat 1.0
QQC2.Dialog {
id: root
property var connection
required property NeoChatConnection connection
ColumnLayout {
Kirigami.Heading {
@@ -37,7 +37,7 @@ QQC2.Dialog {
text: i18n("Sign out")
QQC2.DialogButtonBox.buttonRole: QQC2.DialogButtonBox.AcceptRole
onClicked: {
Controller.logout(root.connection, true);
root.connection.logout(true);
root.close();
root.accepted();
}

View File

@@ -66,7 +66,7 @@ QQC2.Popup {
currentRoom: root.currentRoom
includeCustom: emojiPopup.includeCustom
showQuickReaction: emojiPopup.showQuickReaction
onChosen: {
onChosen: emoji => {
emojiPopup.chosen(emoji)
if (emojiPopup.closeOnChosen) emojiPopup.close()
}

View File

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

View File

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

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