Compare commits
16 Commits
work/tobia
...
work/tobia
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3ede6b4526 | ||
|
|
eda2881d6e | ||
|
|
7ae553b2c3 | ||
|
|
94e650f7ad | ||
|
|
9a4a70178e | ||
|
|
33f505c06c | ||
|
|
47a5952c2a | ||
|
|
6babc1a479 | ||
|
|
58e4ccb680 | ||
|
|
607b6bcef0 | ||
|
|
bae9f39719 | ||
|
|
fa3fdca155 | ||
|
|
8085d19eee | ||
|
|
272f49876e | ||
|
|
ece56a55e8 | ||
|
|
aec35222f9 |
@@ -5,11 +5,11 @@ include:
|
||||
- project: sysadmin/ci-utilities
|
||||
file:
|
||||
- /gitlab-templates/reuse-lint.yml
|
||||
- /gitlab-templates/android-qt6.yml
|
||||
- /gitlab-templates/linux-qt6.yml
|
||||
- /gitlab-templates/windows-qt6.yml
|
||||
# - /gitlab-templates/android-qt6.yml
|
||||
# - /gitlab-templates/linux-qt6.yml
|
||||
# - /gitlab-templates/windows-qt6.yml
|
||||
# - /gitlab-templates/freebsd-qt6.yml
|
||||
- /gitlab-templates/flatpak.yml
|
||||
- /gitlab-templates/craft-android-qt6-apks.yml
|
||||
- /gitlab-templates/craft-appimage-qt6.yml
|
||||
- /gitlab-templates/craft-windows-x86-64-qt6.yml
|
||||
# - /gitlab-templates/craft-android-qt6-apks.yml
|
||||
# - /gitlab-templates/craft-appimage-qt6.yml
|
||||
# - /gitlab-templates/craft-windows-x86-64-qt6.yml
|
||||
|
||||
@@ -40,4 +40,4 @@ Dependencies:
|
||||
|
||||
Options:
|
||||
per-test-timeout: 90
|
||||
require-passing-tests-on: [ 'Linux', 'Android', 'FreeBSD' ]
|
||||
require-passing-tests-on: [ '@all' ]
|
||||
|
||||
@@ -60,9 +60,6 @@ set_package_properties(Qt6 PROPERTIES
|
||||
)
|
||||
|
||||
qt_policy(SET QTP0001 NEW)
|
||||
if (QT_KNOWN_POLICY_QTP0004)
|
||||
qt_policy(SET QTP0004 NEW)
|
||||
endif ()
|
||||
|
||||
find_package(KF6 ${KF_MIN_VERSION} COMPONENTS Kirigami I18n Notifications Config CoreAddons Sonnet ItemModels ColorScheme)
|
||||
set_package_properties(KF6 PROPERTIES
|
||||
@@ -105,7 +102,7 @@ if (NOT ANDROID AND NOT WIN32 AND NOT APPLE)
|
||||
find_package(KF6DBusAddons ${KF_MIN_VERSION} REQUIRED)
|
||||
endif()
|
||||
|
||||
find_package(QuotientQt6 0.8.2)
|
||||
find_package(QuotientQt6 0.7)
|
||||
set_package_properties(QuotientQt6 PROPERTIES
|
||||
TYPE REQUIRED
|
||||
DESCRIPTION "Qt wrapper around Matrix API"
|
||||
|
||||
@@ -38,8 +38,8 @@ Due to the nature of the Matrix specification development NeoChat also supports
|
||||
|
||||
Details where to find stable releases for NeoChat can be found on its [homepage](https://apps.kde.org/neochat).
|
||||
|
||||
Nightly builds for Linux and Windows can be downloaded from [cdn.kde.org](https://cdn.kde.org/ci-builds/network/neochat/).
|
||||
Nightly builds for Android are available from [KDE's nightly F-Droid repository](https://community.kde.org/Android/F-Droid).
|
||||
Nightly builds for linux and windows can be downloaded from [cdn.kde.org](https://cdn.kde.org/ci-builds/network/neochat/).
|
||||
Nightly builds for android are available from [KDE's nightly F-Droid repository](https://community.kde.org/Android/F-Droid).
|
||||
Nightly Flatpaks are available from [KDE's nightly Flatpak repository](https://userbase.kde.org/Tutorials/Flatpak).
|
||||
|
||||
## Building NeoChat
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
"content": {
|
||||
"user_ids": [
|
||||
"@alice:matrix.org",
|
||||
"@bob:kde.org"
|
||||
"@bob:example.com"
|
||||
]
|
||||
},
|
||||
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
|
||||
@@ -47,7 +47,7 @@
|
||||
"content": {
|
||||
"$1532735824654:example.org": {
|
||||
"m.read": {
|
||||
"@bob:kde.org": {
|
||||
"@bob:example.com": {
|
||||
"ts": 1436451550453
|
||||
}
|
||||
}
|
||||
@@ -67,18 +67,6 @@
|
||||
},
|
||||
"type": "m.receipt"
|
||||
},
|
||||
{
|
||||
"content": {
|
||||
"$1532735824654:example.org": {
|
||||
"m.read": {
|
||||
"@tim2:example.com": {
|
||||
"ts": 1436451550454
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"type": "m.receipt"
|
||||
},
|
||||
{
|
||||
"content": {
|
||||
"$1532735824654:example.org": {
|
||||
@@ -148,22 +136,6 @@
|
||||
"age": 1234
|
||||
}
|
||||
},
|
||||
{
|
||||
"content": {
|
||||
"avatar_url": "mxc://example.org/SEsfnsuifSDFSSEF",
|
||||
"displayname": "Bob",
|
||||
"membership": "join"
|
||||
},
|
||||
"event_id": "$143273582443PhrSn:example.org",
|
||||
"origin_server_ts": 1432735824653,
|
||||
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
|
||||
"sender": "bob:kde.org",
|
||||
"state_key": "@bob:kde.org",
|
||||
"type": "m.room.member",
|
||||
"unsigned": {
|
||||
"age": 1234
|
||||
}
|
||||
},
|
||||
{
|
||||
"content": {
|
||||
"displayname": "Look\nat\nme\nI\nput\nnewlines\nin\nmy\ndisplay name",
|
||||
@@ -184,7 +156,7 @@
|
||||
"summary": {
|
||||
"m.heroes": [
|
||||
"@alice:example.com",
|
||||
"@bob:kde.org"
|
||||
"@bob:example.com"
|
||||
],
|
||||
"m.invited_member_count": 0,
|
||||
"m.joined_member_count": 2
|
||||
|
||||
@@ -130,23 +130,7 @@
|
||||
"origin_server_ts": 1432735824653,
|
||||
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
|
||||
"sender": "@example:example.org",
|
||||
"state_key": "@alice:matrix.org",
|
||||
"type": "m.room.member",
|
||||
"unsigned": {
|
||||
"age": 1234
|
||||
}
|
||||
},
|
||||
{
|
||||
"content": {
|
||||
"avatar_url": "mxc://example.org/SEsfnsuifSDFSSEF",
|
||||
"displayname": "Bob",
|
||||
"membership": "join"
|
||||
},
|
||||
"event_id": "$143273582443PhrSn:example.org",
|
||||
"origin_server_ts": 1432735824653,
|
||||
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
|
||||
"sender": "bob:example.org",
|
||||
"state_key": "@bob:example.org",
|
||||
"state_key": "@alice:example.org",
|
||||
"type": "m.room.member",
|
||||
"unsigned": {
|
||||
"age": 1234
|
||||
|
||||
@@ -52,7 +52,7 @@ void ReactionModelTest::basicReaction()
|
||||
QCOMPARE(model.data(model.index(0), ReactionModel::TextContentRole), QStringLiteral("<span style=\"font-family: 'emoji';\">👍</span>"));
|
||||
QCOMPARE(model.data(model.index(0), ReactionModel::ReactionRole), QStringLiteral("👍"));
|
||||
QCOMPARE(model.data(model.index(0), ReactionModel::ToolTipRole),
|
||||
QStringLiteral("Alice Margatroid reacted with <span style=\"font-family: 'emoji';\">👍</span>"));
|
||||
QStringLiteral("@alice:matrix.org reacted with <span style=\"font-family: 'emoji';\">👍</span>"));
|
||||
QCOMPARE(model.data(model.index(0), ReactionModel::HasLocalMember), false);
|
||||
}
|
||||
|
||||
@@ -63,7 +63,7 @@ void ReactionModelTest::newReaction()
|
||||
|
||||
QCOMPARE(model->rowCount(), 1);
|
||||
QCOMPARE(model->data(model->index(0), ReactionModel::ToolTipRole),
|
||||
QStringLiteral("Alice Margatroid reacted with <span style=\"font-family: 'emoji';\">👍</span>"));
|
||||
QStringLiteral("@alice:matrix.org reacted with <span style=\"font-family: 'emoji';\">👍</span>"));
|
||||
|
||||
QSignalSpy spy(model, SIGNAL(modelReset()));
|
||||
|
||||
@@ -72,7 +72,7 @@ void ReactionModelTest::newReaction()
|
||||
QCOMPARE(spy.count(), 2); // Once for each of the 2 new reactions.
|
||||
QCOMPARE(model->data(model->index(1), ReactionModel::ReactionRole), QStringLiteral("😆"));
|
||||
QCOMPARE(model->data(model->index(0), ReactionModel::ToolTipRole),
|
||||
QStringLiteral("Alice Margatroid and Bob reacted with <span style=\"font-family: 'emoji';\">👍</span>"));
|
||||
QStringLiteral("@alice:matrix.org and @bob:example.org reacted with <span style=\"font-family: 'emoji';\">👍</span>"));
|
||||
|
||||
delete model;
|
||||
}
|
||||
|
||||
@@ -97,7 +97,6 @@
|
||||
<p xml:lang="nl">NeoChat is een chat-toepassing die u het volledige voordeel van het Matrix-netwerk laat genieten. Het levert u op een veilige manier tekstberichten, video's en geluidsbestanden naar uw familie, collega's en vrienden te verzenden.</p>
|
||||
<p xml:lang="pl">NoeChat to aplikacja do rozmów, która umożliwia wykorzystanie wszystkich możliwości Matriksa. Umożliwia wysyłanie wiadomości tekstowych, filmów i dźwięków w bezpieczny sposób do twojej rodziny, kolegów i przyjaciół.</p>
|
||||
<p xml:lang="sl">NeoChat je aplikacija za klepet, ki vam omogoča, da v celoti izkoristite omrežje Matrix. Zagotavlja vam varen način za pošiljanje besedilnih sporočil, videoposnetkov in zvočnih datotek vaši družini, sodelavcem in prijateljem.</p>
|
||||
<p xml:lang="sv">NeoChat är ett chattprogram som låter dig dra full nytta av Matrix-nätverket. Det ger dig ett säkert sätt att skicka textmeddelanden, videor och ljudfiler till din familj, kollegor och vänner.</p>
|
||||
<p xml:lang="tr">NeoChat, Matrix ağının tüm özelliklerini kullanan bir sohbet uygulamasıdır. Ailenize, arkadaşlarınıza ve iş arkadaşlarınıza metin iletileri, ses ve video dosyaları göndermenin kolay bir yolunu sunar.</p>
|
||||
<p xml:lang="uk">NeoChat є програмою для спілкування, за допомогою якої ви можете скористатися усіма перевагами мережі Matrix. За її допомогою ви можете безпечно надсилати текстові повідомлення, відео та звукові файли вашим родичам, колегам та друзям.</p>
|
||||
<p xml:lang="x-test">xxNeoChat is a chat app that lets you take full advantage of the Matrix network. It provides you with a secure way to send text messages, videos and audio files to your family, colleagues and friends.xx</p>
|
||||
@@ -213,7 +212,7 @@
|
||||
<li xml:lang="sl">Sticker Packs - MSC2545</li>
|
||||
<li xml:lang="sv">Sticker Packs - MSC2545</li>
|
||||
<li xml:lang="ta">ஒட்டி தொகுப்புகள் - MSC2545</li>
|
||||
<li xml:lang="tr">Çıkartma Paketleri — MSC2545</li>
|
||||
<li xml:lang="tr">Yapışkan Paketleri — MSC2545</li>
|
||||
<li xml:lang="uk">Пакунки наліпок - MSC2545</li>
|
||||
<li xml:lang="x-test">xxSticker Packs - MSC2545xx</li>
|
||||
<li xml:lang="zh-TW">貼圖包 - MSC2545</li>
|
||||
@@ -330,7 +329,6 @@
|
||||
<caption xml:lang="nl">Ontdek nieuwe gemeenschappen met Matrix-ruimten</caption>
|
||||
<caption xml:lang="pl">Odkrywaj nowe społeczności w Przestrzeniach Matriksa</caption>
|
||||
<caption xml:lang="sl">Odkrijte nove skupnosti z Matrix Spaces</caption>
|
||||
<caption xml:lang="sv">Upptäck nya gemenskaper med Matrix Spaces</caption>
|
||||
<caption xml:lang="tr">Matrix Alanlar ile yeni topluluklar keşfedin</caption>
|
||||
<caption xml:lang="uk">Пошук нових спільнот за допомогою Matrix Spaces</caption>
|
||||
<caption xml:lang="x-test">xxDiscover new communities with Matrix Spacesxx</caption>
|
||||
@@ -413,8 +411,6 @@
|
||||
<content_attribute id="social-chat">intense</content_attribute>
|
||||
</content_rating>
|
||||
<releases>
|
||||
<release version="24.05.2" date="2024-07-04"/>
|
||||
<release version="24.05.1" date="2024-06-13"/>
|
||||
<release version="24.05.0" date="2024-05-23"/>
|
||||
<release version="24.02.2" date="2024-04-11"/>
|
||||
<release version="24.02.1" date="2024-03-21"/>
|
||||
|
||||
1148
po/ar/neochat.po
1148
po/ar/neochat.po
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
1154
po/az/neochat.po
1154
po/az/neochat.po
File diff suppressed because it is too large
Load Diff
@@ -77,7 +77,7 @@ SPDX-License-Identifier: CC-BY-SA-4.0
|
||||
></term>
|
||||
<listitem>
|
||||
<para
|
||||
>L'URI de Matrix per a un usuari o una sala. P. ex. matrix:u/usuari:example.org o matrix:r/root:example.org. Això farà que el NeoChat intenti obrir la sala o conversa indicada. </para>
|
||||
>L'URI de Matrix per a un usuari o una sala. P. ex. matrix:u/usuari:exemple.org o matrix:r/root:exemple.org. Això farà que el NeoChat intenti obrir la sala o conversa indicada. </para>
|
||||
</listitem>
|
||||
</varlistentry>
|
||||
</variablelist>
|
||||
|
||||
1112
po/ca/neochat.po
1112
po/ca/neochat.po
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
1144
po/cs/neochat.po
1144
po/cs/neochat.po
File diff suppressed because it is too large
Load Diff
1066
po/da/neochat.po
1066
po/da/neochat.po
File diff suppressed because it is too large
Load Diff
1149
po/de/neochat.po
1149
po/de/neochat.po
File diff suppressed because it is too large
Load Diff
1149
po/el/neochat.po
1149
po/el/neochat.po
File diff suppressed because it is too large
Load Diff
1116
po/en_GB/neochat.po
1116
po/en_GB/neochat.po
File diff suppressed because it is too large
Load Diff
1013
po/eo/neochat.po
1013
po/eo/neochat.po
File diff suppressed because it is too large
Load Diff
1110
po/es/neochat.po
1110
po/es/neochat.po
File diff suppressed because it is too large
Load Diff
1146
po/eu/neochat.po
1146
po/eu/neochat.po
File diff suppressed because it is too large
Load Diff
1139
po/fi/neochat.po
1139
po/fi/neochat.po
File diff suppressed because it is too large
Load Diff
1112
po/fr/neochat.po
1112
po/fr/neochat.po
File diff suppressed because it is too large
Load Diff
1148
po/hu/neochat.po
1148
po/hu/neochat.po
File diff suppressed because it is too large
Load Diff
1114
po/ia/neochat.po
1114
po/ia/neochat.po
File diff suppressed because it is too large
Load Diff
1149
po/id/neochat.po
1149
po/id/neochat.po
File diff suppressed because it is too large
Load Diff
1114
po/ie/neochat.po
1114
po/ie/neochat.po
File diff suppressed because it is too large
Load Diff
1127
po/it/neochat.po
1127
po/it/neochat.po
File diff suppressed because it is too large
Load Diff
982
po/ja/neochat.po
982
po/ja/neochat.po
File diff suppressed because it is too large
Load Diff
1110
po/ka/neochat.po
1110
po/ka/neochat.po
File diff suppressed because it is too large
Load Diff
1139
po/ko/neochat.po
1139
po/ko/neochat.po
File diff suppressed because it is too large
Load Diff
982
po/lt/neochat.po
982
po/lt/neochat.po
File diff suppressed because it is too large
Load Diff
1139
po/lv/neochat.po
1139
po/lv/neochat.po
File diff suppressed because it is too large
Load Diff
1112
po/nl/neochat.po
1112
po/nl/neochat.po
File diff suppressed because it is too large
Load Diff
1528
po/nn/neochat.po
1528
po/nn/neochat.po
File diff suppressed because it is too large
Load Diff
1091
po/pa/neochat.po
1091
po/pa/neochat.po
File diff suppressed because it is too large
Load Diff
1141
po/pl/neochat.po
1141
po/pl/neochat.po
File diff suppressed because it is too large
Load Diff
1150
po/pt/neochat.po
1150
po/pt/neochat.po
File diff suppressed because it is too large
Load Diff
1154
po/pt_BR/neochat.po
1154
po/pt_BR/neochat.po
File diff suppressed because it is too large
Load Diff
1145
po/ru/neochat.po
1145
po/ru/neochat.po
File diff suppressed because it is too large
Load Diff
1101
po/sk/neochat.po
1101
po/sk/neochat.po
File diff suppressed because it is too large
Load Diff
1110
po/sl/neochat.po
1110
po/sl/neochat.po
File diff suppressed because it is too large
Load Diff
2231
po/sv/neochat.po
2231
po/sv/neochat.po
File diff suppressed because it is too large
Load Diff
1229
po/ta/neochat.po
1229
po/ta/neochat.po
File diff suppressed because it is too large
Load Diff
1049
po/tok/neochat.po
1049
po/tok/neochat.po
File diff suppressed because it is too large
Load Diff
1154
po/tr/neochat.po
1154
po/tr/neochat.po
File diff suppressed because it is too large
Load Diff
1108
po/uk/neochat.po
1108
po/uk/neochat.po
File diff suppressed because it is too large
Load Diff
1002
po/zh_CN/neochat.po
1002
po/zh_CN/neochat.po
File diff suppressed because it is too large
Load Diff
1102
po/zh_TW/neochat.po
1102
po/zh_TW/neochat.po
File diff suppressed because it is too large
Load Diff
@@ -192,7 +192,7 @@ set_source_files_properties(qml/OsmLocationPlugin.qml PROPERTIES
|
||||
QT_QML_SINGLETON_TYPE TRUE
|
||||
)
|
||||
|
||||
ecm_add_qml_module(neochat URI org.kde.neochat GENERATE_PLUGIN_SOURCE
|
||||
qt_add_qml_module(neochat URI org.kde.neochat NO_PLUGIN
|
||||
OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/src/org/kde/neochat
|
||||
QML_FILES
|
||||
qml/Main.qml
|
||||
@@ -282,7 +282,7 @@ ecm_add_qml_module(neochat URI org.kde.neochat GENERATE_PLUGIN_SOURCE
|
||||
qml/ConsentDialog.qml
|
||||
qml/AskDirectChatConfirmation.qml
|
||||
qml/HoverLinkIndicator.qml
|
||||
qml/AccountDialog.qml
|
||||
qml/CrossSigningSetupDialog.qml
|
||||
DEPENDENCIES
|
||||
QtCore
|
||||
QtQuick
|
||||
|
||||
@@ -13,15 +13,13 @@ import org.kde.neochat
|
||||
QQC2.Popup {
|
||||
id: root
|
||||
|
||||
padding: Kirigami.Units.largeSpacing
|
||||
padding: 16
|
||||
|
||||
signal chosen(string path)
|
||||
|
||||
contentItem: RowLayout {
|
||||
|
||||
spacing: Kirigami.Units.smallSpacing
|
||||
|
||||
QQC2.ToolButton {
|
||||
Layout.preferredWidth: 160
|
||||
Layout.fillHeight: true
|
||||
|
||||
icon.name: 'mail-attachment'
|
||||
@@ -30,7 +28,7 @@ QQC2.Popup {
|
||||
|
||||
onClicked: {
|
||||
root.close();
|
||||
var fileDialog = openFileDialog.createObject(QQC2.Overlay.overlay);
|
||||
var fileDialog = openFileDialog.createObject(QQC2.ApplicationWindow.overlay);
|
||||
fileDialog.chosen.connect(path => root.chosen(path));
|
||||
fileDialog.open();
|
||||
}
|
||||
@@ -39,8 +37,11 @@ QQC2.Popup {
|
||||
Kirigami.Separator {}
|
||||
|
||||
QQC2.ToolButton {
|
||||
Layout.preferredWidth: 160
|
||||
Layout.fillHeight: true
|
||||
|
||||
padding: 16
|
||||
|
||||
icon.name: 'insert-image'
|
||||
text: i18n("Clipboard image")
|
||||
onClicked: {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
# SPDX-License-Identifier: BSD-2-Clause
|
||||
|
||||
qt_add_library(chatbar STATIC)
|
||||
ecm_add_qml_module(chatbar GENERATE_PLUGIN_SOURCE
|
||||
qt_add_qml_module(chatbar
|
||||
URI org.kde.neochat.chatbar
|
||||
OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/src/org/kde/neochat/chatbar
|
||||
QML_FILES
|
||||
|
||||
@@ -115,7 +115,7 @@ QQC2.Control {
|
||||
displayHint: QQC2.AbstractButton.IconOnly
|
||||
|
||||
onTriggered: {
|
||||
locationChooser.createObject(QQC2.Overlay.overlay, {
|
||||
locationChooser.createObject(QQC2.ApplicationWindow.overlay, {
|
||||
room: root.currentRoom
|
||||
}).open();
|
||||
}
|
||||
@@ -364,7 +364,7 @@ QQC2.Control {
|
||||
ReplyPane {
|
||||
userName: _private.chatBarCache.relationUser.displayName
|
||||
userColor: _private.chatBarCache.relationUser.color
|
||||
userAvatar: _private.chatBarCache.relationUser.avatarUrl
|
||||
userAvatar: _private.chatBarCache.relationUser.avatarSource
|
||||
text: _private.chatBarCache.relationMessage
|
||||
|
||||
onCancel: {
|
||||
|
||||
@@ -103,16 +103,14 @@ Controller::Controller(QObject *parent)
|
||||
connect(&m_accountRegistry, &AccountRegistry::accountCountChanged, this, [this]() {
|
||||
if (m_accountRegistry.size() > oldAccountCount) {
|
||||
auto connection = dynamic_cast<NeoChatConnection *>(m_accountRegistry.accounts()[m_accountRegistry.size() - 1]);
|
||||
connect(
|
||||
connection,
|
||||
&NeoChatConnection::syncDone,
|
||||
this,
|
||||
[this, connection] {
|
||||
if (!m_endpoint.isEmpty()) {
|
||||
connection->setupPushNotifications(m_endpoint);
|
||||
}
|
||||
},
|
||||
Qt::SingleShotConnection);
|
||||
connect(connection, &NeoChatConnection::syncDone, this, [connection]() {
|
||||
NotificationsManager::instance().handleNotifications(connection);
|
||||
});
|
||||
connectSingleShot(connection, &NeoChatConnection::syncDone, this, [this, connection] {
|
||||
if (!m_endpoint.isEmpty()) {
|
||||
connection->setupPushNotifications(m_endpoint);
|
||||
}
|
||||
});
|
||||
}
|
||||
oldAccountCount = m_accountRegistry.size();
|
||||
});
|
||||
@@ -392,6 +390,8 @@ QString Controller::loadFileContent(const QString &path) const
|
||||
return QString::fromLatin1(file.readAll());
|
||||
}
|
||||
|
||||
#include "moc_controller.cpp"
|
||||
|
||||
void Controller::setTestMode(bool test)
|
||||
{
|
||||
testMode = test;
|
||||
@@ -406,14 +406,3 @@ void Controller::removeConnection(const QString &userId)
|
||||
SettingsGroup("Accounts"_ls).remove(userId);
|
||||
}
|
||||
}
|
||||
|
||||
bool Controller::csSupported() const
|
||||
{
|
||||
#if Quotient_VERSION_MINOR > 9
|
||||
return true;
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
#include "moc_controller.cpp"
|
||||
|
||||
@@ -50,19 +50,7 @@ class Controller : public QObject
|
||||
|
||||
Q_PROPERTY(QStringList accountsLoading MEMBER m_accountsLoading NOTIFY accountsLoadingChanged)
|
||||
|
||||
Q_PROPERTY(bool csSupported READ csSupported CONSTANT)
|
||||
|
||||
public:
|
||||
/**
|
||||
* @brief Define the types on inline messages that can be shown.
|
||||
*/
|
||||
enum MessageType {
|
||||
Positive, /**< Positive message, typically green. */
|
||||
Info, /**< Info message, typically highlight color. */
|
||||
Error, /**< Error message, typically red. */
|
||||
};
|
||||
Q_ENUM(MessageType)
|
||||
|
||||
static Controller &instance();
|
||||
static Controller *create(QQmlEngine *engine, QJSEngine *)
|
||||
{
|
||||
@@ -106,8 +94,6 @@ public:
|
||||
|
||||
Q_INVOKABLE void removeConnection(const QString &userId);
|
||||
|
||||
bool csSupported() const;
|
||||
|
||||
private:
|
||||
explicit Controller(QObject *parent = nullptr);
|
||||
|
||||
@@ -135,5 +121,4 @@ Q_SIGNALS:
|
||||
void connectionDropped(NeoChatConnection *connection);
|
||||
void activeConnectionChanged(NeoChatConnection *connection);
|
||||
void accountsLoadingChanged();
|
||||
void showMessage(MessageType messageType, const QString &message);
|
||||
};
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
# SPDX-License-Identifier: BSD-2-Clause
|
||||
|
||||
qt_add_library(devtools STATIC)
|
||||
ecm_add_qml_module(devtools GENERATE_PLUGIN_SOURCE
|
||||
qt_add_qml_module(devtools
|
||||
URI org.kde.neochat.devtools
|
||||
OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/src/org/kde/neochat/devtools
|
||||
QML_FILES
|
||||
|
||||
@@ -33,7 +33,6 @@ public:
|
||||
* a room message.
|
||||
*/
|
||||
enum Type {
|
||||
Author, /**< The message sender and time. */
|
||||
Text, /**< A text message. */
|
||||
Image, /**< A message that is an image. */
|
||||
Audio, /**< A message that is an audio recording. */
|
||||
|
||||
@@ -319,10 +319,7 @@ QString EventHandler::getBody(const Quotient::RoomEvent *event, Qt::TextFormat f
|
||||
auto subjectName = m_room->member(e.userId()).htmlSafeDisplayName();
|
||||
if (e.membership() == Membership::Leave) {
|
||||
if (e.prevContent() && e.prevContent()->displayName) {
|
||||
subjectName = sanitized(*e.prevContent()->displayName);
|
||||
if (prettyPrint) {
|
||||
subjectName = subjectName.toHtmlEscaped();
|
||||
}
|
||||
subjectName = sanitized(*e.prevContent()->displayName).toHtmlEscaped();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1039,7 +1036,7 @@ QString EventHandler::getReadMarkersString() const
|
||||
for (const auto &userId : userIds) {
|
||||
auto member = m_room->member(userId);
|
||||
QString displayName;
|
||||
if (!m_room->memberIds().contains(userId)) {
|
||||
if (member.isEmpty()) {
|
||||
displayName = i18nc("A member who is not in the room has been requested.", "unknown member");
|
||||
} else {
|
||||
displayName = member.displayName();
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
|
||||
#include "imagepackevent.h"
|
||||
#include <QJsonObject>
|
||||
#include <Quotient/omittable.h>
|
||||
|
||||
using namespace Quotient;
|
||||
|
||||
@@ -11,10 +10,10 @@ ImagePackEventContent::ImagePackEventContent(const QJsonObject &json)
|
||||
{
|
||||
if (json.contains(QStringLiteral("pack"))) {
|
||||
pack = ImagePackEventContent::Pack{
|
||||
fromJson<Omittable<QString>>(json["pack"_ls].toObject()["display_name"_ls]),
|
||||
fromJson<Omittable<QUrl>>(json["pack"_ls].toObject()["avatar_url"_ls]),
|
||||
fromJson<Omittable<QStringList>>(json["pack"_ls].toObject()["usage"_ls]),
|
||||
fromJson<Omittable<QString>>(json["pack"_ls].toObject()["attribution"_ls]),
|
||||
fromJson<std::optional<QString>>(json["pack"_ls].toObject()["display_name"_ls]),
|
||||
fromJson<std::optional<QUrl>>(json["pack"_ls].toObject()["avatar_url"_ls]),
|
||||
fromJson<std::optional<QStringList>>(json["pack"_ls].toObject()["usage"_ls]),
|
||||
fromJson<std::optional<QString>>(json["pack"_ls].toObject()["attribution"_ls]),
|
||||
};
|
||||
} else {
|
||||
pack = std::nullopt;
|
||||
@@ -31,9 +30,9 @@ ImagePackEventContent::ImagePackEventContent(const QJsonObject &json)
|
||||
images += ImagePackImage{
|
||||
k,
|
||||
fromJson<QUrl>(json["images"_ls][k]["url"_ls].toString()),
|
||||
fromJson<Omittable<QString>>(json["images"_ls][k]["body"_ls]),
|
||||
fromJson<std::optional<QString>>(json["images"_ls][k]["body"_ls]),
|
||||
info,
|
||||
fromJson<Omittable<QStringList>>(json["images"_ls][k]["usage"_ls]),
|
||||
fromJson<std::optional<QStringList>>(json["images"_ls][k]["usage"_ls]),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
#include <Quotient/accountregistry.h>
|
||||
#include <Quotient/e2ee/sssshandler.h>
|
||||
#include <Quotient/keyverificationsession.h>
|
||||
#include <Quotient/roommember.h>
|
||||
|
||||
#include "controller.h"
|
||||
#include "neochatconfig.h"
|
||||
@@ -51,9 +50,3 @@ struct ForeignSSSSHandler {
|
||||
QML_FOREIGN(Quotient::SSSSHandler)
|
||||
QML_NAMED_ELEMENT(SSSSHandler)
|
||||
};
|
||||
|
||||
struct RoomMemberForeign {
|
||||
Q_GADGET
|
||||
QML_FOREIGN(Quotient::RoomMember)
|
||||
QML_NAMED_ELEMENT(RoomMember)
|
||||
};
|
||||
|
||||
@@ -9,5 +9,5 @@
|
||||
class NeochatAdd3PIdJob : public Quotient::BaseJob
|
||||
{
|
||||
public:
|
||||
explicit NeochatAdd3PIdJob(const QString &clientSecret, const QString &sid, const Quotient::Omittable<QJsonObject> &auth = {});
|
||||
explicit NeochatAdd3PIdJob(const QString &clientSecret, const QString &sid, const Quotient::Omittable<QJsonObject> &auth = Quotient::none);
|
||||
};
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
using namespace Quotient;
|
||||
|
||||
NeochatChangePasswordJob::NeochatChangePasswordJob(const QString &newPassword, bool logoutDevices, const Omittable<QJsonObject> &auth)
|
||||
NeochatChangePasswordJob::NeochatChangePasswordJob(const QString &newPassword, bool logoutDevices, const std::optional<QJsonObject> &auth)
|
||||
: BaseJob(HttpVerb::Post, QStringLiteral("ChangePasswordJob"), "/_matrix/client/r0/account/password")
|
||||
{
|
||||
QJsonObject _data;
|
||||
|
||||
@@ -5,10 +5,8 @@
|
||||
|
||||
#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 = {});
|
||||
explicit NeochatChangePasswordJob(const QString &newPassword, bool logoutDevices, const std::optional<QJsonObject> &auth = std::nullopt);
|
||||
};
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
using namespace Quotient;
|
||||
|
||||
NeoChatDeactivateAccountJob::NeoChatDeactivateAccountJob(const Omittable<QJsonObject> &auth)
|
||||
NeoChatDeactivateAccountJob::NeoChatDeactivateAccountJob(const std::optional<QJsonObject> &auth)
|
||||
: BaseJob(HttpVerb::Post, QStringLiteral("DisableDeviceJob"), "_matrix/client/v3/account/deactivate")
|
||||
{
|
||||
QJsonObject data;
|
||||
|
||||
@@ -4,10 +4,9 @@
|
||||
#pragma once
|
||||
|
||||
#include <Quotient/jobs/basejob.h>
|
||||
#include <Quotient/omittable.h>
|
||||
|
||||
class NeoChatDeactivateAccountJob : public Quotient::BaseJob
|
||||
{
|
||||
public:
|
||||
explicit NeoChatDeactivateAccountJob(const Quotient::Omittable<QJsonObject> &auth = {});
|
||||
explicit NeoChatDeactivateAccountJob(const std::optional<QJsonObject> &auth = std::nullopt);
|
||||
};
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
using namespace Quotient;
|
||||
|
||||
NeochatDeleteDeviceJob::NeochatDeleteDeviceJob(const QString &deviceId, const Omittable<QJsonObject> &auth)
|
||||
NeochatDeleteDeviceJob::NeochatDeleteDeviceJob(const QString &deviceId, const std::optional<QJsonObject> &auth)
|
||||
: BaseJob(HttpVerb::Delete, QStringLiteral("DeleteDeviceJob"), QStringLiteral("/_matrix/client/r0/devices/%1").arg(deviceId).toLatin1())
|
||||
{
|
||||
QJsonObject _data;
|
||||
|
||||
@@ -4,10 +4,9 @@
|
||||
#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 = {});
|
||||
explicit NeochatDeleteDeviceJob(const QString &deviceId, const std::optional<QJsonObject> &auth = std::nullopt);
|
||||
};
|
||||
|
||||
@@ -54,19 +54,14 @@ void LoginHelper::init()
|
||||
m_connection = new NeoChatConnection();
|
||||
}
|
||||
m_connection->resolveServer(m_matrixId);
|
||||
connect(
|
||||
m_connection.get(),
|
||||
&Connection::loginFlowsChanged,
|
||||
this,
|
||||
[this]() {
|
||||
setHomeserverReachable(true);
|
||||
m_testing = false;
|
||||
Q_EMIT testingChanged();
|
||||
m_supportsSso = m_connection->supportsSso();
|
||||
m_supportsPassword = m_connection->supportsPasswordAuth();
|
||||
Q_EMIT loginFlowsChanged();
|
||||
},
|
||||
Qt::SingleShotConnection);
|
||||
connectSingleShot(m_connection.get(), &Connection::loginFlowsChanged, this, [this]() {
|
||||
setHomeserverReachable(true);
|
||||
m_testing = false;
|
||||
Q_EMIT testingChanged();
|
||||
m_supportsSso = m_connection->supportsSso();
|
||||
m_supportsPassword = m_connection->supportsPasswordAuth();
|
||||
Q_EMIT loginFlowsChanged();
|
||||
});
|
||||
});
|
||||
connect(m_connection, &Connection::connected, this, [this] {
|
||||
Q_EMIT connected();
|
||||
@@ -105,14 +100,9 @@ void LoginHelper::init()
|
||||
Q_EMIT Controller::instance().errorOccured(i18n("Network Error"), std::move(error));
|
||||
});
|
||||
|
||||
connect(
|
||||
m_connection.get(),
|
||||
&Connection::syncDone,
|
||||
this,
|
||||
[this]() {
|
||||
Q_EMIT loaded();
|
||||
},
|
||||
Qt::SingleShotConnection);
|
||||
connectSingleShot(m_connection.get(), &Connection::syncDone, this, [this]() {
|
||||
Q_EMIT loaded();
|
||||
});
|
||||
}
|
||||
|
||||
void LoginHelper::setHomeserverReachable(bool reachable)
|
||||
@@ -192,16 +182,11 @@ QUrl LoginHelper::ssoUrl() const
|
||||
void LoginHelper::loginWithSso()
|
||||
{
|
||||
m_connection->resolveServer(m_matrixId);
|
||||
connect(
|
||||
m_connection.get(),
|
||||
&Connection::loginFlowsChanged,
|
||||
this,
|
||||
[this]() {
|
||||
SsoSession *session = m_connection->prepareForSso(m_deviceName);
|
||||
m_ssoUrl = session->ssoUrl();
|
||||
Q_EMIT ssoUrlChanged();
|
||||
},
|
||||
Qt::SingleShotConnection);
|
||||
connectSingleShot(m_connection.get(), &Connection::loginFlowsChanged, this, [this]() {
|
||||
SsoSession *session = m_connection->prepareForSso(m_deviceName);
|
||||
m_ssoUrl = session->ssoUrl();
|
||||
Q_EMIT ssoUrlChanged();
|
||||
});
|
||||
}
|
||||
|
||||
bool LoginHelper::testing() const
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
# SPDX-License-Identifier: BSD-2-Clause
|
||||
|
||||
qt_add_library(login STATIC)
|
||||
ecm_add_qml_module(login GENERATE_PLUGIN_SOURCE
|
||||
qt_add_qml_module(login
|
||||
URI org.kde.neochat.login
|
||||
OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/src/org/kde/neochat/login
|
||||
QML_FILES
|
||||
|
||||
@@ -13,7 +13,7 @@ LoginStep {
|
||||
id: root
|
||||
|
||||
FormCard.FormTextDelegate {
|
||||
text: i18n("Please wait while your messages are loaded from the server. This might take a little while.")
|
||||
text: i18n("Please wait. This might take a little while.")
|
||||
}
|
||||
FormCard.AbstractFormDelegate {
|
||||
contentItem: QQC2.BusyIndicator {}
|
||||
|
||||
@@ -92,7 +92,7 @@ FormCard.FormCardPage {
|
||||
}
|
||||
|
||||
QQC2.ToolButton {
|
||||
text: i18nc("@action:button", "Log out of this account")
|
||||
text: i18nc("@action:button", "Remove this account")
|
||||
icon.name: "edit-delete-remove"
|
||||
onClicked: Controller.removeConnection(modelData)
|
||||
display: QQC2.Button.IconOnly
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
#include "actionsmodel.h"
|
||||
|
||||
#include "chatbarcache.h"
|
||||
#include "controller.h"
|
||||
#include "neochatconnection.h"
|
||||
#include "neochatroom.h"
|
||||
#include "roommanager.h"
|
||||
@@ -24,15 +23,14 @@ QStringList rainbowColors{"#ff2b00"_ls, "#ff5500"_ls, "#ff8000"_ls, "#ffaa00"_ls
|
||||
|
||||
auto leaveRoomLambda = [](const QString &text, NeoChatRoom *room, ChatBarCache *) {
|
||||
if (text.isEmpty()) {
|
||||
Q_EMIT Controller::instance().showMessage(Controller::Info, i18n("Leaving this room."));
|
||||
Q_EMIT room->showMessage(NeoChatRoom::Info, i18n("Leaving this room."));
|
||||
room->connection()->leaveRoom(room);
|
||||
} else {
|
||||
QRegularExpression roomRegex(QStringLiteral(R"(^[#!][^:]+:\w(?:\w|\.|-)*\.\w+(?::\d{1,5})?)"));
|
||||
auto regexMatch = roomRegex.match(text);
|
||||
if (!regexMatch.hasMatch()) {
|
||||
Q_EMIT Controller::instance().showMessage(
|
||||
Controller::Error,
|
||||
i18nc("'<text>' does not look like a room id or alias.", "'%1' does not look like a room id or alias.", text));
|
||||
Q_EMIT room->showMessage(NeoChatRoom::Error,
|
||||
i18nc("'<text>' does not look like a room id or alias.", "'%1' does not look like a room id or alias.", text));
|
||||
return QString();
|
||||
}
|
||||
auto leaving = room->connection()->room(text);
|
||||
@@ -40,10 +38,10 @@ auto leaveRoomLambda = [](const QString &text, NeoChatRoom *room, ChatBarCache *
|
||||
leaving = room->connection()->roomByAlias(text);
|
||||
}
|
||||
if (leaving) {
|
||||
Q_EMIT Controller::instance().showMessage(Controller::Info, i18nc("Leaving room <roomname>.", "Leaving room %1.", text));
|
||||
Q_EMIT room->showMessage(NeoChatRoom::Info, i18nc("Leaving room <roomname>.", "Leaving room %1.", text));
|
||||
room->connection()->leaveRoom(leaving);
|
||||
} else {
|
||||
Q_EMIT Controller::instance().showMessage(Controller::Info, i18nc("Room <roomname> not found", "Room %1 not found.", text));
|
||||
Q_EMIT room->showMessage(NeoChatRoom::Info, i18nc("Room <roomname> not found", "Room %1 not found.", text));
|
||||
}
|
||||
}
|
||||
return QString();
|
||||
@@ -51,7 +49,7 @@ auto leaveRoomLambda = [](const QString &text, NeoChatRoom *room, ChatBarCache *
|
||||
|
||||
auto roomNickLambda = [](const QString &text, NeoChatRoom *room, ChatBarCache *) {
|
||||
if (text.isEmpty()) {
|
||||
Q_EMIT Controller::instance().showMessage(Controller::Error, i18n("No new nickname provided, no changes will happen."));
|
||||
Q_EMIT room->showMessage(NeoChatRoom::Error, i18n("No new nickname provided, no changes will happen."));
|
||||
} else {
|
||||
room->connection()->user()->rename(text, room);
|
||||
}
|
||||
@@ -193,31 +191,28 @@ QList<ActionsModel::Action> actions{
|
||||
QStringLiteral(R"((^|[][[:space:](){}`'";])([!#@][-a-z0-9_=#/.]{1,252}:\w(?:\w|\.|-)*\.\w+(?::\d{1,5})?))"));
|
||||
auto regexMatch = mxidRegex.match(text);
|
||||
if (!regexMatch.hasMatch()) {
|
||||
Q_EMIT Controller::instance().showMessage(Controller::Error,
|
||||
i18nc("'<text>' does not look like a matrix id.", "'%1' does not look like a matrix id.", text));
|
||||
Q_EMIT room->showMessage(NeoChatRoom::Error, i18nc("'<text>' does not look like a matrix id.", "'%1' does not look like a matrix id.", text));
|
||||
return QString();
|
||||
}
|
||||
const RoomMemberEvent *roomMemberEvent = room->currentState().get<RoomMemberEvent>(text);
|
||||
if (roomMemberEvent && roomMemberEvent->membership() == Membership::Invite) {
|
||||
Q_EMIT Controller::instance().showMessage(Controller::Info,
|
||||
i18nc("<user> is already invited to this room.", "%1 is already invited to this room.", text));
|
||||
Q_EMIT room->showMessage(NeoChatRoom::Info, i18nc("<user> is already invited to this room.", "%1 is already invited to this room.", text));
|
||||
return QString();
|
||||
}
|
||||
if (roomMemberEvent && roomMemberEvent->membership() == Membership::Ban) {
|
||||
Q_EMIT Controller::instance().showMessage(Controller::Info, i18nc("<user> is banned from this room.", "%1 is banned from this room.", text));
|
||||
Q_EMIT room->showMessage(NeoChatRoom::Info, i18nc("<user> is banned from this room.", "%1 is banned from this room.", text));
|
||||
return QString();
|
||||
}
|
||||
if (room->localMember().id() == text) {
|
||||
Q_EMIT Controller::instance().showMessage(Controller::Positive, i18n("You are already in this room."));
|
||||
Q_EMIT room->showMessage(NeoChatRoom::Positive, i18n("You are already in this room."));
|
||||
return QString();
|
||||
}
|
||||
if (room->members().contains(room->member(text))) {
|
||||
Q_EMIT Controller::instance().showMessage(Controller::Info, i18nc("<user> is already in this room.", "%1 is already in this room.", text));
|
||||
Q_EMIT room->showMessage(NeoChatRoom::Info, i18nc("<user> is already in this room.", "%1 is already in this room.", text));
|
||||
return QString();
|
||||
}
|
||||
room->inviteToRoom(text);
|
||||
Q_EMIT Controller::instance().showMessage(Controller::Positive,
|
||||
i18nc("<username> was invited into this room", "%1 was invited into this room", text));
|
||||
Q_EMIT room->showMessage(NeoChatRoom::Positive, i18nc("<username> was invited into this room", "%1 was invited into this room", text));
|
||||
return QString();
|
||||
},
|
||||
false,
|
||||
@@ -231,9 +226,8 @@ QList<ActionsModel::Action> actions{
|
||||
QRegularExpression roomRegex(QStringLiteral(R"(^[#!][^:]+:\w(?:\w|\.|-)*\.\w+(?::\d{1,5})?)"));
|
||||
auto regexMatch = roomRegex.match(text);
|
||||
if (!regexMatch.hasMatch()) {
|
||||
Q_EMIT Controller::instance().showMessage(
|
||||
Controller::Error,
|
||||
i18nc("'<text>' does not look like a room id or alias.", "'%1' does not look like a room id or alias.", text));
|
||||
Q_EMIT room->showMessage(NeoChatRoom::Error,
|
||||
i18nc("'<text>' does not look like a room id or alias.", "'%1' does not look like a room id or alias.", text));
|
||||
return QString();
|
||||
}
|
||||
auto targetRoom = text.startsWith(QLatin1Char('!')) ? room->connection()->room(text) : room->connection()->roomByAlias(text);
|
||||
@@ -241,7 +235,7 @@ QList<ActionsModel::Action> actions{
|
||||
RoomManager::instance().resolveResource(targetRoom->id());
|
||||
return QString();
|
||||
}
|
||||
Q_EMIT Controller::instance().showMessage(Controller::Info, i18nc("Joining room <roomname>.", "Joining room %1.", text));
|
||||
Q_EMIT room->showMessage(NeoChatRoom::Info, i18nc("Joining room <roomname>.", "Joining room %1.", text));
|
||||
RoomManager::instance().resolveResource(text, "join"_ls);
|
||||
return QString();
|
||||
},
|
||||
@@ -258,9 +252,8 @@ QList<ActionsModel::Action> actions{
|
||||
QRegularExpression roomRegex(QStringLiteral(R"(^[#!][^:]+:\w(?:\w|\.|-)*\.\w+(?::\d{1,5})?)"));
|
||||
auto regexMatch = roomRegex.match(roomName);
|
||||
if (!regexMatch.hasMatch()) {
|
||||
Q_EMIT Controller::instance().showMessage(
|
||||
Controller::Error,
|
||||
i18nc("'<text>' does not look like a room id or alias.", "'%1' does not look like a room id or alias.", text));
|
||||
Q_EMIT room->showMessage(NeoChatRoom::Error,
|
||||
i18nc("'<text>' does not look like a room id or alias.", "'%1' does not look like a room id or alias.", text));
|
||||
return QString();
|
||||
}
|
||||
auto targetRoom = text.startsWith(QLatin1Char('!')) ? room->connection()->room(text) : room->connection()->roomByAlias(text);
|
||||
@@ -268,7 +261,7 @@ QList<ActionsModel::Action> actions{
|
||||
RoomManager::instance().resolveResource(targetRoom->id());
|
||||
return QString();
|
||||
}
|
||||
Q_EMIT Controller::instance().showMessage(Controller::Info, i18nc("Knocking room <roomname>.", "Knocking room %1.", text));
|
||||
Q_EMIT room->showMessage(NeoChatRoom::Info, i18nc("Knocking room <roomname>.", "Knocking room %1.", text));
|
||||
auto connection = dynamic_cast<NeoChatConnection *>(room->connection());
|
||||
const auto knownServer = roomName.mid(roomName.indexOf(":"_ls) + 1);
|
||||
if (parts.length() >= 2) {
|
||||
@@ -289,16 +282,15 @@ QList<ActionsModel::Action> actions{
|
||||
QRegularExpression roomRegex(QStringLiteral(R"(^[#!][^:]+:\w(?:\w|\.|-)*\.\w+(?::\d{1,5})?)"));
|
||||
auto regexMatch = roomRegex.match(text);
|
||||
if (!regexMatch.hasMatch()) {
|
||||
Q_EMIT Controller::instance().showMessage(
|
||||
Controller::Error,
|
||||
i18nc("'<text>' does not look like a room id or alias.", "'%1' does not look like a room id or alias.", text));
|
||||
Q_EMIT room->showMessage(NeoChatRoom::Error,
|
||||
i18nc("'<text>' does not look like a room id or alias.", "'%1' does not look like a room id or alias.", text));
|
||||
return QString();
|
||||
}
|
||||
if (room->connection()->room(text) || room->connection()->roomByAlias(text)) {
|
||||
Q_EMIT Controller::instance().showMessage(Controller::Info, i18nc("You are already in room <roomname>.", "You are already in room %1.", text));
|
||||
Q_EMIT room->showMessage(NeoChatRoom::Info, i18nc("You are already in room <roomname>.", "You are already in room %1.", text));
|
||||
return QString();
|
||||
}
|
||||
Q_EMIT Controller::instance().showMessage(Controller::Info, i18nc("Joining room <roomname>.", "Joining room %1.", text));
|
||||
Q_EMIT room->showMessage(NeoChatRoom::Info, i18nc("Joining room <roomname>.", "Joining room %1.", text));
|
||||
RoomManager::instance().resolveResource(text, "join"_ls);
|
||||
return QString();
|
||||
},
|
||||
@@ -327,7 +319,7 @@ QList<ActionsModel::Action> actions{
|
||||
QStringLiteral("nick"),
|
||||
[](const QString &text, NeoChatRoom *room, ChatBarCache *) {
|
||||
if (text.isEmpty()) {
|
||||
Q_EMIT Controller::instance().showMessage(Controller::Error, i18n("No new nickname provided, no changes will happen."));
|
||||
Q_EMIT room->showMessage(NeoChatRoom::Error, i18n("No new nickname provided, no changes will happen."));
|
||||
} else {
|
||||
room->connection()->user()->rename(text);
|
||||
}
|
||||
@@ -361,17 +353,15 @@ QList<ActionsModel::Action> actions{
|
||||
QStringLiteral(R"((^|[][[:space:](){}`'";])([!#@][-a-z0-9_=#/.]{1,252}:\w(?:\w|\.|-)*\.\w+(?::\d{1,5})?))"));
|
||||
auto regexMatch = mxidRegex.match(text);
|
||||
if (!regexMatch.hasMatch()) {
|
||||
Q_EMIT Controller::instance().showMessage(Controller::Error,
|
||||
i18nc("'<text>' does not look like a matrix id.", "'%1' does not look like a matrix id.", text));
|
||||
Q_EMIT room->showMessage(NeoChatRoom::Error, i18nc("'<text>' does not look like a matrix id.", "'%1' does not look like a matrix id.", text));
|
||||
return QString();
|
||||
}
|
||||
if (room->connection()->ignoredUsers().contains(text)) {
|
||||
Q_EMIT Controller::instance().showMessage(Controller::Info, i18nc("<username> is already ignored.", "%1 is already ignored.", text));
|
||||
Q_EMIT room->showMessage(NeoChatRoom::Info, i18nc("<username> is already ignored.", "%1 is already ignored.", text));
|
||||
return QString();
|
||||
}
|
||||
room->connection()->addToIgnoredUsers(text);
|
||||
Q_EMIT Controller::instance().showMessage(Controller::Positive, i18nc("<username> is now ignored", "%1 is now ignored.", text));
|
||||
|
||||
Q_EMIT room->showMessage(NeoChatRoom::Positive, i18nc("<username> is now ignored", "%1 is now ignored.", text));
|
||||
return QString();
|
||||
},
|
||||
false,
|
||||
@@ -386,16 +376,15 @@ QList<ActionsModel::Action> actions{
|
||||
QStringLiteral(R"((^|[][[:space:](){}`'";])([!#@][-a-z0-9_=#/.]{1,252}:\w(?:\w|\.|-)*\.\w+(?::\d{1,5})?))"));
|
||||
auto regexMatch = mxidRegex.match(text);
|
||||
if (!regexMatch.hasMatch()) {
|
||||
Q_EMIT Controller::instance().showMessage(Controller::Error,
|
||||
i18nc("'<text>' does not look like a matrix id.", "'%1' does not look like a matrix id.", text));
|
||||
Q_EMIT room->showMessage(NeoChatRoom::Error, i18nc("'<text>' does not look like a matrix id.", "'%1' does not look like a matrix id.", text));
|
||||
return QString();
|
||||
}
|
||||
if (!room->connection()->ignoredUsers().contains(text)) {
|
||||
Q_EMIT Controller::instance().showMessage(Controller::Info, i18nc("<username> is not ignored.", "%1 is not ignored.", text));
|
||||
Q_EMIT room->showMessage(NeoChatRoom::Info, i18nc("<username> is not ignored.", "%1 is not ignored.", text));
|
||||
return QString();
|
||||
}
|
||||
room->connection()->removeFromIgnoredUsers(text);
|
||||
Q_EMIT Controller::instance().showMessage(Controller::Positive, i18nc("<username> is no longer ignored.", "%1 is no longer ignored.", text));
|
||||
Q_EMIT room->showMessage(NeoChatRoom::Positive, i18nc("<username> is no longer ignored.", "%1 is no longer ignored.", text));
|
||||
return QString();
|
||||
},
|
||||
false,
|
||||
@@ -431,14 +420,12 @@ QList<ActionsModel::Action> actions{
|
||||
QStringLiteral(R"((^|[][[:space:](){}`'";])([!#@][-a-z0-9_=#/.]{1,252}:\w(?:\w|\.|-)*\.\w+(?::\d{1,5})?))"));
|
||||
auto regexMatch = mxidRegex.match(parts[0]);
|
||||
if (!regexMatch.hasMatch()) {
|
||||
Q_EMIT Controller::instance().showMessage(Controller::Error,
|
||||
i18nc("'<text>' does not look like a matrix id.", "'%1' does not look like a matrix id.", text));
|
||||
Q_EMIT room->showMessage(NeoChatRoom::Error, i18nc("'<text>' does not look like a matrix id.", "'%1' does not look like a matrix id.", text));
|
||||
return QString();
|
||||
}
|
||||
auto state = room->currentState().get<RoomMemberEvent>(parts[0]);
|
||||
if (state && state->membership() == Membership::Ban) {
|
||||
Q_EMIT Controller::instance().showMessage(Controller::Info,
|
||||
i18nc("<user> is already banned from this room.", "%1 is already banned from this room.", text));
|
||||
Q_EMIT room->showMessage(NeoChatRoom::Info, i18nc("<user> is already banned from this room.", "%1 is already banned from this room.", text));
|
||||
return QString();
|
||||
}
|
||||
auto plEvent = room->currentState().get<RoomPowerLevelsEvent>();
|
||||
@@ -446,18 +433,17 @@ QList<ActionsModel::Action> actions{
|
||||
return QString();
|
||||
}
|
||||
if (plEvent->ban() > plEvent->powerLevelForUser(room->localMember().id())) {
|
||||
Q_EMIT Controller::instance().showMessage(Controller::Error, i18n("You are not allowed to ban users from this room."));
|
||||
Q_EMIT room->showMessage(NeoChatRoom::Error, i18n("You are not allowed to ban users from this room."));
|
||||
return QString();
|
||||
}
|
||||
if (plEvent->powerLevelForUser(room->localMember().id()) <= plEvent->powerLevelForUser(parts[0])) {
|
||||
Q_EMIT Controller::instance().showMessage(
|
||||
Controller::Error,
|
||||
Q_EMIT room->showMessage(
|
||||
NeoChatRoom::Error,
|
||||
i18nc("You are not allowed to ban <username> from this room.", "You are not allowed to ban %1 from this room.", parts[0]));
|
||||
return QString();
|
||||
}
|
||||
room->ban(parts[0], parts.size() > 1 ? parts.mid(1).join(QLatin1Char(' ')) : QString());
|
||||
Q_EMIT Controller::instance().showMessage(Controller::Positive,
|
||||
i18nc("<username> was banned from this room.", "%1 was banned from this room.", parts[0]));
|
||||
Q_EMIT room->showMessage(NeoChatRoom::Positive, i18nc("<username> was banned from this room.", "%1 was banned from this room.", parts[0]));
|
||||
return QString();
|
||||
},
|
||||
false,
|
||||
@@ -472,8 +458,7 @@ QList<ActionsModel::Action> actions{
|
||||
QStringLiteral(R"((^|[][[:space:](){}`'";])([!#@][-a-z0-9_=#/.]{1,252}:\w(?:\w|\.|-)*\.\w+(?::\d{1,5})?))"));
|
||||
auto regexMatch = mxidRegex.match(text);
|
||||
if (!regexMatch.hasMatch()) {
|
||||
Q_EMIT Controller::instance().showMessage(Controller::Error,
|
||||
i18nc("'<text>' does not look like a matrix id.", "'%1' does not look like a matrix id.", text));
|
||||
Q_EMIT room->showMessage(NeoChatRoom::Error, i18nc("'<text>' does not look like a matrix id.", "'%1' does not look like a matrix id.", text));
|
||||
return QString();
|
||||
}
|
||||
auto plEvent = room->currentState().get<RoomPowerLevelsEvent>();
|
||||
@@ -481,18 +466,16 @@ QList<ActionsModel::Action> actions{
|
||||
return QString();
|
||||
}
|
||||
if (plEvent->ban() > plEvent->powerLevelForUser(room->localMember().id())) {
|
||||
Q_EMIT Controller::instance().showMessage(Controller::Error, i18n("You are not allowed to unban users from this room."));
|
||||
Q_EMIT room->showMessage(NeoChatRoom::Error, i18n("You are not allowed to unban users from this room."));
|
||||
return QString();
|
||||
}
|
||||
auto state = room->currentState().get<RoomMemberEvent>(text);
|
||||
if (state && state->membership() != Membership::Ban) {
|
||||
Q_EMIT Controller::instance().showMessage(Controller::Info,
|
||||
i18nc("<user> is not banned from this room.", "%1 is not banned from this room.", text));
|
||||
Q_EMIT room->showMessage(NeoChatRoom::Info, i18nc("<user> is not banned from this room.", "%1 is not banned from this room.", text));
|
||||
return QString();
|
||||
}
|
||||
room->unban(text);
|
||||
Q_EMIT Controller::instance().showMessage(Controller::Positive,
|
||||
i18nc("<username> was unbanned from this room.", "%1 was unbanned from this room.", text));
|
||||
Q_EMIT room->showMessage(NeoChatRoom::Positive, i18nc("<username> was unbanned from this room.", "%1 was unbanned from this room.", text));
|
||||
|
||||
return QString();
|
||||
},
|
||||
@@ -509,16 +492,16 @@ QList<ActionsModel::Action> actions{
|
||||
QStringLiteral(R"((^|[][[:space:](){}`'";])([!#@][-a-z0-9_=#/.]{1,252}:\w(?:\w|\.|-)*\.\w+(?::\d{1,5})?))"));
|
||||
auto regexMatch = mxidRegex.match(parts[0]);
|
||||
if (!regexMatch.hasMatch()) {
|
||||
Q_EMIT Controller::instance().showMessage(Controller::Error,
|
||||
i18nc("'<text>' does not look like a matrix id.", "'%1' does not look like a matrix id.", parts[0]));
|
||||
Q_EMIT room->showMessage(NeoChatRoom::Error,
|
||||
i18nc("'<text>' does not look like a matrix id.", "'%1' does not look like a matrix id.", parts[0]));
|
||||
return QString();
|
||||
}
|
||||
if (parts[0] == room->localMember().id()) {
|
||||
Q_EMIT Controller::instance().showMessage(Controller::Error, i18n("You cannot kick yourself from the room."));
|
||||
Q_EMIT room->showMessage(NeoChatRoom::Error, i18n("You cannot kick yourself from the room."));
|
||||
return QString();
|
||||
}
|
||||
if (!room->isMember(parts[0])) {
|
||||
Q_EMIT Controller::instance().showMessage(Controller::Error, i18nc("<username> is not in this room", "%1 is not in this room.", parts[0]));
|
||||
Q_EMIT room->showMessage(NeoChatRoom::Error, i18nc("<username> is not in this room", "%1 is not in this room.", parts[0]));
|
||||
return QString();
|
||||
}
|
||||
auto plEvent = room->currentState().get<RoomPowerLevelsEvent>();
|
||||
@@ -527,18 +510,17 @@ QList<ActionsModel::Action> actions{
|
||||
}
|
||||
auto kick = plEvent->kick();
|
||||
if (plEvent->powerLevelForUser(room->localMember().id()) < kick) {
|
||||
Q_EMIT Controller::instance().showMessage(Controller::Error, i18n("You are not allowed to kick users from this room."));
|
||||
Q_EMIT room->showMessage(NeoChatRoom::Error, i18n("You are not allowed to kick users from this room."));
|
||||
return QString();
|
||||
}
|
||||
if (plEvent->powerLevelForUser(room->localMember().id()) <= plEvent->powerLevelForUser(parts[0])) {
|
||||
Q_EMIT Controller::instance().showMessage(
|
||||
Controller::Error,
|
||||
Q_EMIT room->showMessage(
|
||||
NeoChatRoom::Error,
|
||||
i18nc("You are not allowed to kick <username> from this room", "You are not allowed to kick %1 from this room.", parts[0]));
|
||||
return QString();
|
||||
}
|
||||
room->kickMember(parts[0], parts.size() > 1 ? parts.mid(1).join(QLatin1Char(' ')) : QString());
|
||||
Q_EMIT Controller::instance().showMessage(Controller::Positive,
|
||||
i18nc("<username> was kicked from this room.", "%1 was kicked from this room.", parts[0]));
|
||||
Q_EMIT room->showMessage(NeoChatRoom::Positive, i18nc("<username> was kicked from this room.", "%1 was kicked from this room.", parts[0]));
|
||||
return QString();
|
||||
},
|
||||
false,
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
#include <Quotient/events/roommessageevent.h>
|
||||
#include <Quotient/room.h>
|
||||
|
||||
#include "messagecontentmodel.h"
|
||||
#include "messageeventmodel.h"
|
||||
#include "messagefiltermodel.h"
|
||||
|
||||
@@ -71,14 +70,6 @@ QVariant MediaMessageFilterModel::data(const QModelIndex &index, int role) const
|
||||
const auto previousEventDay = mapToSource(this->index(index.row() + 1, 0)).data(MessageEventModel::TimeRole).toDateTime().toLocalTime().date();
|
||||
return day != previousEventDay;
|
||||
}
|
||||
// Catch and force the author to be shown for all rows
|
||||
if (role == MessageEventModel::ContentModelRole) {
|
||||
const auto model = qvariant_cast<MessageContentModel *>(mapToSource(index).data(MessageEventModel::ContentModelRole));
|
||||
if (model != nullptr) {
|
||||
model->setShowAuthor(true);
|
||||
}
|
||||
return QVariant::fromValue<MessageContentModel *>(model);
|
||||
}
|
||||
|
||||
return sourceModel()->data(mapToSource(index), role);
|
||||
}
|
||||
|
||||
@@ -30,23 +30,20 @@
|
||||
|
||||
using namespace Quotient;
|
||||
|
||||
MessageContentModel::MessageContentModel(NeoChatRoom *room, const Quotient::RoomEvent *event, bool isReply, bool isPending)
|
||||
MessageContentModel::MessageContentModel(NeoChatRoom *room, const Quotient::RoomEvent *event, bool isReply)
|
||||
: QAbstractListModel(nullptr)
|
||||
, m_room(room)
|
||||
, m_eventId(event != nullptr ? event->id() : QString())
|
||||
, m_eventSenderId(event != nullptr ? event->senderId() : QString())
|
||||
, m_event(loadEvent<RoomEvent>(event->fullJson()))
|
||||
, m_isPending(isPending)
|
||||
, m_event(event)
|
||||
, m_isReply(isReply)
|
||||
{
|
||||
initializeModel();
|
||||
}
|
||||
|
||||
MessageContentModel::MessageContentModel(NeoChatRoom *room, const QString &eventId, bool isReply, bool isPending)
|
||||
MessageContentModel::MessageContentModel(NeoChatRoom *room, const QString &eventId, bool isReply)
|
||||
: QAbstractListModel(nullptr)
|
||||
, m_room(room)
|
||||
, m_eventId(eventId)
|
||||
, m_isPending(isPending)
|
||||
, m_isReply(isReply)
|
||||
{
|
||||
initializeModel();
|
||||
@@ -62,10 +59,10 @@ void MessageContentModel::initializeModel()
|
||||
Quotient::connectUntil(m_room.get(), &NeoChatRoom::extraEventLoaded, this, [this](const QString &eventId) {
|
||||
if (m_room != nullptr) {
|
||||
if (eventId == m_eventId) {
|
||||
m_event = loadEvent<RoomEvent>(m_room->getEvent(eventId)->fullJson());
|
||||
m_event = m_room->getEvent(eventId);
|
||||
Q_EMIT eventUpdated();
|
||||
updateReplyModel();
|
||||
resetContent();
|
||||
updateComponents();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -78,10 +75,9 @@ void MessageContentModel::initializeModel()
|
||||
|
||||
connect(m_room, &NeoChatRoom::pendingEventAboutToMerge, this, [this](Quotient::RoomEvent *serverEvent) {
|
||||
if (m_room != nullptr && m_event != nullptr) {
|
||||
if (m_eventId == serverEvent->id()) {
|
||||
if (m_event->id() == serverEvent->id()) {
|
||||
beginResetModel();
|
||||
m_isPending = false;
|
||||
m_event = loadEvent<RoomEvent>(serverEvent->fullJson());
|
||||
m_event = serverEvent;
|
||||
Q_EMIT eventUpdated();
|
||||
endResetModel();
|
||||
}
|
||||
@@ -89,27 +85,27 @@ void MessageContentModel::initializeModel()
|
||||
});
|
||||
connect(m_room, &NeoChatRoom::replacedEvent, this, [this](const Quotient::RoomEvent *newEvent) {
|
||||
if (m_room != nullptr && m_event != nullptr) {
|
||||
if (m_eventId == newEvent->id()) {
|
||||
if (m_event->id() == newEvent->id()) {
|
||||
beginResetModel();
|
||||
m_event = loadEvent<RoomEvent>(newEvent->fullJson());
|
||||
m_event = newEvent;
|
||||
Q_EMIT eventUpdated();
|
||||
endResetModel();
|
||||
}
|
||||
}
|
||||
});
|
||||
connect(m_room, &NeoChatRoom::newFileTransfer, this, [this](const QString &eventId) {
|
||||
if (m_event != nullptr && eventId == m_eventId) {
|
||||
if (m_event != nullptr && eventId == m_event->id()) {
|
||||
Q_EMIT dataChanged(index(0), index(rowCount() - 1), {FileTransferInfoRole});
|
||||
}
|
||||
});
|
||||
connect(m_room, &NeoChatRoom::fileTransferProgress, this, [this](const QString &eventId) {
|
||||
if (m_event != nullptr && eventId == m_eventId) {
|
||||
if (m_event != nullptr && eventId == m_event->id()) {
|
||||
Q_EMIT dataChanged(index(0), index(rowCount() - 1), {FileTransferInfoRole});
|
||||
}
|
||||
});
|
||||
connect(m_room, &NeoChatRoom::fileTransferCompleted, this, [this](const QString &eventId) {
|
||||
if (m_event != nullptr && eventId == m_eventId) {
|
||||
resetContent();
|
||||
if (m_event != nullptr && eventId == m_event->id()) {
|
||||
updateComponents();
|
||||
Q_EMIT dataChanged(index(0), index(rowCount() - 1), {FileTransferInfoRole});
|
||||
|
||||
QString mxcUrl;
|
||||
@@ -123,76 +119,36 @@ void MessageContentModel::initializeModel()
|
||||
if (mxcUrl.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
auto localPath = m_room->fileTransferInfo(m_eventId).localPath.toLocalFile();
|
||||
auto localPath = m_room->fileTransferInfo(m_event->id()).localPath.toLocalFile();
|
||||
auto config = KSharedConfig::openStateConfig(QStringLiteral("neochatdownloads"))->group(QStringLiteral("downloads"));
|
||||
config.writePathEntry(mxcUrl.mid(6), localPath);
|
||||
}
|
||||
});
|
||||
connect(m_room, &NeoChatRoom::fileTransferFailed, this, [this](const QString &eventId) {
|
||||
if (m_event != nullptr && eventId == m_eventId) {
|
||||
resetContent();
|
||||
if (m_event != nullptr && eventId == m_event->id()) {
|
||||
updateComponents();
|
||||
Q_EMIT dataChanged(index(0), index(rowCount() - 1), {FileTransferInfoRole});
|
||||
}
|
||||
});
|
||||
connect(m_room->editCache(), &ChatBarCache::relationIdChanged, this, [this](const QString &oldEventId, const QString &newEventId) {
|
||||
if (m_event != nullptr && (oldEventId == m_eventId || newEventId == m_eventId)) {
|
||||
if (m_event != nullptr && (oldEventId == m_event->id() || newEventId == m_event->id())) {
|
||||
// HACK: Because DelegateChooser can't switch the delegate on dataChanged it has to think there is a new delegate.
|
||||
beginResetModel();
|
||||
resetContent(newEventId == m_eventId);
|
||||
updateComponents(newEventId == m_event->id());
|
||||
endResetModel();
|
||||
}
|
||||
});
|
||||
connect(m_room, &NeoChatRoom::urlPreviewEnabledChanged, this, [this]() {
|
||||
resetContent();
|
||||
updateComponents();
|
||||
});
|
||||
connect(NeoChatConfig::self(), &NeoChatConfig::ShowLinkPreviewChanged, this, [this]() {
|
||||
resetContent();
|
||||
});
|
||||
connect(m_room, &Room::memberNameUpdated, this, [this](RoomMember member) {
|
||||
if (m_room != nullptr && m_event != nullptr) {
|
||||
if (m_eventSenderId.isEmpty() || m_eventSenderId == member.id()) {
|
||||
Q_EMIT dataChanged(index(0, 0), index(rowCount() - 1, 0), {AuthorRole});
|
||||
}
|
||||
}
|
||||
});
|
||||
connect(m_room, &Room::memberAvatarUpdated, this, [this](RoomMember member) {
|
||||
if (m_room != nullptr && m_event != nullptr) {
|
||||
if (m_eventSenderId.isEmpty() || m_eventSenderId == member.id()) {
|
||||
Q_EMIT dataChanged(index(0, 0), index(rowCount() - 1, 0), {AuthorRole});
|
||||
}
|
||||
}
|
||||
updateComponents();
|
||||
});
|
||||
|
||||
if (m_event != nullptr) {
|
||||
updateReplyModel();
|
||||
}
|
||||
resetModel();
|
||||
}
|
||||
|
||||
bool MessageContentModel::showAuthor() const
|
||||
{
|
||||
return m_showAuthor;
|
||||
}
|
||||
|
||||
void MessageContentModel::setShowAuthor(bool showAuthor)
|
||||
{
|
||||
if (showAuthor == m_showAuthor) {
|
||||
return;
|
||||
}
|
||||
m_showAuthor = showAuthor;
|
||||
|
||||
if (m_event != nullptr) {
|
||||
if (showAuthor) {
|
||||
beginInsertRows({}, 0, 0);
|
||||
m_components.prepend(MessageComponent{MessageComponentType::Author, QString(), {}});
|
||||
endInsertRows();
|
||||
} else {
|
||||
beginRemoveRows({}, 0, 0);
|
||||
m_components.remove(0, 1);
|
||||
endRemoveRows();
|
||||
}
|
||||
}
|
||||
Q_EMIT showAuthorChanged();
|
||||
updateComponents();
|
||||
}
|
||||
|
||||
static LinkPreviewer *emptyLinkPreview = new LinkPreviewer;
|
||||
@@ -208,7 +164,7 @@ QVariant MessageContentModel::data(const QModelIndex &index, int role) const
|
||||
return {};
|
||||
}
|
||||
|
||||
EventHandler eventHandler(m_room, m_event.get());
|
||||
EventHandler eventHandler(m_room, m_event);
|
||||
const auto component = m_components[index.row()];
|
||||
|
||||
if (role == DisplayRole) {
|
||||
@@ -237,24 +193,8 @@ QVariant MessageContentModel::data(const QModelIndex &index, int role) const
|
||||
if (role == EventIdRole) {
|
||||
return eventHandler.getId();
|
||||
}
|
||||
if (role == TimeRole) {
|
||||
const auto pendingIt = std::find_if(m_room->pendingEvents().cbegin(), m_room->pendingEvents().cend(), [this](const PendingEventItem &pendingEvent) {
|
||||
return m_event->transactionId() == pendingEvent->transactionId();
|
||||
});
|
||||
|
||||
auto lastUpdated = pendingIt == m_room->pendingEvents().cend() ? QDateTime() : pendingIt->lastUpdated();
|
||||
return eventHandler.getTime(m_isPending, lastUpdated);
|
||||
}
|
||||
if (role == TimeStringRole) {
|
||||
const auto pendingIt = std::find_if(m_room->pendingEvents().cbegin(), m_room->pendingEvents().cend(), [this](const PendingEventItem &pendingEvent) {
|
||||
return m_event->transactionId() == pendingEvent->transactionId();
|
||||
});
|
||||
|
||||
auto lastUpdated = pendingIt == m_room->pendingEvents().cend() ? QDateTime() : pendingIt->lastUpdated();
|
||||
return eventHandler.getTimeString(false, QLocale::ShortFormat, m_isPending, lastUpdated);
|
||||
}
|
||||
if (role == AuthorRole) {
|
||||
return QVariant::fromValue(eventHandler.getAuthor(m_isPending));
|
||||
return QVariant::fromValue(eventHandler.getAuthor(false));
|
||||
}
|
||||
if (role == MediaInfoRole) {
|
||||
return eventHandler.getMediaInfo();
|
||||
@@ -275,7 +215,7 @@ QVariant MessageContentModel::data(const QModelIndex &index, int role) const
|
||||
return eventHandler.getLocationAssetType();
|
||||
}
|
||||
if (role == PollHandlerRole) {
|
||||
return QVariant::fromValue<PollHandler *>(m_room->poll(m_eventId));
|
||||
return QVariant::fromValue<PollHandler *>(m_room->poll(m_event->id()));
|
||||
}
|
||||
if (role == IsReplyRole) {
|
||||
return eventHandler.hasReply();
|
||||
@@ -314,8 +254,6 @@ QHash<int, QByteArray> MessageContentModel::roleNames() const
|
||||
roles[ComponentTypeRole] = "componentType";
|
||||
roles[ComponentAttributesRole] = "componentAttributes";
|
||||
roles[EventIdRole] = "eventId";
|
||||
roles[TimeRole] = "time";
|
||||
roles[TimeStringRole] = "timeString";
|
||||
roles[AuthorRole] = "author";
|
||||
roles[MediaInfoRole] = "mediaInfo";
|
||||
roles[FileTransferInfoRole] = "fileTransferInfo";
|
||||
@@ -332,7 +270,7 @@ QHash<int, QByteArray> MessageContentModel::roleNames() const
|
||||
return roles;
|
||||
}
|
||||
|
||||
void MessageContentModel::resetModel()
|
||||
void MessageContentModel::updateComponents(bool isEditing)
|
||||
{
|
||||
beginResetModel();
|
||||
m_components.clear();
|
||||
@@ -343,63 +281,35 @@ void MessageContentModel::resetModel()
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_showAuthor) {
|
||||
m_components += MessageComponent{MessageComponentType::Author, QString(), {}};
|
||||
}
|
||||
|
||||
m_components += messageContentComponents();
|
||||
endResetModel();
|
||||
}
|
||||
|
||||
void MessageContentModel::resetContent(bool isEditing)
|
||||
{
|
||||
Q_ASSERT(m_event != nullptr);
|
||||
|
||||
const auto startRow = m_components[0].type == MessageComponentType::Author ? 1 : 0;
|
||||
beginRemoveRows({}, startRow, rowCount() - 1);
|
||||
m_components.remove(startRow, rowCount() - startRow);
|
||||
endRemoveRows();
|
||||
|
||||
const auto newComponents = messageContentComponents(isEditing);
|
||||
if (newComponents.size() == 0) {
|
||||
return;
|
||||
}
|
||||
beginInsertRows({}, startRow, startRow + newComponents.size() - 1);
|
||||
m_components += newComponents;
|
||||
endInsertRows();
|
||||
}
|
||||
|
||||
QList<MessageComponent> MessageContentModel::messageContentComponents(bool isEditing)
|
||||
{
|
||||
QList<MessageComponent> newComponents;
|
||||
|
||||
if (eventCast<const Quotient::RoomMessageEvent>(m_event)
|
||||
&& eventCast<const Quotient::RoomMessageEvent>(m_event)->rawMsgtype() == QStringLiteral("m.key.verification.request")) {
|
||||
newComponents += MessageComponent{MessageComponentType::Verification, QString(), {}};
|
||||
return newComponents;
|
||||
m_components += MessageComponent{MessageComponentType::Verification, QString(), {}};
|
||||
endResetModel();
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_event->isRedacted()) {
|
||||
newComponents += MessageComponent{MessageComponentType::Text, QString(), {}};
|
||||
return newComponents;
|
||||
m_components += MessageComponent{MessageComponentType::Text, QString(), {}};
|
||||
endResetModel();
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_replyModel != nullptr) {
|
||||
newComponents += MessageComponent{MessageComponentType::Reply, QString(), {}};
|
||||
m_components += MessageComponent{MessageComponentType::Reply, QString(), {}};
|
||||
}
|
||||
|
||||
if (isEditing) {
|
||||
newComponents += MessageComponent{MessageComponentType::Edit, QString(), {}};
|
||||
m_components += MessageComponent{MessageComponentType::Edit, QString(), {}};
|
||||
} else {
|
||||
EventHandler eventHandler(m_room, m_event.get());
|
||||
newComponents.append(componentsForType(eventHandler.messageComponentType()));
|
||||
EventHandler eventHandler(m_room, m_event);
|
||||
m_components.append(componentsForType(eventHandler.messageComponentType()));
|
||||
}
|
||||
|
||||
if (m_room->urlPreviewEnabled()) {
|
||||
newComponents = addLinkPreviews(newComponents);
|
||||
addLinkPreviews();
|
||||
}
|
||||
|
||||
return newComponents;
|
||||
endResetModel();
|
||||
}
|
||||
|
||||
void MessageContentModel::updateReplyModel()
|
||||
@@ -408,7 +318,7 @@ void MessageContentModel::updateReplyModel()
|
||||
return;
|
||||
}
|
||||
|
||||
EventHandler eventHandler(m_room, m_event.get());
|
||||
EventHandler eventHandler(m_room, m_event);
|
||||
if (!eventHandler.hasReply()) {
|
||||
return;
|
||||
}
|
||||
@@ -439,7 +349,6 @@ QList<MessageComponent> MessageContentModel::componentsForType(MessageComponentT
|
||||
const auto event = eventCast<const Quotient::RoomMessageEvent>(m_event);
|
||||
auto body = EventHandler::rawMessageBody(*event);
|
||||
components += TextHandler().textComponents(body, EventHandler::messageBodyInputFormat(*event), m_room, event, event->isReplaced());
|
||||
|
||||
if (m_emptyItinerary) {
|
||||
auto fileTransferInfo = fileInfo();
|
||||
|
||||
@@ -450,8 +359,8 @@ QList<MessageComponent> MessageContentModel::componentsForType(MessageComponentT
|
||||
QFile file(fileTransferInfo.localPath.path());
|
||||
file.open(QIODevice::ReadOnly);
|
||||
components += MessageComponent{MessageComponentType::Code,
|
||||
QString::fromStdString(file.readAll().toStdString()),
|
||||
{{QStringLiteral("class"), definitionForFile.name()}}};
|
||||
QString::fromStdString(file.readAll().toStdString()),
|
||||
{{QStringLiteral("class"), definitionForFile.name()}}};
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -459,13 +368,11 @@ QList<MessageComponent> MessageContentModel::componentsForType(MessageComponentT
|
||||
QImageReader reader(fileTransferInfo.localPath.path());
|
||||
components += MessageComponent{MessageComponentType::Pdf, QString(), {{QStringLiteral("size"), reader.size()}}};
|
||||
}
|
||||
} else if (m_itineraryModel != nullptr) {
|
||||
components += MessageComponent{MessageComponentType::Itinerary, QString(), {}};
|
||||
if (m_itineraryModel->rowCount() > 0) {
|
||||
updateItineraryModel();
|
||||
}
|
||||
} else {
|
||||
updateItineraryModel();
|
||||
if (m_itineraryModel != nullptr) {
|
||||
components += MessageComponent{MessageComponentType::Itinerary, QString(), {}};
|
||||
}
|
||||
}
|
||||
return components;
|
||||
}
|
||||
@@ -511,26 +418,24 @@ MessageComponent MessageContentModel::linkPreviewComponent(const QUrl &link)
|
||||
}
|
||||
}
|
||||
|
||||
QList<MessageComponent> MessageContentModel::addLinkPreviews(QList<MessageComponent> inputComponents)
|
||||
void MessageContentModel::addLinkPreviews()
|
||||
{
|
||||
int i = 0;
|
||||
while (i < inputComponents.size()) {
|
||||
const auto component = inputComponents.at(i);
|
||||
while (i < m_components.size()) {
|
||||
const auto component = m_components.at(i);
|
||||
if (component.type == MessageComponentType::Text || component.type == MessageComponentType::Quote) {
|
||||
if (LinkPreviewer::hasPreviewableLinks(component.content)) {
|
||||
const auto links = LinkPreviewer::linkPreviews(component.content);
|
||||
for (qsizetype j = 0; j < links.size(); ++j) {
|
||||
const auto linkPreview = linkPreviewComponent(links[j]);
|
||||
if (!m_removedLinkPreviews.contains(links[j]) && !linkPreview.isEmpty()) {
|
||||
inputComponents.insert(i + j + 1, linkPreview);
|
||||
m_components.insert(i + j + 1, linkPreview);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
i++;
|
||||
}
|
||||
|
||||
return inputComponents;
|
||||
}
|
||||
|
||||
void MessageContentModel::closeLinkPreview(int row)
|
||||
@@ -540,7 +445,8 @@ void MessageContentModel::closeLinkPreview(int row)
|
||||
m_removedLinkPreviews += m_components[row].attributes["link"_ls].toUrl();
|
||||
m_components.remove(row);
|
||||
m_components.squeeze();
|
||||
resetContent();
|
||||
updateComponents();
|
||||
endResetModel();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -561,17 +467,17 @@ void MessageContentModel::updateItineraryModel()
|
||||
m_itineraryModel = new ItineraryModel(this);
|
||||
connect(m_itineraryModel, &ItineraryModel::loaded, this, [this]() {
|
||||
if (m_itineraryModel->rowCount() == 0) {
|
||||
m_emptyItinerary = true;
|
||||
m_itineraryModel->deleteLater();
|
||||
m_itineraryModel = nullptr;
|
||||
resetContent();
|
||||
m_emptyItinerary = true;
|
||||
updateComponents();
|
||||
}
|
||||
});
|
||||
connect(m_itineraryModel, &ItineraryModel::loadErrorOccurred, this, [this]() {
|
||||
m_emptyItinerary = true;
|
||||
m_itineraryModel->deleteLater();
|
||||
m_itineraryModel = nullptr;
|
||||
resetContent();
|
||||
m_emptyItinerary = true;
|
||||
updateComponents();
|
||||
});
|
||||
}
|
||||
m_itineraryModel->setPath(filePath.toString());
|
||||
@@ -599,13 +505,13 @@ FileTransferInfo MessageContentModel::fileInfo() const
|
||||
}
|
||||
auto config = KSharedConfig::openStateConfig(QStringLiteral("neochatdownloads"))->group(QStringLiteral("downloads"));
|
||||
if (!config.hasKey(mxcUrl.mid(6))) {
|
||||
return m_room->fileTransferInfo(m_eventId);
|
||||
return m_room->fileTransferInfo(m_event->id());
|
||||
}
|
||||
const auto path = config.readPathEntry(mxcUrl.mid(6), QString());
|
||||
QFileInfo info(path);
|
||||
if (!info.isFile()) {
|
||||
config.deleteEntry(mxcUrl);
|
||||
return m_room->fileTransferInfo(m_eventId);
|
||||
return m_room->fileTransferInfo(m_event->id());
|
||||
}
|
||||
// TODO: we could check the hash here
|
||||
return FileTransferInfo{
|
||||
|
||||
@@ -39,11 +39,6 @@ class MessageContentModel : public QAbstractListModel
|
||||
QML_ELEMENT
|
||||
QML_UNCREATABLE("")
|
||||
|
||||
/**
|
||||
* @brief Whether the author component is being shown.
|
||||
*/
|
||||
Q_PROPERTY(bool showAuthor READ showAuthor WRITE setShowAuthor NOTIFY showAuthorChanged)
|
||||
|
||||
public:
|
||||
/**
|
||||
* @brief Defines the model roles.
|
||||
@@ -53,8 +48,6 @@ public:
|
||||
ComponentTypeRole, /**< The type of component to visualise the message. */
|
||||
ComponentAttributesRole, /**< The attributes of the component. */
|
||||
EventIdRole, /**< The matrix event ID of the event. */
|
||||
TimeRole, /**< The timestamp for when the event was sent (as a QDateTime). */
|
||||
TimeStringRole, /**< The timestamp for when the event was sent as a string (in QLocale::ShortFormat). */
|
||||
AuthorRole, /**< The author of the event. */
|
||||
MediaInfoRole, /**< The media info for the event. */
|
||||
FileTransferInfoRole, /**< FileTransferInfo for any downloading files. */
|
||||
@@ -73,11 +66,8 @@ public:
|
||||
};
|
||||
Q_ENUM(Roles)
|
||||
|
||||
explicit MessageContentModel(NeoChatRoom *room, const Quotient::RoomEvent *event, bool isReply = false, bool isPending = false);
|
||||
MessageContentModel(NeoChatRoom *room, const QString &eventId, bool isReply = false, bool isPending = false);
|
||||
|
||||
bool showAuthor() const;
|
||||
void setShowAuthor(bool showAuthor);
|
||||
explicit MessageContentModel(NeoChatRoom *room, const Quotient::RoomEvent *event, bool isReply = false);
|
||||
MessageContentModel(NeoChatRoom *room, const QString &eventId, bool isReply = false);
|
||||
|
||||
/**
|
||||
* @brief Get the given role value at the given index.
|
||||
@@ -108,25 +98,19 @@ public:
|
||||
Q_INVOKABLE void closeLinkPreview(int row);
|
||||
|
||||
Q_SIGNALS:
|
||||
void showAuthorChanged();
|
||||
void eventUpdated();
|
||||
|
||||
private:
|
||||
QPointer<NeoChatRoom> m_room;
|
||||
QString m_eventId;
|
||||
QString m_eventSenderId;
|
||||
Quotient::RoomEventPtr m_event;
|
||||
const Quotient::RoomEvent *m_event = nullptr;
|
||||
|
||||
bool m_isPending;
|
||||
bool m_showAuthor = true;
|
||||
bool m_isReply;
|
||||
|
||||
void initializeModel();
|
||||
|
||||
QList<MessageComponent> m_components;
|
||||
void resetModel();
|
||||
void resetContent(bool isEditing = false);
|
||||
QList<MessageComponent> messageContentComponents(bool isEditing = false);
|
||||
void updateComponents(bool isEditing = false);
|
||||
|
||||
QPointer<MessageContentModel> m_replyModel;
|
||||
void updateReplyModel();
|
||||
@@ -135,7 +119,7 @@ private:
|
||||
|
||||
QList<MessageComponent> componentsForType(MessageComponentType::Type type);
|
||||
MessageComponent linkPreviewComponent(const QUrl &link);
|
||||
QList<MessageComponent> addLinkPreviews(QList<MessageComponent> inputComponents);
|
||||
void addLinkPreviews();
|
||||
|
||||
QList<QUrl> m_removedLinkPreviews;
|
||||
|
||||
|
||||
@@ -36,6 +36,7 @@ QHash<int, QByteArray> MessageEventModel::roleNames() const
|
||||
roles[DelegateTypeRole] = "delegateType";
|
||||
roles[EventIdRole] = "eventId";
|
||||
roles[TimeRole] = "time";
|
||||
roles[TimeStringRole] = "timeString";
|
||||
roles[SectionRole] = "section";
|
||||
roles[AuthorRole] = "author";
|
||||
roles[HighlightRole] = "isHighlighted";
|
||||
@@ -83,16 +84,12 @@ void MessageEventModel::setRoom(NeoChatRoom *room)
|
||||
return;
|
||||
}
|
||||
|
||||
beginResetModel();
|
||||
if (m_currentRoom) {
|
||||
// HACK: Reset the model to a null room first to make sure QML dismantles
|
||||
// last room's objects before the room is actually changed
|
||||
beginResetModel();
|
||||
m_currentRoom->disconnect(this);
|
||||
m_currentRoom = nullptr;
|
||||
endResetModel();
|
||||
m_reactionModels.clear();
|
||||
}
|
||||
|
||||
beginResetModel();
|
||||
m_currentRoom = room;
|
||||
Q_EMIT roomChanged();
|
||||
if (room) {
|
||||
@@ -152,7 +149,7 @@ void MessageEventModel::setRoom(NeoChatRoom *room)
|
||||
}
|
||||
if (biggest < m_currentRoom->maxTimelineIndex()) {
|
||||
auto rowBelowInserted = m_currentRoom->maxTimelineIndex() - biggest + timelineBaseIndex() - 1;
|
||||
refreshEventRoles(rowBelowInserted, {ContentModelRole});
|
||||
refreshEventRoles(rowBelowInserted, {MessageFilterModel::ShowAuthorRole});
|
||||
}
|
||||
for (auto i = m_currentRoom->maxTimelineIndex() - biggest; i <= m_currentRoom->maxTimelineIndex() - lowest; ++i) {
|
||||
refreshLastUserEvents(i);
|
||||
@@ -179,13 +176,13 @@ void MessageEventModel::setRoom(NeoChatRoom *room)
|
||||
endMoveRows();
|
||||
movingEvent = false;
|
||||
}
|
||||
fullEventRefresh(timelineBaseIndex());
|
||||
refreshRow(timelineBaseIndex()); // Refresh the looks
|
||||
refreshLastUserEvents(0);
|
||||
if (timelineBaseIndex() > 0) { // Refresh below, see #312
|
||||
refreshEventRoles(timelineBaseIndex() - 1, {ContentModelRole});
|
||||
refreshEventRoles(timelineBaseIndex() - 1, {MessageFilterModel::ShowAuthorRole});
|
||||
}
|
||||
});
|
||||
connect(m_currentRoom, &Room::pendingEventChanged, this, &MessageEventModel::fullEventRefresh);
|
||||
connect(m_currentRoom, &Room::pendingEventChanged, this, &MessageEventModel::refreshRow);
|
||||
connect(m_currentRoom, &Room::pendingEventAboutToDiscard, this, [this](int i) {
|
||||
beginRemoveRows({}, i, i);
|
||||
});
|
||||
@@ -215,36 +212,16 @@ void MessageEventModel::setRoom(NeoChatRoom *room)
|
||||
}
|
||||
refreshEventRoles(eventId, {Qt::DisplayRole});
|
||||
});
|
||||
connect(m_currentRoom, &Room::changed, this, [this](Room::Changes changes) {
|
||||
if (changes & (Room::Change::PartiallyReadStats | Room::Change::UnreadStats | Room::Change::Other | Room::Change::Members)) {
|
||||
// this is slow
|
||||
for (auto it = m_currentRoom->messageEvents().rbegin(); it != m_currentRoom->messageEvents().rend(); ++it) {
|
||||
auto event = it->event();
|
||||
refreshEventRoles(event->id(), {ReadMarkersRole, ReadMarkersStringRole, ExcessReadMarkersRole});
|
||||
}
|
||||
connect(m_currentRoom, &Room::changed, this, [this]() {
|
||||
for (auto it = m_currentRoom->messageEvents().rbegin(); it != m_currentRoom->messageEvents().rend(); ++it) {
|
||||
auto event = it->event();
|
||||
refreshEventRoles(event->id(), {ReadMarkersRole, ReadMarkersStringRole, ExcessReadMarkersRole});
|
||||
}
|
||||
});
|
||||
connect(m_currentRoom->connection(), &Connection::ignoredUsersListChanged, this, [this] {
|
||||
beginResetModel();
|
||||
endResetModel();
|
||||
});
|
||||
connect(m_currentRoom, &Room::memberNameUpdated, this, [this](RoomMember member) {
|
||||
for (auto it = m_currentRoom->messageEvents().rbegin(); it != m_currentRoom->messageEvents().rend(); ++it) {
|
||||
auto event = it->event();
|
||||
if (event->senderId() == member.id()) {
|
||||
refreshEventRoles(event->id(), {AuthorRole});
|
||||
}
|
||||
}
|
||||
});
|
||||
connect(m_currentRoom, &Room::memberAvatarUpdated, this, [this](RoomMember member) {
|
||||
for (auto it = m_currentRoom->messageEvents().rbegin(); it != m_currentRoom->messageEvents().rend(); ++it) {
|
||||
auto event = it->event();
|
||||
if (event->senderId() == member.id()) {
|
||||
refreshEventRoles(event->id(), {AuthorRole});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
qCDebug(MessageEvent) << "Connected to room" << room->id() << "as" << room->localMember().id();
|
||||
} else {
|
||||
lastReadEventId.clear();
|
||||
@@ -258,15 +235,14 @@ void MessageEventModel::setRoom(NeoChatRoom *room)
|
||||
}
|
||||
}
|
||||
|
||||
void MessageEventModel::fullEventRefresh(int row)
|
||||
int MessageEventModel::refreshEvent(const QString &eventId)
|
||||
{
|
||||
auto roles = roleNames().keys();
|
||||
// The author of an event never changes so should only be updated when a member
|
||||
// changed signal is emitted.
|
||||
// This also avoids any race conditions where a member is updating and this refresh
|
||||
// tries to access a member event that has already been deleted.
|
||||
roles.removeAll(AuthorRole);
|
||||
refreshEventRoles(row, roles);
|
||||
return refreshEventRoles(eventId);
|
||||
}
|
||||
|
||||
void MessageEventModel::refreshRow(int row)
|
||||
{
|
||||
refreshEventRoles(row);
|
||||
}
|
||||
|
||||
int MessageEventModel::timelineBaseIndex() const
|
||||
@@ -359,11 +335,11 @@ QDateTime MessageEventModel::makeMessageTimestamp(const Quotient::Room::rev_iter
|
||||
using Quotient::TimelineItem;
|
||||
auto rit = std::find_if(baseIt, timeline.rend(), hasValidTimestamp);
|
||||
if (rit != timeline.rend()) {
|
||||
return {rit->event()->originTimestamp().date(), {0, 0}, QTimeZone::LocalTime};
|
||||
return {rit->event()->originTimestamp().date(), {0, 0}, Qt::LocalTime};
|
||||
};
|
||||
auto it = std::find_if(baseIt.base(), timeline.end(), hasValidTimestamp);
|
||||
if (it != timeline.end()) {
|
||||
return {it->event()->originTimestamp().date(), {0, 0}, QTimeZone::LocalTime};
|
||||
return {it->event()->originTimestamp().date(), {0, 0}, Qt::LocalTime};
|
||||
};
|
||||
|
||||
// What kind of room is that?..
|
||||
@@ -382,7 +358,8 @@ void MessageEventModel::refreshLastUserEvents(int baseTimelineRow)
|
||||
const auto limit = timelineBottom + std::min(baseTimelineRow + 10, m_currentRoom->timelineSize());
|
||||
for (auto it = timelineBottom + std::max(baseTimelineRow - 10, 0); it != limit; ++it) {
|
||||
if ((*it)->senderId() == lastSender) {
|
||||
fullEventRefresh(it - timelineBottom);
|
||||
auto idx = index(it - timelineBottom);
|
||||
Q_EMIT dataChanged(idx, idx);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -527,6 +504,11 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
|
||||
return eventHandler.getTime(isPending, lastUpdated);
|
||||
}
|
||||
|
||||
if (role == TimeStringRole) {
|
||||
auto lastUpdated = isPending ? pendingIt->lastUpdated() : QDateTime();
|
||||
return eventHandler.getTimeString(false, QLocale::ShortFormat, isPending, lastUpdated);
|
||||
}
|
||||
|
||||
if (role == SectionRole) {
|
||||
auto lastUpdated = isPending ? pendingIt->lastUpdated() : QDateTime();
|
||||
return eventHandler.getTimeString(true, QLocale::ShortFormat, isPending, lastUpdated);
|
||||
|
||||
@@ -42,6 +42,7 @@ public:
|
||||
DelegateTypeRole = Qt::UserRole + 1, /**< The delegate type of the message. */
|
||||
EventIdRole, /**< The matrix event ID of the event. */
|
||||
TimeRole, /**< The timestamp for when the event was sent (as a QDateTime). */
|
||||
TimeStringRole, /**< The timestamp for when the event was sent as a string (in QLocale::ShortFormat). */
|
||||
SectionRole, /**< The date of the event as a string. */
|
||||
AuthorRole, /**< The author of the event. */
|
||||
HighlightRole, /**< Whether the event should be highlighted. */
|
||||
@@ -107,6 +108,10 @@ public:
|
||||
protected:
|
||||
bool event(QEvent *event) override;
|
||||
|
||||
private Q_SLOTS:
|
||||
int refreshEvent(const QString &eventId);
|
||||
void refreshRow(int row);
|
||||
|
||||
private:
|
||||
QPointer<NeoChatRoom> m_currentRoom = nullptr;
|
||||
QString lastReadEventId;
|
||||
@@ -124,7 +129,6 @@ private:
|
||||
bool canFetchMore(const QModelIndex &parent) const override;
|
||||
void fetchMore(const QModelIndex &parent) override;
|
||||
|
||||
void fullEventRefresh(int row);
|
||||
void refreshLastUserEvents(int baseTimelineRow);
|
||||
void refreshEventRoles(int row, const QList<int> &roles = {});
|
||||
int refreshEventRoles(const QString &eventId, const QList<int> &roles = {});
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
#include <KLocalizedString>
|
||||
|
||||
#include "enums/delegatetype.h"
|
||||
#include "messagecontentmodel.h"
|
||||
#include "messageeventmodel.h"
|
||||
#include "neochatconfig.h"
|
||||
#include "timelinemodel.h"
|
||||
@@ -92,12 +91,22 @@ QVariant MessageFilterModel::data(const QModelIndex &index, int role) const
|
||||
return authorList(mapToSource(index).row());
|
||||
} else if (role == ExcessAuthorsRole) {
|
||||
return excessAuthors(mapToSource(index).row());
|
||||
} else if (role == MessageEventModel::ContentModelRole) {
|
||||
const auto model = qvariant_cast<MessageContentModel *>(mapToSource(index).data(MessageEventModel::ContentModelRole));
|
||||
if (model != nullptr && !showAuthor(index)) {
|
||||
model->setShowAuthor(false);
|
||||
} else if (role == ShowAuthorRole) {
|
||||
for (auto r = index.row() + 1; r < rowCount(); ++r) {
|
||||
auto i = this->index(r, 0);
|
||||
// Note !itemData(i).empty() is a check for instances where rows have been removed, e.g. when the read marker is moved.
|
||||
// While the row is removed the subsequent row indexes are not changed so we need to skip over the removed index.
|
||||
// See - https://doc.qt.io/qt-5/qabstractitemmodel.html#beginRemoveRows
|
||||
if (data(i, MessageEventModel::SpecialMarksRole) != EventStatus::Hidden && !itemData(i).empty()) {
|
||||
return data(i, MessageEventModel::AuthorRole) != data(index, MessageEventModel::AuthorRole)
|
||||
|| data(i, MessageEventModel::DelegateTypeRole) == DelegateType::State
|
||||
|| data(i, MessageEventModel::TimeRole).toDateTime().msecsTo(data(index, MessageEventModel::TimeRole).toDateTime()) > 600000
|
||||
|| data(i, MessageEventModel::TimeRole).toDateTime().toLocalTime().date().day()
|
||||
!= data(index, MessageEventModel::TimeRole).toDateTime().toLocalTime().date().day();
|
||||
}
|
||||
}
|
||||
return QVariant::fromValue<MessageContentModel *>(model);
|
||||
|
||||
return true;
|
||||
}
|
||||
return QSortFilterProxyModel::data(index, role);
|
||||
}
|
||||
@@ -109,28 +118,10 @@ QHash<int, QByteArray> MessageFilterModel::roleNames() const
|
||||
roles[StateEventsRole] = "stateEvents";
|
||||
roles[AuthorListRole] = "authorList";
|
||||
roles[ExcessAuthorsRole] = "excessAuthors";
|
||||
roles[ShowAuthorRole] = "showAuthor";
|
||||
return roles;
|
||||
}
|
||||
|
||||
bool MessageFilterModel::showAuthor(QModelIndex index) const
|
||||
{
|
||||
for (auto r = index.row() + 1; r < rowCount(); ++r) {
|
||||
auto i = this->index(r, 0);
|
||||
// Note !itemData(i).empty() is a check for instances where rows have been removed, e.g. when the read marker is moved.
|
||||
// While the row is removed the subsequent row indexes are not changed so we need to skip over the removed index.
|
||||
// See - https://doc.qt.io/qt-5/qabstractitemmodel.html#beginRemoveRows
|
||||
if (data(i, MessageEventModel::SpecialMarksRole) != EventStatus::Hidden && !itemData(i).empty()) {
|
||||
return data(i, MessageEventModel::AuthorRole) != data(index, MessageEventModel::AuthorRole)
|
||||
|| data(i, MessageEventModel::DelegateTypeRole) == DelegateType::State
|
||||
|| data(i, MessageEventModel::TimeRole).toDateTime().msecsTo(data(index, MessageEventModel::TimeRole).toDateTime()) > 600000
|
||||
|| data(i, MessageEventModel::TimeRole).toDateTime().toLocalTime().date().day()
|
||||
!= data(index, MessageEventModel::TimeRole).toDateTime().toLocalTime().date().day();
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
QString MessageFilterModel::aggregateEventToString(int sourceRow) const
|
||||
{
|
||||
QStringList parts;
|
||||
|
||||
@@ -34,6 +34,7 @@ public:
|
||||
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. */
|
||||
ShowAuthorRole, /**< Whether the author (name and avatar) should be shown at this message. */
|
||||
LastRole, // Keep this last
|
||||
};
|
||||
|
||||
@@ -61,8 +62,6 @@ public:
|
||||
private:
|
||||
bool eventIsVisible(int sourceRow, const QModelIndex &sourceParent) const;
|
||||
|
||||
bool showAuthor(QModelIndex index) const;
|
||||
|
||||
/**
|
||||
* @brief Aggregation of the text of consecutive state events starting at row.
|
||||
*
|
||||
|
||||
@@ -221,10 +221,6 @@ int RoomTreeModel::rowCount(const QModelIndex &parent) const
|
||||
parentItem = static_cast<RoomTreeItem *>(parent.internalPointer());
|
||||
}
|
||||
|
||||
if (!parentItem) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return parentItem->childCount();
|
||||
}
|
||||
|
||||
|
||||
@@ -82,6 +82,8 @@ QVariant SearchModel::data(const QModelIndex &index, int role) const
|
||||
EventHandler eventHandler(m_room, &event);
|
||||
|
||||
switch (role) {
|
||||
case ShowAuthorRole:
|
||||
return true;
|
||||
case AuthorRole:
|
||||
return QVariant::fromValue(eventHandler.getAuthor());
|
||||
case ShowSectionRole:
|
||||
@@ -91,6 +93,10 @@ QVariant SearchModel::data(const QModelIndex &index, int role) const
|
||||
return event.originTimestamp().date() != m_result->results[row - 1].result->originTimestamp().date();
|
||||
case SectionRole:
|
||||
return eventHandler.getTimeString(true);
|
||||
case TimeRole:
|
||||
return eventHandler.getTime();
|
||||
case TimeStringRole:
|
||||
return eventHandler.getTimeString(false);
|
||||
case ShowReactionsRole:
|
||||
return false;
|
||||
case ShowReadMarkersRole:
|
||||
@@ -116,9 +122,6 @@ QVariant SearchModel::data(const QModelIndex &index, int role) const
|
||||
}
|
||||
return {};
|
||||
}
|
||||
case IsEditableRole: {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return DelegateType::Message;
|
||||
}
|
||||
@@ -139,6 +142,9 @@ QHash<int, QByteArray> SearchModel::roleNames() const
|
||||
{AuthorRole, "author"},
|
||||
{ShowSectionRole, "showSection"},
|
||||
{SectionRole, "section"},
|
||||
{TimeRole, "time"},
|
||||
{TimeStringRole, "timeString"},
|
||||
{ShowAuthorRole, "showAuthor"},
|
||||
{EventIdRole, "eventId"},
|
||||
{ExcessReadMarkersRole, "excessReadMarkers"},
|
||||
{HighlightRole, "isHighlighted"},
|
||||
@@ -152,7 +158,6 @@ QHash<int, QByteArray> SearchModel::roleNames() const
|
||||
{IsThreadedRole, "isThreaded"},
|
||||
{ThreadRootRole, "threadRoot"},
|
||||
{ContentModelRole, "contentModel"},
|
||||
{IsEditableRole, "isEditable"},
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -52,9 +52,12 @@ public:
|
||||
*/
|
||||
enum Roles {
|
||||
DelegateTypeRole = Qt::DisplayRole + 1,
|
||||
ShowAuthorRole,
|
||||
AuthorRole,
|
||||
ShowSectionRole,
|
||||
SectionRole,
|
||||
TimeRole,
|
||||
TimeStringRole,
|
||||
EventIdRole,
|
||||
ExcessReadMarkersRole,
|
||||
HighlightRole,
|
||||
@@ -68,7 +71,6 @@ public:
|
||||
IsThreadedRole,
|
||||
ThreadRootRole,
|
||||
ContentModelRole,
|
||||
IsEditableRole,
|
||||
};
|
||||
Q_ENUM(Roles)
|
||||
explicit SearchModel(QObject *parent = nullptr);
|
||||
|
||||
@@ -5,9 +5,7 @@
|
||||
|
||||
#include <QGuiApplication>
|
||||
|
||||
#include <Quotient/avatar.h>
|
||||
#include <Quotient/events/roompowerlevelsevent.h>
|
||||
#include <Quotient/room.h>
|
||||
|
||||
#include "enums/powerlevel.h"
|
||||
#include "neochatroom.h"
|
||||
@@ -27,15 +25,9 @@ void UserListModel::setRoom(NeoChatRoom *room)
|
||||
}
|
||||
|
||||
if (m_currentRoom) {
|
||||
// HACK: Reset the model to a null room first to make sure QML dismantles
|
||||
// last room's objects before the room is actually changed
|
||||
beginResetModel();
|
||||
m_currentRoom->disconnect(this);
|
||||
m_currentRoom->connection()->disconnect(this);
|
||||
m_currentRoom = nullptr;
|
||||
endResetModel();
|
||||
}
|
||||
|
||||
m_currentRoom = room;
|
||||
|
||||
if (m_currentRoom) {
|
||||
@@ -47,10 +39,7 @@ void UserListModel::setRoom(NeoChatRoom *room)
|
||||
connect(m_currentRoom, &Room::memberAvatarUpdated, this, [this](RoomMember member) {
|
||||
refreshMember(member, {AvatarRole});
|
||||
});
|
||||
connect(m_currentRoom, &Room::memberListChanged, this, [this]() {
|
||||
// this is slow
|
||||
UserListModel::refreshAllMembers();
|
||||
});
|
||||
connect(m_currentRoom, &Room::changed, this, &UserListModel::refreshAllMembers);
|
||||
connect(m_currentRoom->connection(), &Connection::loggedOut, this, [this]() {
|
||||
setRoom(nullptr);
|
||||
});
|
||||
@@ -67,9 +56,6 @@ NeoChatRoom *UserListModel::room() const
|
||||
|
||||
QVariant UserListModel::data(const QModelIndex &index, int role) const
|
||||
{
|
||||
if (!m_currentRoom) {
|
||||
return {};
|
||||
}
|
||||
if (!index.isValid()) {
|
||||
return QVariant();
|
||||
}
|
||||
@@ -79,25 +65,25 @@ QVariant UserListModel::data(const QModelIndex &index, int role) const
|
||||
"users.count()";
|
||||
return {};
|
||||
}
|
||||
auto memberId = m_members.at(index.row());
|
||||
auto member = m_members.at(index.row());
|
||||
if (role == DisplayNameRole) {
|
||||
return m_currentRoom->member(memberId).disambiguatedName();
|
||||
return member.disambiguatedName();
|
||||
}
|
||||
if (role == UserIdRole) {
|
||||
return memberId;
|
||||
return member.id();
|
||||
}
|
||||
if (role == AvatarRole) {
|
||||
return m_currentRoom->memberAvatar(memberId).url();
|
||||
return member.avatarUrl();
|
||||
}
|
||||
if (role == ObjectRole) {
|
||||
return QVariant::fromValue(memberId);
|
||||
return QVariant::fromValue(member);
|
||||
}
|
||||
if (role == PowerLevelRole) {
|
||||
auto plEvent = m_currentRoom->currentState().get<RoomPowerLevelsEvent>();
|
||||
if (!plEvent) {
|
||||
return 0;
|
||||
}
|
||||
return plEvent->powerLevelForUser(memberId);
|
||||
return plEvent->powerLevelForUser(member.id());
|
||||
}
|
||||
if (role == PowerLevelStringRole) {
|
||||
auto pl = m_currentRoom->currentState().get<RoomPowerLevelsEvent>();
|
||||
@@ -107,7 +93,7 @@ QVariant UserListModel::data(const QModelIndex &index, int role) const
|
||||
return QStringLiteral("Not Available");
|
||||
}
|
||||
|
||||
auto userPl = pl->powerLevelForUser(memberId);
|
||||
auto userPl = pl->powerLevelForUser(member.id());
|
||||
|
||||
return i18nc("%1 is the name of the power level, e.g. admin and %2 is the value that represents.",
|
||||
"%1 (%2)",
|
||||
@@ -138,7 +124,7 @@ void UserListModel::memberJoined(const Quotient::RoomMember &member)
|
||||
{
|
||||
auto pos = findUserPos(member);
|
||||
beginInsertRows(QModelIndex(), pos, pos);
|
||||
m_members.insert(pos, member.id());
|
||||
m_members.insert(pos, member);
|
||||
endInsertRows();
|
||||
}
|
||||
|
||||
@@ -158,8 +144,6 @@ void UserListModel::refreshMember(const Quotient::RoomMember &member, const QLis
|
||||
{
|
||||
auto pos = findUserPos(member);
|
||||
if (pos != m_members.size()) {
|
||||
// The update will have changed the state event so we need to insert the updated member object.
|
||||
m_members.insert(pos, member.id());
|
||||
Q_EMIT dataChanged(index(pos), index(pos), roles);
|
||||
} else {
|
||||
qWarning() << "Trying to access a room member not in the user list";
|
||||
@@ -172,15 +156,8 @@ void UserListModel::refreshAllMembers()
|
||||
m_members.clear();
|
||||
|
||||
if (m_currentRoom != nullptr) {
|
||||
m_members = m_currentRoom->joinedMemberIds();
|
||||
#if Quotient_VERSION_MINOR > 8
|
||||
MemberSorter sorter;
|
||||
#else
|
||||
MemberSorter sorter(m_currentRoom);
|
||||
#endif
|
||||
std::sort(m_members.begin(), m_members.end(), [&sorter, this](const auto &left, const auto &right) {
|
||||
return sorter(m_currentRoom->member(left), m_currentRoom->member(right));
|
||||
});
|
||||
m_members = m_currentRoom->members();
|
||||
std::sort(m_members.begin(), m_members.end(), m_currentRoom->memberSorter());
|
||||
}
|
||||
endResetModel();
|
||||
Q_EMIT usersRefreshed();
|
||||
@@ -188,18 +165,15 @@ void UserListModel::refreshAllMembers()
|
||||
|
||||
int UserListModel::findUserPos(const RoomMember &member) const
|
||||
{
|
||||
return findUserPos(member.id());
|
||||
return findUserPos(member.displayName());
|
||||
}
|
||||
|
||||
int UserListModel::findUserPos(const QString &userId) const
|
||||
int UserListModel::findUserPos(const QString &username) const
|
||||
{
|
||||
if (!m_currentRoom) {
|
||||
return 0;
|
||||
}
|
||||
const auto pos = std::find_if(m_members.cbegin(), m_members.cend(), [&userId](const QString &memberId) {
|
||||
return userId == memberId;
|
||||
});
|
||||
return pos - m_members.cbegin();
|
||||
return m_currentRoom->memberSorter().lowerBoundIndex(m_members, username);
|
||||
}
|
||||
|
||||
QHash<int, QByteArray> UserListModel::roleNames() const
|
||||
|
||||
@@ -92,7 +92,7 @@ private Q_SLOTS:
|
||||
|
||||
private:
|
||||
QPointer<NeoChatRoom> m_currentRoom;
|
||||
QList<QString> m_members;
|
||||
QList<Quotient::RoomMember> m_members;
|
||||
|
||||
int findUserPos(const Quotient::RoomMember &member) const;
|
||||
[[nodiscard]] int findUserPos(const QString &username) const;
|
||||
|
||||
@@ -269,10 +269,8 @@ Name[it]=Condivisione
|
||||
Name[ka]=გაზიარება
|
||||
Name[lv]=Kopīgot
|
||||
Name[nl]=Gedeelde
|
||||
Name[nn]=Del
|
||||
Name[pl]=Udostępnij
|
||||
Name[sl]=Deli
|
||||
Name[sv]=Dela
|
||||
Name[ta]=பகிர்
|
||||
Name[tr]=Paylaş
|
||||
Name[uk]=Оприлюднення
|
||||
@@ -294,10 +292,8 @@ Comment[it]=Il risultato della condivisione di un contenuto
|
||||
Comment[ka]=შემცველობის ნაწილის გაზიარების შედეგი
|
||||
Comment[lv]=Satura kopīgošanas rezultāts
|
||||
Comment[nl]=Het resultaat van het delen van een stukje inhoud
|
||||
Comment[nn]=Resultatet av deling av innhald
|
||||
Comment[pl]=Wynik udostępniania kawałka treści
|
||||
Comment[sl]=Rezultat deljenega kosa vsebine
|
||||
Comment[sv]=Resultatet av att dela innehåll
|
||||
Comment[ta]=எதையோ பகிர்ந்ததன் விளைவு
|
||||
Comment[tr]=Bir parça içerik paylaşımının sonucu
|
||||
Comment[uk]=Результат оприлюднення даних
|
||||
|
||||
@@ -12,15 +12,19 @@
|
||||
#include "linkpreviewer.h"
|
||||
#include "neochatconfig.h"
|
||||
#include "neochatroom.h"
|
||||
#include "notificationsmanager.h"
|
||||
#include "roommanager.h"
|
||||
#include "spacehierarchycache.h"
|
||||
|
||||
#include <Quotient/connection.h>
|
||||
#include <Quotient/csapi/cross_signing.h>
|
||||
#include <Quotient/e2ee/cryptoutils.h>
|
||||
#include <Quotient/e2ee/e2ee_common.h>
|
||||
#include <Quotient/jobs/basejob.h>
|
||||
#include <Quotient/quotient_common.h>
|
||||
#include <qt6keychain/keychain.h>
|
||||
|
||||
#include <olm/pk.h>
|
||||
|
||||
#include <KLocalizedString>
|
||||
|
||||
#include <Quotient/csapi/content-repo.h>
|
||||
@@ -67,10 +71,6 @@ void NeoChatConnection::connectSignals()
|
||||
});
|
||||
connect(this, &NeoChatConnection::syncDone, this, [this] {
|
||||
setIsOnline(true);
|
||||
|
||||
connect(this, &NeoChatConnection::syncDone, this, [this]() {
|
||||
NotificationsManager::instance().handleNotifications(this);
|
||||
});
|
||||
});
|
||||
connect(this, &NeoChatConnection::networkError, this, [this]() {
|
||||
setIsOnline(false);
|
||||
@@ -326,14 +326,9 @@ void NeoChatConnection::createRoom(const QString &name, const QString &topic, co
|
||||
connect(job, &CreateRoomJob::failure, this, [job] {
|
||||
Q_EMIT Controller::instance().errorOccured(i18n("Room creation failed: %1", job->errorString()), {});
|
||||
});
|
||||
connect(
|
||||
this,
|
||||
&Connection::newRoom,
|
||||
this,
|
||||
[](Room *room) {
|
||||
RoomManager::instance().resolveResource(room->id());
|
||||
},
|
||||
Qt::SingleShotConnection);
|
||||
connectSingleShot(this, &Connection::newRoom, this, [](Room *room) {
|
||||
RoomManager::instance().resolveResource(room->id());
|
||||
});
|
||||
}
|
||||
|
||||
void NeoChatConnection::createSpace(const QString &name, const QString &topic, const QString &parent, bool setChildParent)
|
||||
@@ -363,14 +358,9 @@ void NeoChatConnection::createSpace(const QString &name, const QString &topic, c
|
||||
connect(job, &CreateRoomJob::failure, this, [job] {
|
||||
Q_EMIT Controller::instance().errorOccured(i18n("Space creation failed: %1", job->errorString()), {});
|
||||
});
|
||||
connect(
|
||||
this,
|
||||
&Connection::newRoom,
|
||||
this,
|
||||
[](Room *room) {
|
||||
RoomManager::instance().resolveResource(room->id());
|
||||
},
|
||||
Qt::SingleShotConnection);
|
||||
connectSingleShot(this, &Connection::newRoom, this, [](Room *room) {
|
||||
RoomManager::instance().resolveResource(room->id());
|
||||
});
|
||||
}
|
||||
|
||||
bool NeoChatConnection::directChatExists(Quotient::User *user)
|
||||
@@ -553,4 +543,248 @@ LinkPreviewer *NeoChatConnection::previewerForLink(const QUrl &link)
|
||||
return previewer;
|
||||
}
|
||||
|
||||
void NeoChatConnection::setupCrossSigningKeys(const QString &password)
|
||||
{
|
||||
auto masterKeyPrivate = getRandom<32>();
|
||||
auto masterKeyContext = makeCStruct(olm_pk_signing, olm_pk_signing_size, olm_clear_pk_signing);
|
||||
QByteArray masterKeyPublic(olm_pk_signing_public_key_length(), 0);
|
||||
olm_pk_signing_key_from_seed(masterKeyContext.get(),
|
||||
masterKeyPublic.data(),
|
||||
masterKeyPublic.length(),
|
||||
masterKeyPrivate.data(),
|
||||
masterKeyPrivate.viewAsByteArray().length());
|
||||
|
||||
auto selfSigningKeyPrivate = getRandom<32>();
|
||||
auto selfSigningKeyContext = makeCStruct(olm_pk_signing, olm_pk_signing_size, olm_clear_pk_signing);
|
||||
QByteArray selfSigningKeyPublic(olm_pk_signing_public_key_length(), 0);
|
||||
olm_pk_signing_key_from_seed(selfSigningKeyContext.get(),
|
||||
selfSigningKeyPublic.data(),
|
||||
selfSigningKeyPublic.length(),
|
||||
selfSigningKeyPrivate.data(),
|
||||
selfSigningKeyPrivate.viewAsByteArray().length());
|
||||
|
||||
auto userSigningKeyPrivate = getRandom<32>();
|
||||
auto userSigningKeyContext = makeCStruct(olm_pk_signing, olm_pk_signing_size, olm_clear_pk_signing);
|
||||
QByteArray userSigningKeyPublic(olm_pk_signing_public_key_length(), 0);
|
||||
olm_pk_signing_key_from_seed(userSigningKeyContext.get(),
|
||||
userSigningKeyPublic.data(),
|
||||
userSigningKeyPublic.length(),
|
||||
userSigningKeyPrivate.data(),
|
||||
userSigningKeyPrivate.viewAsByteArray().length());
|
||||
|
||||
database()->storeEncrypted("m.cross_signing.master"_ls, masterKeyPrivate.viewAsByteArray());
|
||||
database()->storeEncrypted("m.cross_signing.self_signing"_ls, selfSigningKeyPrivate.viewAsByteArray());
|
||||
database()->storeEncrypted("m.cross_signing.user_signing"_ls, userSigningKeyPrivate.viewAsByteArray());
|
||||
|
||||
auto masterKey = CrossSigningKey{
|
||||
.userId = userId(),
|
||||
.usage = {"master"_ls},
|
||||
.keys = {{"ed25519:"_ls + QString::fromLatin1(masterKeyPublic), QString::fromLatin1(masterKeyPublic)}},
|
||||
.signatures = {},
|
||||
};
|
||||
auto selfSigningKey = CrossSigningKey{
|
||||
.userId = userId(),
|
||||
.usage = {"self_signing"_ls},
|
||||
.keys = {{"ed25519:"_ls + QString::fromLatin1(selfSigningKeyPublic), QString::fromLatin1(selfSigningKeyPublic)}},
|
||||
};
|
||||
auto userSigningKey = CrossSigningKey{
|
||||
.userId = userId(),
|
||||
.usage = {"user_signing"_ls},
|
||||
.keys = {{"ed25519:"_ls + QString::fromLatin1(userSigningKeyPublic), QString::fromLatin1(userSigningKeyPublic)}},
|
||||
|
||||
};
|
||||
|
||||
auto selfSigningKeyJson = toJson(selfSigningKey);
|
||||
selfSigningKeyJson.remove("signatures"_ls);
|
||||
selfSigningKey.signatures = QJsonObject{
|
||||
{userId(),
|
||||
QJsonObject{{"ed25519:"_ls + QString::fromLatin1(masterKeyPublic),
|
||||
QString::fromLatin1(sign(masterKeyPrivate.viewAsByteArray(), QJsonDocument(selfSigningKeyJson).toJson(QJsonDocument::Compact)))}}}};
|
||||
auto userSigningKeyJson = toJson(userSigningKey);
|
||||
userSigningKeyJson.remove("signatures"_ls);
|
||||
userSigningKey.signatures = QJsonObject{
|
||||
{userId(),
|
||||
QJsonObject{{"ed25519:"_ls + QString::fromLatin1(masterKeyPublic),
|
||||
QString::fromLatin1(sign(masterKeyPrivate.viewAsByteArray(), QJsonDocument(userSigningKeyJson).toJson(QJsonDocument::Compact)))}}}};
|
||||
|
||||
const auto encodedMasterKeyPrivate = viewAsByteArray(masterKeyPrivate).toBase64();
|
||||
const auto encodedSelfSigningKeyPrivate = viewAsByteArray(selfSigningKeyPrivate).toBase64();
|
||||
const auto encodedUserSigningKeyPrivate = viewAsByteArray(userSigningKeyPrivate).toBase64();
|
||||
|
||||
callApi<UploadCrossSigningKeysJob>(masterKey, selfSigningKey, userSigningKey, std::nullopt)
|
||||
.then(
|
||||
[this, encodedMasterKeyPrivate, encodedSelfSigningKeyPrivate, encodedUserSigningKeyPrivate, masterKeyPublic]() {
|
||||
finishCrossSigningSetup(encodedMasterKeyPrivate,
|
||||
encodedSelfSigningKeyPrivate,
|
||||
encodedUserSigningKeyPrivate,
|
||||
QString::fromLatin1(masterKeyPublic));
|
||||
},
|
||||
[this,
|
||||
password,
|
||||
masterKey,
|
||||
selfSigningKey,
|
||||
userSigningKey,
|
||||
encodedMasterKeyPrivate,
|
||||
encodedSelfSigningKeyPrivate,
|
||||
encodedUserSigningKeyPrivate,
|
||||
masterKeyPublic](const auto &job) {
|
||||
callApi<UploadCrossSigningKeysJob>(masterKey,
|
||||
selfSigningKey,
|
||||
userSigningKey,
|
||||
AuthenticationData{
|
||||
.type = "m.login.password"_ls,
|
||||
.session = job->jsonData()["session"_ls].toString(),
|
||||
.authInfo =
|
||||
QVariantHash{
|
||||
{"password"_ls, password},
|
||||
{"identifier"_ls,
|
||||
QJsonObject{
|
||||
{"type"_ls, "m.id.user"_ls},
|
||||
{"user"_ls, userId()},
|
||||
}},
|
||||
},
|
||||
})
|
||||
.then(
|
||||
[this, encodedMasterKeyPrivate, encodedSelfSigningKeyPrivate, encodedUserSigningKeyPrivate, masterKeyPublic]() {
|
||||
finishCrossSigningSetup(encodedMasterKeyPrivate,
|
||||
encodedSelfSigningKeyPrivate,
|
||||
encodedUserSigningKeyPrivate,
|
||||
QString::fromLatin1(masterKeyPublic));
|
||||
},
|
||||
[]() {
|
||||
qWarning() << "Failed to setup cross-signing keys";
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
void NeoChatConnection::finishCrossSigningSetup(const QByteArray &encodedMasterKeyPrivate,
|
||||
const QByteArray &encodedSelfSigningKeyPrivate,
|
||||
const QByteArray &encodedUserSigningKeyPrivate,
|
||||
const QString &masterKeyPublic)
|
||||
{
|
||||
auto key = getRandom(32);
|
||||
QByteArray data = QByteArrayLiteral("\x8B\x01") + viewAsByteArray(key);
|
||||
data[8] &= ~(1 << 7); // Byte 63 needs to be set to 0
|
||||
data.append(std::accumulate(data.cbegin(), data.cend(), uint8_t{0}, std::bit_xor<>()));
|
||||
data = base58Encode(data);
|
||||
QList<QString> groups;
|
||||
for (auto i = 0; i < data.size() / 4; i++) {
|
||||
groups += QString::fromLatin1(data.mid(i * 4, i * 4 + 4));
|
||||
}
|
||||
|
||||
// The key to be shown to the user
|
||||
const auto formatted = groups.join(QStringLiteral(" "));
|
||||
Q_EMIT showSecurityKey(formatted);
|
||||
const auto identifier = QString::fromLatin1(QCryptographicHash::hash(QUuid::createUuid().toString().toLatin1(), QCryptographicHash::Sha256));
|
||||
|
||||
setAccountData("m.secret_storage.default_key"_ls,
|
||||
{
|
||||
{"key"_ls, identifier},
|
||||
});
|
||||
|
||||
struct EncryptionData {
|
||||
QString ciphertext;
|
||||
QString iv;
|
||||
QString mac;
|
||||
};
|
||||
|
||||
auto encryptAccountData = [this, &key, identifier](QLatin1String info, const QByteArray &plainText) {
|
||||
const auto iv = getRandom(16);
|
||||
const auto &kdfKeys = hkdfSha256(byte_view_t<>(key).subspan<0, DefaultPbkdf2KeyLength>(), zeroes<32>(), asCBytes<>(info));
|
||||
if (!kdfKeys.has_value()) {
|
||||
qWarning() << "Key Setup: Failed to calculate HKDF" << info;
|
||||
// Q_EMIT error(DecryptionError);
|
||||
return EncryptionData{};
|
||||
}
|
||||
const auto &encrypted = aesCtr256Encrypt(plainText, kdfKeys.value().aes(), asCBytes<AesBlockSize>(iv));
|
||||
if (!encrypted.has_value()) {
|
||||
qWarning() << "Key Setup: Failed to encrypt test keys" << info;
|
||||
// emit error(DecryptionError);
|
||||
return EncryptionData{};
|
||||
}
|
||||
const auto &hmacResult = hmacSha256(kdfKeys.value().mac(), encrypted.value());
|
||||
if (!hmacResult.has_value()) {
|
||||
qWarning() << "Key Setup: Failed to calculate HMAC" << info;
|
||||
// emit error(DecryptionError);
|
||||
return EncryptionData{};
|
||||
}
|
||||
return EncryptionData{
|
||||
.ciphertext = QString::fromLatin1(encrypted.value().toBase64()),
|
||||
.iv = QString::fromLatin1(iv.viewAsByteArray()),
|
||||
.mac = QString::fromLatin1(hmacResult.value().toBase64()),
|
||||
};
|
||||
};
|
||||
|
||||
auto testData = encryptAccountData({}, zeroedByteArray());
|
||||
setAccountData("m.secret_storage.key.%1"_ls.arg(identifier),
|
||||
{
|
||||
{"algorithm"_ls, "m.secret_storage.v1.aes-hmac-sha2"_ls},
|
||||
{"iv"_ls, testData.iv},
|
||||
{"mac"_ls, testData.mac},
|
||||
});
|
||||
|
||||
auto masterData = encryptAccountData("m.cross_signing.master"_ls, encodedMasterKeyPrivate);
|
||||
setAccountData("m.cross_signing.master"_ls,
|
||||
{{"encrypted"_ls,
|
||||
QJsonObject{{identifier,
|
||||
QJsonObject{
|
||||
{"iv"_ls, masterData.iv},
|
||||
{"ciphertext"_ls, masterData.ciphertext},
|
||||
{"mac"_ls, masterData.mac},
|
||||
}}}}});
|
||||
|
||||
auto selfSigningData = encryptAccountData("m.cross_signing.self_signing"_ls, encodedSelfSigningKeyPrivate);
|
||||
setAccountData("m.cross_signing.self_signing"_ls,
|
||||
{{"encrypted"_ls,
|
||||
QJsonObject{{identifier,
|
||||
QJsonObject{
|
||||
{"iv"_ls, selfSigningData.iv},
|
||||
{"ciphertext"_ls, selfSigningData.ciphertext},
|
||||
{"mac"_ls, selfSigningData.mac},
|
||||
}}}}});
|
||||
|
||||
auto userSigningData = encryptAccountData("m.cross_signing.user_signing"_ls, encodedUserSigningKeyPrivate);
|
||||
setAccountData("m.cross_signing.user_signing"_ls,
|
||||
{{"encrypted"_ls,
|
||||
QJsonObject{{identifier,
|
||||
QJsonObject{
|
||||
{"iv"_ls, userSigningData.iv},
|
||||
{"ciphertext"_ls, userSigningData.ciphertext},
|
||||
{"mac"_ls, userSigningData.mac},
|
||||
}}}}});
|
||||
|
||||
// Adding the verified master key manually so that we don't have to wait until we receive it from the server
|
||||
auto query = database()->prepareQuery(QStringLiteral("INSERT INTO master_keys(userId, key, verified) VALUES (:userId, :key, :verified);"));
|
||||
query.bindValue(":userId"_ls, userId());
|
||||
query.bindValue(":key"_ls, masterKeyPublic);
|
||||
query.bindValue(":verified"_ls, true);
|
||||
database()->execute(query);
|
||||
|
||||
const auto selfSigningKey = database()->loadEncrypted("m.cross_signing.self_signing"_ls);
|
||||
QHash<QString, QHash<QString, QJsonObject>> signatures;
|
||||
auto json = QJsonObject{
|
||||
{"keys"_ls,
|
||||
QJsonObject{
|
||||
{"ed25519:"_ls + deviceId(), edKeyForUserDevice(userId(), deviceId())},
|
||||
{"curve25519:"_ls + deviceId(), curveKeyForUserDevice(userId(), deviceId())},
|
||||
}},
|
||||
{"algorithms"_ls, QJsonArray{"m.olm.v1.curve25519-aes-sha2"_ls, "m.megolm.v1.aes-sha2"_ls}},
|
||||
{"device_id"_ls, deviceId()},
|
||||
{"user_id"_ls, userId()},
|
||||
};
|
||||
auto signature = sign(selfSigningKey, QJsonDocument(json).toJson(QJsonDocument::Compact));
|
||||
json["signatures"_ls] = QJsonObject{
|
||||
{userId(),
|
||||
QJsonObject{
|
||||
{"ed25519:"_ls + database()->selfSigningPublicKey(), QString::fromLatin1(signature)},
|
||||
}},
|
||||
};
|
||||
signatures[userId()][deviceId()] = json;
|
||||
callApi<UploadCrossSigningSignaturesJob>(signatures).onFailure([](const auto &job) {
|
||||
qWarning() << "Failed to upload self-signing signature" << job->error() << job->errorString();
|
||||
});
|
||||
|
||||
// TODO start a key backup and store in account data
|
||||
}
|
||||
|
||||
#include "moc_neochatconnection.cpp"
|
||||
|
||||
@@ -184,6 +184,8 @@ public:
|
||||
|
||||
LinkPreviewer *previewerForLink(const QUrl &link);
|
||||
|
||||
Q_INVOKABLE void setupCrossSigningKeys(const QString &password);
|
||||
|
||||
Q_SIGNALS:
|
||||
void labelChanged();
|
||||
void identityServerChanged();
|
||||
@@ -196,6 +198,7 @@ Q_SIGNALS:
|
||||
void passwordStatus(NeoChatConnection::PasswordStatus status);
|
||||
void userConsentRequired(QUrl url);
|
||||
void badgeNotificationCountChanged(NeoChatConnection *connection, int count);
|
||||
void showSecurityKey(const QString &securityKey);
|
||||
|
||||
private:
|
||||
bool m_isOnline = true;
|
||||
@@ -204,6 +207,10 @@ private:
|
||||
ThreePIdModel *m_threePIdModel;
|
||||
|
||||
void connectSignals();
|
||||
void finishCrossSigningSetup(const QByteArray &encodedMasterKeyPrivate,
|
||||
const QByteArray &encodedSelfSigningKeyPrivate,
|
||||
const QByteArray &encodedUserSigningKeyPrivate,
|
||||
const QString &masterKeyPublic);
|
||||
|
||||
int m_badgeNotificationCount = 0;
|
||||
|
||||
|
||||
@@ -36,7 +36,6 @@
|
||||
|
||||
#include "chatbarcache.h"
|
||||
#include "clipboard.h"
|
||||
#include "controller.h"
|
||||
#include "eventhandler.h"
|
||||
#include "events/joinrulesevent.h"
|
||||
#include "events/pollevent.h"
|
||||
@@ -97,28 +96,23 @@ NeoChatRoom::NeoChatRoom(Connection *connection, QString roomId, JoinState joinS
|
||||
});
|
||||
connect(this, &Room::displaynameChanged, this, &NeoChatRoom::displayNameChanged);
|
||||
|
||||
connect(
|
||||
this,
|
||||
&Room::baseStateLoaded,
|
||||
this,
|
||||
[this]() {
|
||||
updatePushNotificationState(QStringLiteral("m.push_rules"));
|
||||
connectSingleShot(this, &Room::baseStateLoaded, this, [this]() {
|
||||
updatePushNotificationState(QStringLiteral("m.push_rules"));
|
||||
|
||||
Q_EMIT canEncryptRoomChanged();
|
||||
if (this->joinState() != JoinState::Invite) {
|
||||
return;
|
||||
}
|
||||
auto roomMemberEvent = currentState().get<RoomMemberEvent>(localMember().id());
|
||||
QImage avatar_image;
|
||||
if (roomMemberEvent && !member(roomMemberEvent->senderId()).avatarUrl().isEmpty()) {
|
||||
avatar_image = memberAvatar(roomMemberEvent->senderId()).get(this->connection(), 128, [] {});
|
||||
} else {
|
||||
qWarning() << "using this room's avatar";
|
||||
avatar_image = avatar(128);
|
||||
}
|
||||
NotificationsManager::instance().postInviteNotification(this, displayName(), member(roomMemberEvent->senderId()).htmlSafeDisplayName(), avatar_image);
|
||||
},
|
||||
Qt::SingleShotConnection);
|
||||
Q_EMIT canEncryptRoomChanged();
|
||||
if (this->joinState() != JoinState::Invite) {
|
||||
return;
|
||||
}
|
||||
auto roomMemberEvent = currentState().get<RoomMemberEvent>(localMember().id());
|
||||
QImage avatar_image;
|
||||
if (roomMemberEvent && !member(roomMemberEvent->senderId()).avatarUrl().isEmpty()) {
|
||||
avatar_image = memberAvatar(roomMemberEvent->senderId()).get(this->connection(), 128, [] {});
|
||||
} else {
|
||||
qWarning() << "using this room's avatar";
|
||||
avatar_image = avatar(128);
|
||||
}
|
||||
NotificationsManager::instance().postInviteNotification(this, displayName(), member(roomMemberEvent->senderId()).htmlSafeDisplayName(), avatar_image);
|
||||
});
|
||||
connect(this, &Room::changed, this, [this] {
|
||||
Q_EMIT canEncryptRoomChanged();
|
||||
Q_EMIT parentIdsChanged();
|
||||
@@ -1347,9 +1341,10 @@ void NeoChatRoom::updatePushNotificationState(QString type)
|
||||
void NeoChatRoom::reportEvent(const QString &eventId, const QString &reason)
|
||||
{
|
||||
auto job = connection()->callApi<ReportContentJob>(id(), eventId, -50, reason);
|
||||
connect(job, &BaseJob::finished, this, [job]() {
|
||||
connect(job, &BaseJob::finished, this, [this, job]() {
|
||||
if (job->error() == BaseJob::Success) {
|
||||
Q_EMIT Controller::instance().showMessage(Controller::Positive, i18n("Report sent successfully."));
|
||||
Q_EMIT showMessage(Positive, i18n("Report sent successfully."));
|
||||
Q_EMIT showMessage(MessageType::Positive, i18n("Report sent successfully."));
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -1718,13 +1713,4 @@ void NeoChatRoom::setRoomState(const QString &type, const QString &stateKey, con
|
||||
setState(type, stateKey, QJsonDocument::fromJson(content).object());
|
||||
}
|
||||
|
||||
#if Quotient_VERSION_MINOR == 8
|
||||
QList<RoomMember> NeoChatRoom::otherMembersTyping() const
|
||||
{
|
||||
auto memberTyping = membersTyping();
|
||||
memberTyping.removeAll(localMember());
|
||||
return memberTyping;
|
||||
}
|
||||
#endif
|
||||
|
||||
#include "moc_neochatroom.cpp"
|
||||
|
||||
@@ -201,11 +201,17 @@ class NeoChatRoom : public Quotient::Room
|
||||
*/
|
||||
Q_PROPERTY(ChatBarCache *editCache READ editCache CONSTANT)
|
||||
|
||||
#if Quotient_VERSION_MINOR == 8
|
||||
Q_PROPERTY(QList<Quotient::RoomMember> otherMembersTyping READ otherMembersTyping NOTIFY typingChanged)
|
||||
#endif
|
||||
|
||||
public:
|
||||
/**
|
||||
* @brief Define the types on inline messages that can be shown.
|
||||
*/
|
||||
enum MessageType {
|
||||
Positive, /**< Positive message, typically green. */
|
||||
Info, /**< Info message, typically highlight color. */
|
||||
Error, /**< Error message, typically red. */
|
||||
};
|
||||
Q_ENUM(MessageType)
|
||||
|
||||
explicit NeoChatRoom(Quotient::Connection *connection, QString roomId, Quotient::JoinState joinState = {});
|
||||
|
||||
[[nodiscard]] QDateTime lastActiveTime();
|
||||
@@ -607,10 +613,6 @@ private:
|
||||
void cleanupExtraEventRange(Quotient::RoomEventsRange events);
|
||||
void cleanupExtraEvent(const QString &eventId);
|
||||
|
||||
#if Quotient_VERSION_MINOR == 8
|
||||
QList<Quotient::RoomMember> otherMembersTyping() const;
|
||||
#endif
|
||||
|
||||
private Q_SLOTS:
|
||||
void updatePushNotificationState(QString type);
|
||||
|
||||
@@ -632,6 +634,7 @@ Q_SIGNALS:
|
||||
void readOnlyChanged();
|
||||
void displayNameChanged();
|
||||
void pushNotificationStateChanged(PushNotificationState::State state);
|
||||
void showMessage(MessageType messageType, const QString &message);
|
||||
void canEncryptRoomChanged();
|
||||
void joinRuleChanged();
|
||||
void historyVisibilityChanged();
|
||||
|
||||
@@ -54,88 +54,97 @@ void NotificationsManager::handleNotifications(QPointer<NeoChatConnection> conne
|
||||
|
||||
void NotificationsManager::processNotificationJob(QPointer<NeoChatConnection> connection, Quotient::GetNotificationsJob *job, bool initialization)
|
||||
{
|
||||
if (!job || !connection || !connection->isLoggedIn()) {
|
||||
if (job == nullptr) {
|
||||
return;
|
||||
}
|
||||
if (connection == nullptr || !connection->isLoggedIn()) {
|
||||
qWarning() << QStringLiteral("No connection for GetNotificationsJob %1").arg(job->objectName());
|
||||
return;
|
||||
}
|
||||
|
||||
const auto connectionId = connection->user()->id();
|
||||
|
||||
// If pagination has occurred set off the next job
|
||||
auto nextToken = job->jsonData()["next_token"_ls].toString();
|
||||
if (!nextToken.isEmpty()) {
|
||||
auto nextJob = connection->callApi<GetNotificationsJob>(nextToken);
|
||||
m_connActiveJob.append(connectionId);
|
||||
connect(nextJob, &BaseJob::success, this, [this, nextJob, connection, initialization]() {
|
||||
m_connActiveJob.removeAll(connection->user()->id());
|
||||
processNotificationJob(connection, nextJob, initialization);
|
||||
});
|
||||
}
|
||||
|
||||
const auto notifications = job->jsonData()["notifications"_ls].toArray();
|
||||
if (initialization) {
|
||||
for (const auto ¬ification : notifications) {
|
||||
m_oldNotifications[connectionId] = QStringList();
|
||||
for (const auto &n : notifications) {
|
||||
if (!m_initialTimestamp.contains(connectionId)) {
|
||||
m_initialTimestamp[connectionId] = notification["ts"_ls].toVariant().toLongLong();
|
||||
m_initialTimestamp[connectionId] = n.toObject()["ts"_ls].toDouble();
|
||||
} else {
|
||||
qint64 timestamp = notification["ts"_ls].toVariant().toLongLong();
|
||||
qint64 timestamp = n.toObject()["ts"_ls].toDouble();
|
||||
if (timestamp > m_initialTimestamp[connectionId]) {
|
||||
m_initialTimestamp[connectionId] = timestamp;
|
||||
}
|
||||
}
|
||||
|
||||
auto connectionNotifications = m_oldNotifications.value(connectionId);
|
||||
connectionNotifications += notification["event"_ls]["event_id"_ls].toString();
|
||||
connectionNotifications += n.toObject()["event"_ls].toObject()["event_id"_ls].toString();
|
||||
m_oldNotifications[connectionId] = connectionNotifications;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
QMap<QString, std::pair<qint64, QJsonObject>> notificationsToPost;
|
||||
for (const auto &n : notifications) {
|
||||
const auto notification = n.toObject();
|
||||
if (notification["read"_ls].toBool()) {
|
||||
continue;
|
||||
}
|
||||
auto connectionNotifications = m_oldNotifications.value(connectionId);
|
||||
if (connectionNotifications.contains(notification["event"_ls]["event_id"_ls].toString())) {
|
||||
if (connectionNotifications.contains(notification["event"_ls].toObject()["event_id"_ls].toString())) {
|
||||
continue;
|
||||
}
|
||||
connectionNotifications += notification["event"_ls]["event_id"_ls].toString();
|
||||
connectionNotifications += notification["event"_ls].toObject()["event_id"_ls].toString();
|
||||
m_oldNotifications[connectionId] = connectionNotifications;
|
||||
|
||||
if (!shouldPostNotification(connection, n)) {
|
||||
continue;
|
||||
}
|
||||
auto room = connection->room(notification["room_id"_ls].toString());
|
||||
if (shouldPostNotification(connection, n)) {
|
||||
// The room might have been deleted (for example rejected invitation).
|
||||
auto sender = room->member(notification["event"_ls].toObject()["sender"_ls].toString());
|
||||
|
||||
const auto &roomId = notification["room_id"_ls].toString();
|
||||
if (!notificationsToPost.contains(roomId) || notificationsToPost[roomId].first < notification["ts"_ls].toVariant().toLongLong()) {
|
||||
notificationsToPost[roomId] = {notification["ts"_ls].toVariant().toLongLong(), notification};
|
||||
}
|
||||
}
|
||||
QString body;
|
||||
|
||||
for (const auto &[roomId, pair] : notificationsToPost.asKeyValueRange()) {
|
||||
const auto ¬ification = pair.second;
|
||||
const auto room = connection->room(roomId);
|
||||
if (!room) {
|
||||
continue;
|
||||
}
|
||||
auto sender = room->member(notification["event"_ls]["sender"_ls].toString());
|
||||
|
||||
QString body;
|
||||
if (notification["event"_ls]["type"_ls].toString() == "org.matrix.msc3381.poll.start"_ls) {
|
||||
body = notification["event"_ls]["content"_ls]["org.matrix.msc3381.poll.start"_ls]["question"_ls]["body"_ls].toString();
|
||||
} else if (notification["event"_ls]["type"_ls] == "m.room.encrypted"_ls) {
|
||||
const auto decrypted = connection->decryptNotification(notification);
|
||||
body = decrypted["content"_ls]["body"_ls].toString();
|
||||
if (body.isEmpty()) {
|
||||
body = i18n("Encrypted Message");
|
||||
if (notification["event"_ls].toObject()["type"_ls].toString() == "org.matrix.msc3381.poll.start"_ls) {
|
||||
body = notification["event"_ls]
|
||||
.toObject()["content"_ls]
|
||||
.toObject()["org.matrix.msc3381.poll.start"_ls]
|
||||
.toObject()["question"_ls]
|
||||
.toObject()["body"_ls]
|
||||
.toString();
|
||||
} else {
|
||||
body = notification["event"_ls].toObject()["content"_ls].toObject()["body"_ls].toString();
|
||||
}
|
||||
} else {
|
||||
body = notification["event"_ls]["content"_ls]["body"_ls].toString();
|
||||
}
|
||||
|
||||
QImage avatar_image;
|
||||
if (!sender.avatarUrl().isEmpty()) {
|
||||
avatar_image = room->memberAvatar(sender.id()).get(connection, 128, {});
|
||||
} else {
|
||||
avatar_image = room->avatar(128);
|
||||
if (notification["event"_ls]["type"_ls] == "m.room.encrypted"_ls) {
|
||||
auto decrypted = connection->decryptNotification(notification);
|
||||
body = decrypted["content"_ls].toObject()["body"_ls].toString();
|
||||
if (body.isEmpty()) {
|
||||
body = i18n("Encrypted Message");
|
||||
}
|
||||
}
|
||||
|
||||
QImage avatar_image;
|
||||
if (!sender.avatarUrl().isEmpty()) {
|
||||
avatar_image = room->memberAvatar(sender.id()).get(connection, 128, {});
|
||||
} else {
|
||||
avatar_image = room->avatar(128);
|
||||
}
|
||||
postNotification(dynamic_cast<NeoChatRoom *>(room),
|
||||
sender.displayName(),
|
||||
body,
|
||||
avatar_image,
|
||||
notification["event"_ls].toObject()["event_id"_ls].toString(),
|
||||
true);
|
||||
}
|
||||
postNotification(dynamic_cast<NeoChatRoom *>(room),
|
||||
sender.displayName(),
|
||||
body,
|
||||
avatar_image,
|
||||
notification["event"_ls].toObject()["event_id"_ls].toString(),
|
||||
true,
|
||||
pair.first);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -166,10 +175,6 @@ bool NotificationsManager::shouldPostNotification(QPointer<NeoChatConnection> co
|
||||
return false;
|
||||
}
|
||||
|
||||
if (m_notifications.contains(room->id()) && m_notifications[room->id()].first > timestamp) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -178,22 +183,17 @@ void NotificationsManager::postNotification(NeoChatRoom *room,
|
||||
const QString &text,
|
||||
const QImage &icon,
|
||||
const QString &replyEventId,
|
||||
bool canReply,
|
||||
qint64 timestamp)
|
||||
bool canReply)
|
||||
{
|
||||
const QString roomId = room->id();
|
||||
|
||||
if (auto notification = m_notifications.value(roomId).second) {
|
||||
notification->close();
|
||||
}
|
||||
|
||||
auto notification = new KNotification(QStringLiteral("message"));
|
||||
m_notifications.insert(roomId, {timestamp, notification});
|
||||
connect(notification, &KNotification::closed, this, [this, roomId, notification] {
|
||||
if (m_notifications[roomId].second == notification) {
|
||||
KNotification *notification = m_notifications.value(roomId);
|
||||
if (!notification) {
|
||||
notification = new KNotification(QStringLiteral("message"));
|
||||
m_notifications.insert(roomId, notification);
|
||||
connect(notification, &KNotification::closed, this, [this, roomId] {
|
||||
m_notifications.remove(roomId);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
QString entry;
|
||||
if (sender == room->displayName()) {
|
||||
@@ -204,7 +204,7 @@ void NotificationsManager::postNotification(NeoChatRoom *room,
|
||||
entry = i18n("%1: %2", sender, text.toHtmlEscaped());
|
||||
}
|
||||
|
||||
notification->setText(entry);
|
||||
notification->setText(notification->text() + QLatin1Char('\n') + entry);
|
||||
notification->setPixmap(createNotificationImage(icon, room));
|
||||
|
||||
auto defaultAction = notification->addDefaultAction(i18n("Open NeoChat in this room"));
|
||||
@@ -289,6 +289,7 @@ void NotificationsManager::postInviteNotification(NeoChatRoom *rawRoom, const QS
|
||||
notification->setHint(QStringLiteral("x-kde-origin-name"), room->localMember().id());
|
||||
|
||||
notification->sendEvent();
|
||||
m_invitations.insert(room->id(), notification);
|
||||
}
|
||||
|
||||
void NotificationsManager::clearInvitationNotification(const QString &roomId)
|
||||
@@ -342,7 +343,7 @@ void NotificationsManager::postPushNotification(const QByteArray &message)
|
||||
|
||||
notification->sendEvent();
|
||||
|
||||
m_notifications.insert(roomId, {json["ts"_ls].toVariant().toLongLong(), notification});
|
||||
m_notifications.insert(roomId, notification);
|
||||
} else {
|
||||
qWarning() << "Skipping unsupported push notification" << type;
|
||||
}
|
||||
|
||||
@@ -47,13 +47,8 @@ public:
|
||||
/**
|
||||
* @brief Display a native notification for an message.
|
||||
*/
|
||||
Q_INVOKABLE void postNotification(NeoChatRoom *room,
|
||||
const QString &sender,
|
||||
const QString &text,
|
||||
const QImage &icon,
|
||||
const QString &replyEventId,
|
||||
bool canReply,
|
||||
qint64 timestamp);
|
||||
Q_INVOKABLE void
|
||||
postNotification(NeoChatRoom *room, const QString &sender, const QString &text, const QImage &icon, const QString &replyEventId, bool canReply);
|
||||
|
||||
/**
|
||||
* @brief Display a native notification for an invite.
|
||||
@@ -87,7 +82,7 @@ private:
|
||||
|
||||
bool shouldPostNotification(QPointer<NeoChatConnection> connection, const QJsonValue ¬ification);
|
||||
|
||||
QHash<QString, std::pair<qint64, KNotification *>> m_notifications;
|
||||
QHash<QString, KNotification *> m_notifications;
|
||||
QHash<QString, QPointer<KNotification>> m_invitations;
|
||||
|
||||
private Q_SLOTS:
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
# SPDX-FileCopyrightText: 2024 Tobias Fella <tobias.fella@kde.org>
|
||||
# SPDX-License-Identifier: BSD-2-Clause
|
||||
|
||||
kcoreaddons_add_plugin(neochatshareplugin SOURCES purposeplugin.cpp INSTALL_NAMESPACE "kf6/purpose")
|
||||
target_link_libraries(neochatshareplugin
|
||||
kcoreaddons_add_plugin(neochatplugin SOURCES purposeplugin.cpp INSTALL_NAMESPACE "kf6/purpose")
|
||||
target_link_libraries(neochatplugin
|
||||
Qt::DBus
|
||||
KF6::Purpose
|
||||
KF6::KIOGui
|
||||
|
||||
@@ -4,8 +4,31 @@
|
||||
{
|
||||
"Name": "Tobias Fella",
|
||||
"Name[ar]": "توبياس فلة",
|
||||
"Name[ca@valencia]": "Tobias Fella",
|
||||
"Name[ca]": "Tobias Fella",
|
||||
"Name[cs]": "Tobias Fella",
|
||||
"Name[de]": "Tobias Fella",
|
||||
"Name[en_GB]": "Tobias Fella",
|
||||
"Name[eo]": "Tobias Fella",
|
||||
"Name[es]": "Tobias Fella",
|
||||
"Name[eu]": "Tobias Fella",
|
||||
"Name[fi]": "Tobias Fella",
|
||||
"Name[fr]": "Tobias Fella",
|
||||
"Name[gl]": "Tobias Fella",
|
||||
"Name[hu]": "Tobias Fella",
|
||||
"Name[ia]": "Tobias Fella",
|
||||
"Name[it]": "Tobias Fella",
|
||||
"Name[ka]": "Tobias Fella",
|
||||
"Name[lv]": "Tobias Fella",
|
||||
"Name[nl]": "Tobias Fella",
|
||||
"Name[pl]": "Tobias Fella",
|
||||
"Name[ru]": "Tobias Fella",
|
||||
"Name[sl]": "Tobias Fella",
|
||||
"Name[ta]": "டோபியாஸ் ஃபெல்லா",
|
||||
"Name[x-test]": "xxTobias Fellaxx"
|
||||
"Name[tr]": "Tobias Fella",
|
||||
"Name[uk]": "Tobias Fella",
|
||||
"Name[x-test]": "xxTobias Fellaxx",
|
||||
"Name[zh_TW]": "Tobias Fella"
|
||||
}
|
||||
],
|
||||
"Category": "Utilities",
|
||||
@@ -14,6 +37,7 @@
|
||||
"Description[ca@valencia]": "Compartix a través de NeoChat",
|
||||
"Description[ca]": "Comparteix a través del NeoChat",
|
||||
"Description[de]": "Über NeoChat teilen",
|
||||
"Description[en_GB]": "Share via NeoChat",
|
||||
"Description[eo]": "Kundividi per NeoChat",
|
||||
"Description[es]": "Compartir mediante NeoChat",
|
||||
"Description[eu]": "Partekatu NeoChat bidez",
|
||||
@@ -26,11 +50,9 @@
|
||||
"Description[ka]": "გააზიარეთ NeoChat-ით",
|
||||
"Description[lv]": "Kopīgot ar „NeoChat“",
|
||||
"Description[nl]": "Delen via NeoChat",
|
||||
"Description[nn]": "Del via NeoChat",
|
||||
"Description[pl]": "Udostępnij przez NeoChat",
|
||||
"Description[ru]": "Опубликовать в NeoChat",
|
||||
"Description[sl]": "Deli prek NeoChat",
|
||||
"Description[sv]": "Dela via NeoChat",
|
||||
"Description[ta]": "நியோச்சாட் மூலம் பகிர்",
|
||||
"Description[tr]": "NeoChat ile Paylaş",
|
||||
"Description[uk]": "Оприлюднити за допомогою NeoChat",
|
||||
@@ -40,9 +62,32 @@
|
||||
"License": "GPL",
|
||||
"Name": "NeoChat",
|
||||
"Name[ar]": "نيوتشات",
|
||||
"Name[ast]": "NeoChat",
|
||||
"Name[ca@valencia]": "NeoChat",
|
||||
"Name[ca]": "NeoChat",
|
||||
"Name[cs]": "NeoChat",
|
||||
"Name[de]": "NeoChat",
|
||||
"Name[en_GB]": "NeoChat",
|
||||
"Name[eo]": "NeoChat",
|
||||
"Name[es]": "NeoChat",
|
||||
"Name[eu]": "NeoChat",
|
||||
"Name[fi]": "NeoChat",
|
||||
"Name[fr]": "NeoChat",
|
||||
"Name[gl]": "NeoChat",
|
||||
"Name[hu]": "NeoChat",
|
||||
"Name[ia]": "Neochat",
|
||||
"Name[it]": "NeoChat",
|
||||
"Name[ka]": "NeoChat",
|
||||
"Name[lv]": "NeoChat",
|
||||
"Name[nl]": "NeoChat",
|
||||
"Name[pl]": "NeoChat",
|
||||
"Name[ru]": "NeoChat",
|
||||
"Name[sl]": "NeoChat",
|
||||
"Name[ta]": "நியோச்சாட்",
|
||||
"Name[tr]": "NeoChat",
|
||||
"Name[uk]": "NeoChat",
|
||||
"Name[x-test]": "xxNeoChatxx",
|
||||
"Name[zh_TW]": "NeoChat",
|
||||
"X-Purpose-ActionDisplay": "NeoChat"
|
||||
},
|
||||
"X-Purpose-PluginTypes": [
|
||||
|
||||
@@ -1,84 +0,0 @@
|
||||
// SPDX-FileCopyrightText: 2019 Black Hat <bhat@encom.eu.org>
|
||||
// SPDX-FileCopyrightText: 2020 Carl Schwan <carl@carlschwan.eu>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls as QQC2
|
||||
import QtQuick.Layouts
|
||||
|
||||
import org.kde.kirigami as Kirigami
|
||||
import org.kde.kirigamiaddons.components as KirigamiComponents
|
||||
import org.kde.kirigamiaddons.formcard as FormCard
|
||||
import org.kde.prison
|
||||
|
||||
import org.kde.neochat
|
||||
|
||||
Kirigami.Dialog {
|
||||
id: root
|
||||
|
||||
property NeoChatConnection connection
|
||||
|
||||
leftPadding: 0
|
||||
rightPadding: 0
|
||||
topPadding: 0
|
||||
bottomPadding: 0
|
||||
|
||||
standardButtons: Kirigami.Dialog.NoButton
|
||||
|
||||
width: Math.min(QQC2.ApplicationWindow.window.width, Kirigami.Units.gridUnit * 24)
|
||||
|
||||
contentItem: ColumnLayout {
|
||||
spacing: 0
|
||||
|
||||
KirigamiComponents.Avatar {
|
||||
id: avatar
|
||||
|
||||
readonly property string mediaId: root.connection.localUser.avatarMediaId
|
||||
|
||||
Layout.preferredWidth: Kirigami.Units.iconSizes.huge * 2
|
||||
Layout.preferredHeight: Kirigami.Units.iconSizes.huge * 2
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
|
||||
name: root.room ? root.room.member(root.user.id).displayName : root.user.displayName
|
||||
source: mediaId ? root.connection.makeMediaUrl("mxc://" + mediaId) : ""
|
||||
color: root.room ? root.room.member(root.user.id).color : QmlUtils.getUserColor(root.user.hueF)
|
||||
}
|
||||
|
||||
QQC2.Label {
|
||||
Layout.fillWidth: true
|
||||
font.bold: false
|
||||
font.weight: Font.DemiBold
|
||||
font.pixelSize: 32
|
||||
|
||||
elide: Text.ElideRight
|
||||
wrapMode: Text.NoWrap
|
||||
text: root.connection.localUser.displayName
|
||||
textFormat: Text.PlainText
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
}
|
||||
|
||||
Kirigami.SelectableLabel {
|
||||
Layout.fillWidth: true
|
||||
Layout.bottomMargin: Kirigami.Units.largeSpacing
|
||||
font.pixelSize: 16
|
||||
textFormat: TextEdit.PlainText
|
||||
text: root.connection.localUser.id
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
}
|
||||
FormCard.FormDelegateSeparator {}
|
||||
FormCard.FormButtonDelegate {
|
||||
text: i18n("Edit Account")
|
||||
}
|
||||
FormCard.FormButtonDelegate {
|
||||
text: i18n("Switch Account")
|
||||
}
|
||||
FormCard.FormButtonDelegate {
|
||||
text: i18n("Logout")
|
||||
}
|
||||
|
||||
}
|
||||
Component {
|
||||
id: qrMaximizeComponent
|
||||
QrCodeMaximizeComponent {}
|
||||
}
|
||||
}
|
||||
@@ -15,7 +15,6 @@ QQC2.Menu {
|
||||
id: root
|
||||
|
||||
required property NeoChatConnection connection
|
||||
required property Kirigami.ApplicationWindow window
|
||||
|
||||
margins: Kirigami.Units.smallSpacing
|
||||
|
||||
@@ -31,16 +30,26 @@ QQC2.Menu {
|
||||
QQC2.MenuItem {
|
||||
text: i18n("Notification settings")
|
||||
icon.name: "notifications"
|
||||
onTriggered: {
|
||||
NeoChatSettingsView.open('notifications');
|
||||
}
|
||||
onTriggered: pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat.settings', 'NeoChatSettings'), {
|
||||
defaultPage: "notifications",
|
||||
connection: root.connection
|
||||
}, {
|
||||
title: i18n("Configure"),
|
||||
width: Kirigami.Units.gridUnit * 50,
|
||||
height: Kirigami.Units.gridUnit * 42
|
||||
})
|
||||
}
|
||||
QQC2.MenuItem {
|
||||
text: i18n("Devices")
|
||||
icon.name: "computer-symbolic"
|
||||
onTriggered: {
|
||||
NeoChatSettingsView.open('devices');
|
||||
}
|
||||
onTriggered: pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat.settings', 'NeoChatSettings'), {
|
||||
defaultPage: "devices",
|
||||
connection: root.connection
|
||||
}, {
|
||||
title: i18n("Configure"),
|
||||
width: Kirigami.Units.gridUnit * 50,
|
||||
height: Kirigami.Units.gridUnit * 42
|
||||
})
|
||||
}
|
||||
QQC2.MenuItem {
|
||||
text: i18n("Open developer tools")
|
||||
@@ -58,7 +67,7 @@ QQC2.Menu {
|
||||
text: i18nc("@action:inmenu", "Open Secret Backup")
|
||||
icon.name: "unlock"
|
||||
visible: Config.secretBackup
|
||||
onTriggered: root.window.pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat', 'UnlockSSSSDialog'), {}, {
|
||||
onTriggered: pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat', 'UnlockSSSSDialog'), {}, {
|
||||
title: i18nc("@title:window", "Open Key Backup")
|
||||
})
|
||||
}
|
||||
@@ -66,12 +75,12 @@ QQC2.Menu {
|
||||
text: i18nc("@action:inmenu", "Verify this Device")
|
||||
icon.name: "security-low"
|
||||
onTriggered: root.connection.startSelfVerification()
|
||||
enabled: Controller.csSupported
|
||||
enabled: Controller.ssssSupported
|
||||
}
|
||||
QQC2.MenuItem {
|
||||
text: i18n("Logout")
|
||||
icon.name: "list-remove-user"
|
||||
onTriggered: confirmLogoutDialogComponent.createObject(QQC2.ApplicationWindow.window.overlay).open()
|
||||
onTriggered: confirmLogoutDialogComponent.createObject(applicationWindow().overlay).open()
|
||||
}
|
||||
|
||||
Component {
|
||||
|
||||
@@ -16,8 +16,21 @@ Components.AbstractMaximizeComponent {
|
||||
|
||||
/**
|
||||
* @brief The message author.
|
||||
*
|
||||
* This should consist of the following:
|
||||
* - id - The matrix ID of the author.
|
||||
* - isLocalUser - Whether the author is the local user.
|
||||
* - avatarSource - The mxc URL for the author's avatar in the current room.
|
||||
* - avatarMediaId - The media ID of the author's avatar.
|
||||
* - avatarUrl - The mxc URL for the author's avatar.
|
||||
* - displayName - The display name of the author.
|
||||
* - display - The name of the author.
|
||||
* - color - The color for the author.
|
||||
* - object - The Quotient::User object for the author.
|
||||
*
|
||||
* @sa Quotient::User
|
||||
*/
|
||||
property RoomMember author
|
||||
property var author
|
||||
|
||||
/**
|
||||
* @brief The timestamp of the message.
|
||||
@@ -49,7 +62,7 @@ Components.AbstractMaximizeComponent {
|
||||
implicitHeight: Kirigami.Units.iconSizes.medium
|
||||
|
||||
name: root.author.name ?? root.author.displayName
|
||||
source: root.author.avatarUrl
|
||||
source: root.author.avatarSource
|
||||
color: root.author.color
|
||||
}
|
||||
ColumnLayout {
|
||||
|
||||
@@ -108,11 +108,14 @@ Loader {
|
||||
}
|
||||
|
||||
QQC2.MenuItem {
|
||||
text: i18nc("@action:inmenu", "Room Settings")
|
||||
icon.name: 'settings-configure-symbolic'
|
||||
onTriggered: {
|
||||
RoomSettingsView.openRoomSettings(root.room, RoomSettingsView.Room);
|
||||
}
|
||||
text: i18n("Room Settings")
|
||||
icon.name: "configure"
|
||||
onTriggered: QQC2.ApplicationWindow.window.pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat.settings', 'RoomSettings'), {
|
||||
room: room,
|
||||
connection: connection
|
||||
}, {
|
||||
title: i18n("Room Settings")
|
||||
})
|
||||
}
|
||||
|
||||
QQC2.MenuSeparator {}
|
||||
@@ -184,10 +187,14 @@ Loader {
|
||||
}
|
||||
|
||||
QQC2.ToolButton {
|
||||
text: i18nc("@action:button", "Room Settings")
|
||||
icon.name: 'settings-configure-symbolic'
|
||||
icon.name: 'settings-configure'
|
||||
onClicked: {
|
||||
RoomSettingsView.openRoomSettings(root.room, RoomSettingsView.Room);
|
||||
QQC2.ApplicationWindow.window.pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat.settings', 'RoomSettings'), {
|
||||
room: room,
|
||||
connection: root.connection
|
||||
}, {
|
||||
title: i18n("Room Settings")
|
||||
});
|
||||
drawer.close();
|
||||
}
|
||||
}
|
||||
|
||||
33
src/qml/CrossSigningSetupDialog.qml
Normal file
33
src/qml/CrossSigningSetupDialog.qml
Normal file
@@ -0,0 +1,33 @@
|
||||
// SPDX-FileCopyrightText: 2024 Tobias Fella <tobias.fella@kde.org>
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls as QQC2
|
||||
import org.kde.kirigami as Kirigami
|
||||
|
||||
import org.kde.neochat
|
||||
|
||||
Kirigami.PromptDialog {
|
||||
id: root
|
||||
|
||||
required property NeoChatConnection connection
|
||||
|
||||
title: i18nc("@title:dialog", "Setup Encryption Keys")
|
||||
dialogType: Kirigami.PromptDialog.Information
|
||||
|
||||
onRejected: {
|
||||
root.close();
|
||||
}
|
||||
|
||||
footer: QQC2.DialogButtonBox {
|
||||
standardButtons: QQC2.Dialog.Cancel
|
||||
|
||||
QQC2.Button {
|
||||
text: i18nc("@action:button", "Setup Keys")
|
||||
QQC2.DialogButtonBox.buttonRole: QQC2.DialogButtonBox.AcceptRole
|
||||
onClicked: {
|
||||
root.connection.setupCrossSigningKeys("password123");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -102,15 +102,11 @@ RowLayout {
|
||||
display: QQC2.AbstractButton.IconOnly
|
||||
onClicked: root.search();
|
||||
icon.name: "search"
|
||||
text: i18nc("@action", "Search Rooms")
|
||||
text: i18nc("@action", "Search Room")
|
||||
Shortcut {
|
||||
sequence: "Ctrl+F"
|
||||
onActivated: searchButton.clicked()
|
||||
}
|
||||
|
||||
QQC2.ToolTip.visible: hovered
|
||||
QQC2.ToolTip.text: text
|
||||
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||
}
|
||||
|
||||
QQC2.ToolButton {
|
||||
@@ -134,9 +130,9 @@ RowLayout {
|
||||
}
|
||||
}
|
||||
|
||||
QQC2.ToolTip.visible: hovered
|
||||
QQC2.ToolTip.text: text
|
||||
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||
QQC2.ToolTip {
|
||||
text: parent.text
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user