Compare commits

..

16 Commits

Author SHA1 Message Date
Tobias Fella
3ede6b4526 More untested stuff :) 2024-06-26 22:56:33 +02:00
Tobias Fella
eda2881d6e Work 2024-06-21 21:57:26 +02:00
Tobias Fella
7ae553b2c3 Bunch o' work 2024-06-20 23:27:54 +02:00
Tobias Fella
94e650f7ad Create Cross-signing keys when required
This should be almost entirely in libquotient, but that's not prepared to actually use user-interactive authentication...
2024-06-20 21:48:22 +02:00
Tobias Fella
9a4a70178e Add basic cross-signing support 2024-06-08 15:20:08 +02:00
James Graham
33f505c06c Don't store RoomMembers in ReactionModel
Don't store RoomMember objects in the reaction model just grab them everytime incase the state event is replaced.
2024-06-08 15:20:08 +02:00
James Graham
47a5952c2a Use new libquotient functionality to encrypt direct chats by default
Needs https://github.com/quotient-im/libQuotient/pull/730
2024-06-08 15:20:08 +02:00
Tobias Fella
6babc1a479 Disable failing CIs 2024-06-08 15:20:08 +02:00
James Graham
58e4ccb680 Use the new libquotient version of the serveracl event
Use with https://github.com/quotient-im/libQuotient/pull/729
2024-06-08 15:20:08 +02:00
James Graham
607b6bcef0 Use updated membersTyping functions from libquotient 2024-06-08 15:20:08 +02:00
James Graham
bae9f39719 Request direct chat by userId to avoid breakage with upcoming libquotient changes 2024-06-08 15:20:07 +02:00
Tobias Fella
fa3fdca155 Unconditionally use SSSS 2024-06-08 15:20:07 +02:00
James Graham
8085d19eee Actually user.h needs to still be included in actionsmodel 2024-06-08 15:20:07 +02:00
James Graham
272f49876e Make use of new RoomMember item from libquotient
Depends on https://github.com/quotient-im/libQuotient/pull/695

Currently basic just to show a working implementation using RoomMember. Currently only the room event and search models are moved over. Will change everything else over once the dependent pr is complete.
2024-06-08 15:20:07 +02:00
James Graham
ece56a55e8 handle remaining stuff for Quotient::Omittable deprecation 2024-06-08 15:20:07 +02:00
James Graham
aec35222f9 Remove uses of Quotient:Omittable
Note this technically won't build for now because of the lack of RoomMember support but I'll push that at the quotient-next branch next. 

This is needed as well to get a branch that builds on dev.
2024-06-08 15:19:59 +02:00
163 changed files with 25146 additions and 25100 deletions

View File

@@ -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

View File

@@ -40,4 +40,4 @@ Dependencies:
Options:
per-test-timeout: 90
require-passing-tests-on: [ 'Linux', 'Android', 'FreeBSD' ]
require-passing-tests-on: [ '@all' ]

View File

@@ -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"

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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;
}

View File

@@ -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"/>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -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>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -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

View File

@@ -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: {

View File

@@ -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

View File

@@ -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: {

View File

@@ -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"

View File

@@ -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);
};

View File

@@ -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

View File

@@ -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. */

View File

@@ -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();

View File

@@ -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]),
};
}
}

View File

@@ -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)
};

View File

@@ -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);
};

View File

@@ -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;

View File

@@ -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);
};

View File

@@ -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;

View File

@@ -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);
};

View File

@@ -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;

View File

@@ -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);
};

View File

@@ -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

View File

@@ -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

View File

@@ -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 {}

View File

@@ -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

View File

@@ -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,

View File

@@ -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);
}

View File

@@ -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{

View File

@@ -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;

View File

@@ -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);

View File

@@ -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 = {});

View File

@@ -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;

View File

@@ -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.
*

View File

@@ -221,10 +221,6 @@ int RoomTreeModel::rowCount(const QModelIndex &parent) const
parentItem = static_cast<RoomTreeItem *>(parent.internalPointer());
}
if (!parentItem) {
return 0;
}
return parentItem->childCount();
}

View File

@@ -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"},
};
}

View File

@@ -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);

View File

@@ -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

View File

@@ -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;

View File

@@ -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]=Результат оприлюднення даних

View File

@@ -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"

View File

@@ -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;

View File

@@ -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"

View File

@@ -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();

View File

@@ -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 &notification : 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 &notification = 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;
}

View File

@@ -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 &notification);
QHash<QString, std::pair<qint64, KNotification *>> m_notifications;
QHash<QString, KNotification *> m_notifications;
QHash<QString, QPointer<KNotification>> m_invitations;
private Q_SLOTS:

View File

@@ -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

View File

@@ -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": [

View File

@@ -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 {}
}
}

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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();
}
}

View 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");
}
}
}
}

View File

@@ -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