Compare commits

...

39 Commits

Author SHA1 Message Date
Tobias Fella
cb94261727 Fix compatibility with libQuotient dev branch 2024-04-17 13:01:32 +02:00
l10n daemon script
efac7e4860 GIT_SILENT Sync po/docbooks with svn 2024-04-17 01:25:31 +00:00
Tobias Fella
0f6fd3adee Remove outdated event registrations 2024-04-16 22:39:16 +02:00
James Graham
e5e0405401 Fix typo 2024-04-16 21:16:21 +01:00
Tobias Fella
bb35e9ce15 Add option to show all rooms in "uncategorized" tab 2024-04-16 20:49:17 +02:00
Tobias Fella
5881db4e55 Add missing import to global menu 2024-04-16 20:25:04 +02:00
Tobias Fella
89f7167b08 Remember previous downloads after restarts
Fixes #636
2024-04-16 17:55:26 +02:00
l10n daemon script
3b39fcff84 GIT_SILENT Sync po/docbooks with svn 2024-04-16 01:27:26 +00:00
Tobias Fella
0daf45a465 Fix opening account editor 2024-04-15 17:48:26 +02:00
Tobias Fella
c0d7b96e79 Mark ThreePIdModel as uncreatable 2024-04-15 17:15:01 +02:00
Laurent Montel
a247e40865 Add missing includes moc 2024-04-15 08:10:50 +02:00
Laurent Montel
c3e7a99bca Add missing #pragma once 2024-04-15 08:10:22 +02:00
l10n daemon script
9080d8be6a GIT_SILENT Sync po/docbooks with svn 2024-04-15 01:20:15 +00:00
James Graham
b7ee83f6b6 Add visualisation of the account's third party IDs in the account editor.
A category won't be shown if there are no relevant IDs (will add the ability to add new ones later).

Part of network/neochat#565

![image](/uploads/7da00b0b4acf90d145c09969ac2a91e1/image.png)
2024-04-14 16:37:34 +00:00
James Graham
1e24bde9a9 Add hover button for maximising a code block
Forgot to add it with the maximize mr
2024-04-14 16:32:12 +00:00
l10n daemon script
ba82df1152 GIT_SILENT Sync po/docbooks with svn 2024-04-14 01:20:38 +00:00
Tobias Fella
8980fe7838 Improve README 2024-04-13 21:59:16 +02:00
James Graham
ef34ed7c20 Fix Verification Window Sizing
Update the layouts in the device verifcation process to make sure that all possible window sizes can be handled

BUG: 485309
2024-04-13 18:51:06 +00:00
James Graham
17d60b79ca Fix showing the unread count for DM section when not selected 2024-04-13 15:15:27 +00:00
James Graham
ab0a32c339 Add a debug options page to devtools with the option to always allow verification for now
Closes network/neochat#646
2024-04-13 15:11:25 +00:00
James Graham
697778df8d Fix feature flag devtools page by adding necohat import as this is where config comes from now 2024-04-13 15:28:26 +01:00
Yuri Chornoivan
55caf84b94 Fix minor typos 2024-04-13 08:47:48 +03:00
l10n daemon script
335c012f1b GIT_SILENT Sync po/docbooks with svn 2024-04-13 01:25:05 +00:00
Tobias Fella
3c4c538de8 Use declarative type registration for remaining types 2024-04-12 22:17:39 +02:00
Tobias Fella
c344a3ee55 Devtools: Implement changing room state 2024-04-12 22:08:33 +02:00
l10n daemon script
d2695947ed GIT_SILENT Sync po/docbooks with svn 2024-04-12 01:22:49 +00:00
Tobias Fella
21beeef920 Load Main qml component from module 2024-04-11 21:01:55 +02:00
James Graham
a4630a53fa Create QML module for login 2024-04-11 18:56:08 +00:00
Tobias Fella
f5aef8d0c3 Use Qt.createComponent in non-weird way
Fixed #647
2024-04-11 18:16:19 +02:00
l10n daemon script
e044e66030 GIT_SILENT Sync po/docbooks with svn 2024-04-11 01:23:29 +00:00
James Graham
88bfacd386 Show QR code for room in drawer 2024-04-10 18:19:05 +00:00
James Graham
c61c73088f Add button to get a QR code for the local user to the account editor page 2024-04-10 17:19:11 +00:00
l10n daemon script
2887263f26 GIT_SILENT Sync po/docbooks with svn 2024-04-10 01:24:51 +00:00
James Graham
72b90bdf5c Fix gaps at the top and bottom of SpaceHomePage witht he wrong background colour.
Note: the bottom gap only happened after a room was loaded for the first time
2024-04-09 19:03:24 +00:00
James Graham
163b02f023 Add image info for stickers
Closes network/neochat#584
2024-04-09 18:54:59 +00:00
James Graham
1a96899336 Linkpreviewer Improvements
- Have LinkPreviewers stored in NeoChatConnection so that they don't have to be reloaded everytime the MessageContentModel is refreshed
- This means the link is never changed (it will be swiched for a new previewer with the new link)
- LinkPreviewers are stored by URL so they can be re-used by any event with the same URL

BUG: 484927  (because the offending code is ripped out)
2024-04-09 18:35:16 +00:00
l10n daemon script
554c086aba GIT_SILENT Sync po/docbooks with svn 2024-04-09 01:25:03 +00:00
Heiko Becker
1fad9bf7db GIT_SILENT Update Appstream for new release
(cherry picked from commit 86e8dc2e40)
2024-04-08 17:57:25 +02:00
l10n daemon script
22d922e451 GIT_SILENT Sync po/docbooks with svn 2024-04-08 01:23:31 +00:00
165 changed files with 25413 additions and 21681 deletions

1
.gitignore vendored
View File

@@ -12,3 +12,4 @@ kate.project.ctags.*
.idea/
cmake-build-*
src/res.generated.qrc
.qmlls.ini

View File

@@ -1,6 +1,6 @@
<!--
SPDX-FileCopyrightText: 2020-2021 Carl Schwan <carlschwan@kde.org>
SPDX-FileCopyrightText: 2020-2021 Tobias Fella <tobias.fella@kde.org>
SPDX-FileCopyrightText: 2020-2024 Tobias Fella <tobias.fella@kde.org>
SPDX-FileCopyrightText: 2023 James Graham <james.h.graham@protonmail.com>
SPDX-License-Identifier: CC0-1.0
-->
@@ -16,19 +16,18 @@ A Qt/QML based Matrix client.
## Introduction
NeoChat is a client for [Matrix](https://matrix.org), the decentralized communication protocol for instant
messaging. It is a fork of Spectral, using KDE frameworks, most notably [Kirigami](https://invent.kde.org/frameworks/kirigami)
to provide a convergent experience across multiple platforms.
messaging.
NeoChat also make use of other KDE Frameworks as well as [libQuotient](https://github.com/quotient-im/libQuotient), a
NeoChat is based on KDE frameworks and as [libQuotient](https://github.com/quotient-im/libQuotient), a
Qt-based SDK for the [Matrix Protocol](https://spec.matrix.org/).
![Timeline](https://cdn.kde.org/screenshots/neochat/application.png)
## Features
NeoChat aims to be a fully featured application for the Matrix specification. As such everything in the current stable specification with the notable exceptions
of VoIP, threads and some aspects of End-to-End Encryption are supported. There are a few other smaller omissions due to the fact that the Matrix spec is constantly
evolving but the aim remains to provide eventual support for the entire spec.
NeoChat aims to be a fully featured application for the Matrix specification. As such most parts of the current specification are supported, with the notable exceptions
of VoIP, threads, and some aspects of End-to-End Encryption. There are a few other smaller omissions due to the fact that the Matrix spec is constantly
evolving, but the aim remains to provide eventual support for the entire spec.
Due to the nature of the Matrix specification development NeoChat also supports numerous unstable features. Currently these are:
- Polls - MSC3381
@@ -39,26 +38,9 @@ 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).
In addition to the stable builds, unstable nightly builds are available for all platforms. These can be downloaded
from the [binary factory](https://binary-factory.kde.org/). There are unstable versions for the following platforms
in addition to stable ones:
- Android
- MacOS
- Windows
Additionally the nightly Flatpak version can be obtained from the nightly Flatpak repo using the following commands in your terminal:
```
flatpak remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo
flatpak remote-add --if-not-exists kdeapps --from https://distribute.kde.org/kdeapps.flatpakrepo
flatpak install kdeapps org.kde.neochat
```
The unstable Android version can also be obtained from the [KDE nightly F-Droid repo](https://community.kde.org/Android/FDroid).
## Running
Just start the executable in your preferred way - either from the build directory or from the installed location.
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
@@ -69,14 +51,18 @@ is primarily aimed at Linux development.
For Windows and Android [Craft](https://invent.kde.org/packaging/craft) is the primary choice. There are guides for setting up
development environments for [Windows](https://community.kde.org/Get_Involved/development/Windows) and [Android](https://develop.kde.org/docs/packaging/android/building_applications/).
## Running
Just start the executable in your preferred way - either from the build directory or from the installed location.
## Tests
Tests are in the repository under [autotests](autotests) and should all pass for any contribution.
Tests are in the repository under [autotests](autotests) and [appiumtests](appiumtests).
The project has CI setup to test new commits to the repository. All tests are expected to pass for a merge request to
be complete.
Current build status
## Current build status
![coverage](https://invent.kde.org/network/neochat/badges/master/pipeline.svg)
@@ -100,9 +86,9 @@ The best place to reach the maintainers is on the KDE Matrix instance in the Neo
## Acknowledgement
This program utilizes [libQuotient](https://github.com/quotient-im/libQuotient/) as its Matrix SDK.
NeoChat utilizes [libQuotient](https://github.com/quotient-im/libQuotient/) as its Matrix SDK.
This program is a fork of [Spectral](https://gitlab.com/spectral-im/spectral/).
NeoChat is a fork of [Spectral](https://gitlab.com/spectral-im/spectral/).
## License

View File

@@ -130,7 +130,7 @@ void DelegateSizeHelperTest::equalBreakpoint_data()
}
/**
* We expect a default return except in the case where the the two percentages are
* We expect a default return except in the case where the two percentages are
* equal as that case can be calculated without dividing by zero.
*/
void DelegateSizeHelperTest::equalBreakpoint()

View File

@@ -32,8 +32,6 @@ private Q_SLOTS:
void linkPreviewsReject_data();
void linkPreviewsReject();
void editedLink();
};
void LinkPreviewerTest::initTestCase()
@@ -59,7 +57,7 @@ void LinkPreviewerTest::linkPreviewsMatch()
QFETCH(QUrl, testOutputLink);
auto event = TestUtils::loadEventFromFile<RoomMessageEvent>(eventSource);
auto linkPreviewer = LinkPreviewer(room, event.get());
auto linkPreviewer = LinkPreviewer(LinkPreviewer::linkPreview(event.get()), connection);
QCOMPARE(linkPreviewer.empty(), false);
QCOMPARE(linkPreviewer.url(), testOutputLink);
@@ -79,22 +77,7 @@ void LinkPreviewerTest::linkPreviewsReject()
QFETCH(QString, eventSource);
auto event = TestUtils::loadEventFromFile<RoomMessageEvent>(eventSource);
auto linkPreviewer = LinkPreviewer(room, event.get());
QCOMPARE(linkPreviewer.empty(), true);
QCOMPARE(linkPreviewer.url(), QUrl());
}
void LinkPreviewerTest::editedLink()
{
room->syncNewEvents(QStringLiteral("test-linkpreviewerintial-sync.json"));
auto event = eventCast<const RoomMessageEvent>(room->messageEvents().at(0).get());
auto linkPreviewer = LinkPreviewer(room, event);
QCOMPARE(linkPreviewer.empty(), false);
QCOMPARE(linkPreviewer.url(), QUrl("https://kde.org"_ls));
room->syncNewEvents(QStringLiteral("test-linkpreviewerreplace-sync.json"));
auto linkPreviewer = LinkPreviewer(LinkPreviewer::linkPreview(event.get()), connection);
QCOMPARE(linkPreviewer.empty(), true);
QCOMPARE(linkPreviewer.url(), QUrl());

View File

@@ -394,6 +394,7 @@
<content_attribute id="social-chat">intense</content_attribute>
</content_rating>
<releases>
<release version="24.02.2" date="2024-04-11"/>
<release version="24.02.1" date="2024-03-21"/>
<release version="24.02.0" date="2024-02-28">
<url>https://kde.org/announcements/megarelease/6/#neochat</url>

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

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

@@ -174,6 +174,9 @@ add_library(neochat STATIC
sharehandler.h
models/roomtreeitem.cpp
models/roomtreeitem.h
foreigntypes.h
models/threepidmodel.cpp
models/threepidmodel.h
)
set_source_files_properties(qml/OsmLocationPlugin.qml PROPERTIES
@@ -183,7 +186,7 @@ set_source_files_properties(qml/OsmLocationPlugin.qml PROPERTIES
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
qml/Main.qml
qml/AccountMenu.qml
qml/ExploreComponent.qml
qml/ExploreComponentMobile.qml
@@ -200,7 +203,6 @@ qt_add_qml_module(neochat URI org.kde.neochat NO_PLUGIN
qml/ExplorerDelegate.qml
qml/InviteUserPage.qml
qml/ImageEditorPage.qml
qml/WelcomePage.qml
qml/NeochatMaximizeComponent.qml
qml/FancyEffectsContainer.qml
qml/TypingPane.qml
@@ -213,19 +215,6 @@ qt_add_qml_module(neochat URI org.kde.neochat NO_PLUGIN
qml/PieProgressBar.qml
qml/QuickFormatBar.qml
qml/EmojiPicker.qml
qml/LoginStep.qml
qml/Login.qml
qml/Homeserver.qml
qml/Username.qml
qml/RegisterPassword.qml
qml/Captcha.qml
qml/Terms.qml
qml/Email.qml
qml/Password.qml
qml/LoginRegister.qml
qml/Loading.qml
qml/LoginMethod.qml
qml/Sso.qml
qml/UserDetailDialog.qml
qml/CreateRoomDialog.qml
qml/EmojiDialog.qml
@@ -291,6 +280,7 @@ qt_add_qml_module(neochat URI org.kde.neochat NO_PLUGIN
qml/AccountSwitchDialog.qml
qml/ConfirmLeaveDialog.qml
qml/CodeMaximizeComponent.qml
qml/EditStateDialog.qml
RESOURCES
qml/confetti.png
qml/glowdot.png
@@ -299,6 +289,7 @@ qt_add_qml_module(neochat URI org.kde.neochat NO_PLUGIN
add_subdirectory(settings)
add_subdirectory(timeline)
add_subdirectory(devtools)
add_subdirectory(login)
if(UNIX)
qt_target_qml_sources(neochat QML_FILES qml/ShareAction.qml)
@@ -390,7 +381,7 @@ if (NOT ANDROID AND NOT WIN32 AND NOT APPLE)
endif()
target_include_directories(neochat PRIVATE ${CMAKE_BINARY_DIR} ${CMAKE_CURRENT_SOURCE_DIR}/models ${CMAKE_CURRENT_SOURCE_DIR}/enums)
target_link_libraries(neochat PRIVATE settingsplugin timelineplugin devtoolsplugin)
target_link_libraries(neochat PRIVATE settingsplugin timelineplugin devtoolsplugin loginplugin)
target_link_libraries(neochat PUBLIC
Qt::Core
Qt::Quick

View File

@@ -91,7 +91,7 @@ class ChatDocumentHandler : public QObject
Q_PROPERTY(CompletionModel *completionModel READ completionModel CONSTANT)
/**
* @brief The current room that the the text document is being handled for.
* @brief The current room that the text document is being handled for.
*/
Q_PROPERTY(NeoChatRoom *room READ room WRITE setRoom NOTIFY roomChanged)

View File

@@ -157,7 +157,7 @@ void Controller::addConnection(NeoChatConnection *c)
});
connect(c, &NeoChatConnection::loggedOut, this, [this, c] {
if (accounts().count() > 1) {
// Only set the connection if the the account being logged out is currently active
// Only set the connection if the account being logged out is currently active
if (c == activeConnection()) {
setActiveConnection(dynamic_cast<NeoChatConnection *>(accounts().accounts()[0]));
}

View File

@@ -23,7 +23,7 @@ ColumnLayout {
model: root.connection.accountDataEventTypes
delegate: FormCard.FormButtonDelegate {
text: modelData
onClicked: applicationWindow().pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat', 'MessageSourceSheet.qml'), {
onClicked: applicationWindow().pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat', 'MessageSourceSheet'), {
sourceText: root.connection.accountDataJsonString(modelData)
}, {
title: i18nc("@title:window", "Event Source"),

View File

@@ -8,6 +8,7 @@ qt_add_qml_module(devtools
QML_FILES
DevtoolsPage.qml
AccountData.qml
DebugOptions.qml
FeatureFlagPage.qml
RoomData.qml
ServerData.qml

View File

@@ -0,0 +1,27 @@
// SPDX-FileCopyrightText: 2024 James Graham <james.h.graham@protonmail.com>
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
import QtQuick
import QtQuick.Layouts
import org.kde.kirigami as Kirigami
import org.kde.kirigamiaddons.formcard as FormCard
import org.kde.neochat
FormCard.FormCardPage {
id: root
FormCard.FormCard {
Layout.topMargin: Kirigami.Units.largeSpacing
FormCard.FormCheckDelegate {
id: roomAccountDataVisibleCheck
text: i18nc("@option:check Enable the matrix 'threads' feature", "Always allow device verification")
description: i18n("Allow the user to start a verification session with devices that were already verified")
checked: Config.alwaysVerifyDevice
onToggled: Config.alwaysVerifyDevice = checked
}
}
}

View File

@@ -25,6 +25,11 @@ FormCard.FormCardPage {
readonly property real tabWidth: tabBar.width / tabBar.count
QQC2.TabButton {
text: i18nc("@title:tab", "Debug Options")
implicitWidth: tabBar.tabWidth
}
QQC2.TabButton {
text: qsTr("Room Data")
@@ -52,6 +57,7 @@ FormCard.FormCardPage {
currentIndex: tabBar.currentIndex
DebugOptions {}
RoomData {
room: root.room
connection: root.connection

View File

@@ -7,7 +7,7 @@ import QtQuick.Layouts
import org.kde.kirigami as Kirigami
import org.kde.kirigamiaddons.formcard as FormCard
import org.kde.neochat.config
import org.kde.neochat
FormCard.FormCardPage {
id: root

View File

@@ -46,7 +46,7 @@ ColumnLayout {
model: root.room.accountDataEventTypes
delegate: FormCard.FormButtonDelegate {
text: modelData
onClicked: applicationWindow().pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat', 'MessageSourceSheet.qml'), {
onClicked: applicationWindow().pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat', 'MessageSourceSheet'), {
sourceText: root.room.roomAcountDataJson(text)
}, {
title: i18n("Event Source"),
@@ -74,14 +74,9 @@ ColumnLayout {
description: i18ncp("'Event' being some JSON data, not something physically happening.", "%1 event of this type", "%1 events of this type", model.eventCount)
onClicked: {
if (model.eventCount === 1) {
onClicked: applicationWindow().pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat', 'MessageSourceSheet.qml'), {
sourceText: stateModel.stateEventJson(stateModel.index(model.index, 0))
}, {
title: i18n("Event Source"),
width: Kirigami.Units.gridUnit * 25
})
openEventSource(model.type, model.stateKey);
} else {
pageStack.pushDialogLayer(stateKeysComponent, {
pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat.devtools', 'StateKeys'), {
room: root.room,
eventType: model.type
}, {
@@ -91,9 +86,17 @@ ColumnLayout {
}
}
}
Component {
id: stateKeysComponent
StateKeys {}
}
}
function openEventSource(type: string, stateKey: string): void {
onClicked: applicationWindow().pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat', 'MessageSourceSheet'), {
model: stateModel,
allowEdit: true,
room: root.room,
type: type,
stateKey: stateKey,
}, {
title: i18n("Event Source"),
width: Kirigami.Units.gridUnit * 25
});
}
}

View File

@@ -31,13 +31,21 @@ FormCard.FormCardPage {
delegate: FormCard.FormButtonDelegate {
text: model.stateKey
onClicked: applicationWindow().pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat', 'MessageSourceSheet.qml'), {
sourceText: stateKeysModel.stateEventJson(stateKeysModel.index(model.index, 0))
}, {
title: i18nc("@title:window", "Event Source"),
width: Kirigami.Units.gridUnit * 25
})
onClicked: openEventSource(model.stateKey)
}
}
}
function openEventSource(stateKey: string): void {
applicationWindow().pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat', 'MessageSourceSheet'), {
model: stateKeysModel,
allowEdit: true,
room: root.room,
type: root.eventType,
stateKey: stateKey
}, {
title: i18nc("@title:window", "Event Source"),
width: Kirigami.Units.gridUnit * 25
});
}
}

View File

@@ -74,6 +74,9 @@ void ImagePackEventContent::fillJson(QJsonObject *o) const
}
imageJson["usage"_ls] = usageJson;
}
if (image.info.has_value()) {
imageJson["info"_ls] = Quotient::EventContent::toInfoJson(*image.info);
}
imagesJson[image.shortcode] = imageJson;
}
(*o)["images"_ls] = imagesJson;

View File

@@ -89,6 +89,4 @@ public:
QUO_EVENT(ImagePackEvent, "im.ponies.room_emotes")
using KeyedStateEventBase::KeyedStateEventBase;
};
REGISTER_EVENT_TYPE(ImagePackEvent)
}

View File

@@ -40,5 +40,4 @@ public:
*/
QJsonArray allow() const;
};
REGISTER_EVENT_TYPE(JoinRulesEvent)
}

56
src/foreigntypes.h Normal file
View File

@@ -0,0 +1,56 @@
// SPDX-FileCopyrightText: 2024 Tobias Fella <tobias.fella@kde.org>
// SPDX-License-Identifier: LGPL-2.0-or-later
#pragma once
#include <QQmlEngine>
#include <Quotient/accountregistry.h>
#include <Quotient/keyverificationsession.h>
#if __has_include("Quotient/e2ee/sssshandler.h")
#include <Quotient/e2ee/sssshandler.h>
#endif
#include "controller.h"
#include "neochatconfig.h"
struct ForeignConfig {
Q_GADGET
QML_FOREIGN(NeoChatConfig)
QML_NAMED_ELEMENT(Config)
QML_SINGLETON
public:
static NeoChatConfig *create(QQmlEngine *, QJSEngine *)
{
QQmlEngine::setObjectOwnership(NeoChatConfig::self(), QQmlEngine::CppOwnership);
return NeoChatConfig::self();
}
};
struct ForeignAccountRegistry {
Q_GADGET
QML_FOREIGN(Quotient::AccountRegistry)
QML_NAMED_ELEMENT(AccountRegistry)
QML_SINGLETON
public:
static Quotient::AccountRegistry *create(QQmlEngine *, QJSEngine *)
{
QQmlEngine::setObjectOwnership(&Controller::instance().accounts(), QQmlEngine::CppOwnership);
return &Controller::instance().accounts();
}
};
struct ForeignKeyVerificationSession {
Q_GADGET
QML_FOREIGN(Quotient::KeyVerificationSession)
QML_NAMED_ELEMENT(KeyVerificationSession)
QML_UNCREATABLE("")
};
#if __has_include("Quotient/e2ee/sssshandler.h")
struct ForeignSSSSHandler {
Q_GADGET
QML_FOREIGN(Quotient::SSSSHandler)
QML_NAMED_ELEMENT(SSSSHandler)
};
#endif

View File

@@ -8,34 +8,22 @@
#include <Quotient/events/roommessageevent.h>
#include "neochatconfig.h"
#include "neochatroom.h"
#include "neochatconnection.h"
#include "utils.h"
using namespace Quotient;
LinkPreviewer::LinkPreviewer(const NeoChatRoom *room, const Quotient::RoomMessageEvent *event, QObject *parent)
LinkPreviewer::LinkPreviewer(const QUrl &url, QObject *parent)
: QObject(parent)
, m_currentRoom(room)
, m_event(event)
, m_loaded(false)
, m_url(linkPreview(event))
, m_url(url)
{
connect(this, &LinkPreviewer::urlChanged, this, &LinkPreviewer::emptyChanged);
Q_ASSERT(dynamic_cast<Connection *>(this->parent()));
if (m_event != nullptr && m_currentRoom != nullptr) {
loadUrlPreview();
connect(m_currentRoom, &NeoChatRoom::urlPreviewEnabledChanged, this, &LinkPreviewer::loadUrlPreview);
// Make sure that we react to edits
connect(m_currentRoom, &NeoChatRoom::replacedEvent, this, [this](const Quotient::RoomEvent *newEvent) {
if (m_event->id() == newEvent->id()) {
m_event = eventCast<const Quotient::RoomMessageEvent>(newEvent);
m_url = linkPreview(m_event);
Q_EMIT urlChanged();
loadUrlPreview();
}
});
}
connect(this, &LinkPreviewer::urlChanged, this, &LinkPreviewer::emptyChanged);
connect(NeoChatConfig::self(), &NeoChatConfig::ShowLinkPreviewChanged, this, &LinkPreviewer::loadUrlPreview);
loadUrlPreview();
}
bool LinkPreviewer::loaded() const
@@ -65,14 +53,14 @@ QUrl LinkPreviewer::url() const
void LinkPreviewer::loadUrlPreview()
{
if (!m_currentRoom || !NeoChatConfig::showLinkPreview() || !m_currentRoom->urlPreviewEnabled()) {
return;
}
if (m_url.scheme() == QStringLiteral("https")) {
m_loaded = false;
Q_EMIT loadedChanged();
auto conn = m_currentRoom->connection();
auto conn = dynamic_cast<Connection *>(this->parent());
if (conn == nullptr) {
return;
}
GetUrlPreviewJob *job = conn->callApi<GetUrlPreviewJob>(m_url);
connect(job, &BaseJob::success, this, [this, job, conn]() {

View File

@@ -53,14 +53,15 @@ class LinkPreviewer : public QObject
Q_PROPERTY(QUrl imageSource READ imageSource NOTIFY imageSourceChanged)
/**
* @brief Whether the there is a link to preview.
* @brief Whether there is a link to preview.
*
* A linkPreviwer is empty if the URL is empty.
*/
Q_PROPERTY(bool empty READ empty NOTIFY emptyChanged)
public:
explicit LinkPreviewer(const NeoChatRoom *room = nullptr, const Quotient::RoomMessageEvent *event = nullptr, QObject *parent = nullptr);
LinkPreviewer() = default;
explicit LinkPreviewer(const QUrl &url, QObject *parent = nullptr);
[[nodiscard]] QUrl url() const;
[[nodiscard]] bool loaded() const;
@@ -76,18 +77,6 @@ public:
*/
static bool hasPreviewableLinks(const Quotient::RoomMessageEvent *event);
private:
const NeoChatRoom *m_currentRoom;
const Quotient::RoomMessageEvent *m_event;
bool m_loaded;
QString m_title = QString();
QString m_description = QString();
QUrl m_imageSource = QUrl();
QUrl m_url;
void loadUrlPreview();
/**
* @brief Return the link to be previewed from the given event.
*
@@ -96,6 +85,15 @@ private:
*/
static QUrl linkPreview(const Quotient::RoomMessageEvent *event);
private:
bool m_loaded;
QString m_title = QString();
QString m_description = QString();
QUrl m_imageSource = QUrl();
QUrl m_url;
void loadUrlPreview();
Q_SIGNALS:
void loadedChanged();
void titleChanged();

23
src/login/CMakeLists.txt Normal file
View File

@@ -0,0 +1,23 @@
# SPDX-FileCopyrightText: 2024 James Graham <james.h.graham@protonmail.com>
# SPDX-License-Identifier: BSD-2-Clause
qt_add_library(login STATIC)
qt_add_qml_module(login
URI org.kde.neochat.login
OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/src/org/kde/neochat/login
QML_FILES
WelcomePage.qml
LoginStep.qml
Captcha.qml
Email.qml
Homeserver.qml
Loading.qml
Login.qml
LoginMethod.qml
LoginRegister.qml
Password.qml
RegisterPassword.qml
Sso.qml
Terms.qml
Username.qml
)

View File

@@ -43,6 +43,6 @@ LoginStep {
}
}
previousAction: Kirigami.Action {
onTriggered: root.processed("Username.qml")
onTriggered: root.processed("Username")
}
}

View File

@@ -55,6 +55,6 @@ LoginStep {
}
}
previousAction: Kirigami.Action {
onTriggered: root.processed("Username.qml")
onTriggered: root.processed("Username")
}
}

View File

@@ -40,9 +40,9 @@ LoginStep {
nextAction: Kirigami.Action {
text: Registration.testing ? i18n("Loading") : null
enabled: Registration.status > Registration.ServerNoRegistration
onTriggered: root.processed("Username.qml")
onTriggered: root.processed("Username")
}
previousAction: Kirigami.Action {
onTriggered: root.processed("LoginRegister.qml")
onTriggered: root.processed("LoginRegister")
}
}

View File

@@ -38,18 +38,18 @@ LoginStep {
text: LoginHelper.isLoggedIn ? i18n("Already logged in") : (LoginHelper.testing && matrixIdField.acceptableInput) ? i18n("Loading…") : i18nc("@action:button", "Continue")
onTriggered: {
if (LoginHelper.supportsSso && LoginHelper.supportsPassword) {
processed("LoginMethod.qml");
processed("LoginMethod");
} else if (LoginHelper.supportsSso) {
processed("Sso.qml");
processed("Sso");
} else {
processed("Password.qml");
processed("Password");
}
}
enabled: LoginHelper.homeserverReachable
}
previousAction: Kirigami.Action {
onTriggered: {
root.processed("LoginRegister.qml");
root.processed("LoginRegister");
}
}
}

View File

@@ -18,12 +18,12 @@ LoginStep {
FormCard.FormButtonDelegate {
id: loginPasswordButton
text: i18nc("@action:button", "Login with password")
onClicked: processed("Password.qml")
onClicked: processed("Password")
}
FormCard.FormButtonDelegate {
id: loginSsoButton
text: i18nc("@action:button", "Login with single sign-on")
onClicked: processed("Sso.qml")
onClicked: processed("Sso")
}
}

View File

@@ -22,13 +22,13 @@ LoginStep {
FormCard.FormButtonDelegate {
id: loginButton
text: i18nc("@action:button", "Login")
onClicked: root.processed("Login.qml")
onClicked: root.processed("Login")
}
FormCard.FormDelegateSeparator {}
FormCard.FormButtonDelegate {
text: i18nc("@action:button", "Register")
onClicked: root.processed("Homeserver.qml")
onClicked: root.processed("Homeserver")
}
}

View File

@@ -15,7 +15,7 @@ LoginStep {
Connections {
target: LoginHelper
function onConnected() {
processed("Loading.qml");
processed("Loading");
}
}
@@ -46,6 +46,6 @@ LoginStep {
}
}
previousAction: Kirigami.Action {
onTriggered: processed("Login.qml")
onTriggered: processed("Login")
}
}

View File

@@ -47,6 +47,6 @@ LoginStep {
}
previousAction: Kirigami.Action {
onTriggered: root.processed("Username.qml")
onTriggered: root.processed("Username")
}
}

View File

@@ -22,7 +22,7 @@ LoginStep {
UrlHelper.openUrl(LoginHelper.ssoUrl);
}
function onConnected() {
processed("Loading.qml");
processed("Loading");
}
}
@@ -31,7 +31,7 @@ LoginStep {
}
previousAction: Kirigami.Action {
onTriggered: processed("Login.qml")
onTriggered: processed("Login")
}
nextAction: Kirigami.Action {

View File

@@ -33,6 +33,6 @@ LoginStep {
}
}
previousAction: Kirigami.Action {
onTriggered: root.processed("Username.qml")
onTriggered: root.processed("Username")
}
}

View File

@@ -37,11 +37,11 @@ LoginStep {
nextAction: Kirigami.Action {
text: Registration.status === Registration.TestingUsername ? i18n("Loading") : null
onTriggered: root.processed("RegisterPassword.qml")
onTriggered: root.processed("RegisterPassword")
enabled: Registration.status === Registration.Ready
}
previousAction: Kirigami.Action {
onTriggered: root.processed("Homeserver.qml")
onTriggered: root.processed("Homeserver")
}
}

View File

@@ -10,7 +10,6 @@ import org.kde.kirigamiaddons.formcard as FormCard
import org.kde.neochat
import org.kde.neochat.settings
import org.kde.neochat.accounts
FormCard.FormCardPage {
id: root
@@ -19,7 +18,7 @@ FormCard.FormCardPage {
property bool _showExisting: showExisting && root.currentStepString === root.initialStep
property alias currentStep: module.item
property string currentStepString: initialStep
property string initialStep: "LoginRegister.qml"
property string initialStep: "LoginRegister"
signal connectionChosen
@@ -129,14 +128,14 @@ FormCard.FormCardPage {
Loader {
id: module
Layout.fillWidth: true
sourceComponent: Qt.createComponent('org.kde.neochat', root.initialStep)
sourceComponent: Qt.createComponent('org.kde.neochat.login', root.initialStep)
Connections {
id: stepConnections
target: currentStep
function onProcessed(nextStep: string): void {
module.source = nextStep;
module.source = nextStep + ".qml";
root.currentStepString = nextStep;
headerMessage.text = "";
headerMessage.visible = false;
@@ -167,16 +166,16 @@ FormCard.FormCardPage {
target: Registration
function onNextStepChanged() {
if (Registration.nextStep === "m.login.recaptcha") {
stepConnections.onProcessed("Captcha.qml");
stepConnections.onProcessed("Captcha");
}
if (Registration.nextStep === "m.login.terms") {
stepConnections.onProcessed("Terms.qml");
stepConnections.onProcessed("Terms");
}
if (Registration.nextStep === "m.login.email.identity") {
stepConnections.onProcessed("Email.qml");
stepConnections.onProcessed("Email");
}
if (Registration.nextStep === "loading") {
stepConnections.onProcessed("Loading.qml");
stepConnections.onProcessed("Loading");
}
}
}
@@ -217,7 +216,7 @@ FormCard.FormCardPage {
FormCard.FormButtonDelegate {
text: i18nc("@action:button", "Open proxy settings")
icon.name: "settings-configure"
onClicked: pageStack.pushDialogLayer(Qt.createComponent("org.kde.neochat.settings", "NetworkProxyPage.qml"), {}, {
onClicked: pageStack.pushDialogLayer(Qt.createComponent("org.kde.neochat.settings", "NetworkProxyPage"), {}, {
title: i18nc("@title:window", "Proxy Settings")
});
}

View File

@@ -35,11 +35,6 @@
#include "neochat-version.h"
#include <Quotient/accountregistry.h>
#if __has_include("Quotient/e2ee/sssshandler.h")
#include <Quotient/e2ee/sssshandler.h>
#endif
#include <Quotient/keyverificationsession.h>
#include <Quotient/networkaccessmanager.h>
#include "blurhashimageprovider.h"
@@ -231,15 +226,9 @@ int main(int argc, char *argv[])
Q_IMPORT_QML_PLUGIN(org_kde_neochat_settingsPlugin)
Q_IMPORT_QML_PLUGIN(org_kde_neochat_timelinePlugin)
Q_IMPORT_QML_PLUGIN(org_kde_neochat_devtoolsPlugin)
Q_IMPORT_QML_PLUGIN(org_kde_neochat_loginPlugin)
qml_register_types_org_kde_neochat();
qmlRegisterSingletonInstance("org.kde.neochat.config", 1, 0, "Config", NeoChatConfig::self());
qmlRegisterSingletonInstance("org.kde.neochat.accounts", 1, 0, "AccountRegistry", &Controller::instance().accounts());
qmlRegisterUncreatableType<KeyVerificationSession>("com.github.quotient_im.libquotient", 1, 0, "KeyVerificationSession", {});
#if __has_include("Quotient/e2ee/sssshandler.h")
qmlRegisterType<SSSSHandler>("com.github.quotient_im.libquotient", 1, 0, "SSSSHandler");
#endif
QQmlApplicationEngine engine;
@@ -290,7 +279,7 @@ int main(int argc, char *argv[])
engine.addImageProvider(QLatin1String("mxc"), MatrixImageProvider::create(&engine, &engine));
engine.addImageProvider(QLatin1String("blurhash"), new BlurhashImageProvider);
engine.load(QUrl(QStringLiteral("qrc:/qt/qml/org/kde/neochat/qml/main.qml")));
engine.loadFromModule("org.kde.neochat", "Main");
if (engine.rootObjects().isEmpty()) {
return -1;
}

View File

@@ -7,3 +7,5 @@ void MediaManager::startPlayback()
{
Q_EMIT playbackStarted();
}
#include "moc_mediamanager.cpp"

View File

@@ -3,7 +3,11 @@
#include "accountemoticonmodel.h"
#include <QImage>
#include <QMimeDatabase>
#include <Quotient/csapi/content-repo.h>
#include <Quotient/events/eventcontent.h>
#include <qcoro/qcorosignal.h>
using namespace Quotient;
@@ -162,7 +166,15 @@ QCoro::Task<void> AccountEmoticonModel::doSetEmoticonImage(int index, QUrl sourc
co_return;
}
m_images->images[index].url = job->contentUri();
m_images->images[index].info = none;
auto mime = QMimeDatabase().mimeTypeForUrl(source);
source.setScheme("file"_ls);
QFileInfo fileInfo(source.isLocalFile() ? source.toLocalFile() : source.toString());
EventContent::ImageInfo info;
if (mime.name().startsWith("image/"_ls)) {
QImage image(source.toLocalFile());
info = EventContent::ImageInfo(source, fileInfo.size(), mime, image.size(), fileInfo.fileName());
}
m_images->images[index].info = info;
QJsonObject data;
m_images->fillJson(&data);
m_connection->setAccountData("im.ponies.user_emotes"_ls, data);
@@ -175,11 +187,21 @@ QCoro::Task<void> AccountEmoticonModel::doAddEmoticon(QUrl source, QString short
if (job->error() != BaseJob::NoError) {
co_return;
}
auto mime = QMimeDatabase().mimeTypeForUrl(source);
source.setScheme("file"_ls);
QFileInfo fileInfo(source.isLocalFile() ? source.toLocalFile() : source.toString());
EventContent::ImageInfo info;
if (mime.name().startsWith("image/"_ls)) {
QImage image(source.toLocalFile());
info = EventContent::ImageInfo(source, fileInfo.size(), mime, image.size(), fileInfo.fileName());
}
m_images->images.append(ImagePackEventContent::ImagePackImage{
shortcode,
job->contentUri(),
description,
none,
info,
QStringList{type},
});
QJsonObject data;

View File

@@ -208,3 +208,5 @@ void ItineraryModel::sendToItinerary()
job->start();
#endif
}
#include "moc_itinerarymodel.cpp"

View File

@@ -62,3 +62,5 @@ void LineModel::resetModel()
beginResetModel();
endResetModel();
}
#include "moc_linemodel.cpp"

View File

@@ -2,13 +2,13 @@
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
#include "messagecontentmodel.h"
#include "neochatconfig.h"
#include <QImageReader>
#include <Quotient/events/redactionevent.h>
#include <Quotient/events/roommessageevent.h>
#include <Quotient/events/stickerevent.h>
#include <Quotient/room.h>
#include <KLocalizedString>
@@ -23,9 +23,12 @@
#include "filetype.h"
#include "itinerarymodel.h"
#include "linkpreviewer.h"
#include "neochatconnection.h"
#include "neochatroom.h"
#include "texthandler.h"
using namespace Quotient;
MessageContentModel::MessageContentModel(const Quotient::RoomEvent *event, NeoChatRoom *room)
: QAbstractListModel(nullptr)
, m_room(room)
@@ -76,6 +79,21 @@ MessageContentModel::MessageContentModel(const Quotient::RoomEvent *event, NeoCh
if (m_event != nullptr && eventId == m_event->id()) {
updateComponents();
Q_EMIT dataChanged(index(0), index(rowCount() - 1), {FileTransferInfoRole});
QString mxcUrl;
if (auto event = eventCast<const Quotient::RoomMessageEvent>(m_event)) {
if (event->hasFileContent()) {
mxcUrl = event->content()->fileInfo()->url().toString();
}
} else if (auto event = eventCast<const Quotient::StickerEvent>(m_event)) {
mxcUrl = event->image().fileInfo()->url().toString();
}
if (mxcUrl.isEmpty()) {
return;
}
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) {
@@ -92,23 +110,11 @@ MessageContentModel::MessageContentModel(const Quotient::RoomEvent *event, NeoCh
endResetModel();
}
});
connect(m_room, &NeoChatRoom::urlPreviewEnabledChanged, this, &MessageContentModel::updateLinkPreviewer);
connect(NeoChatConfig::self(), &NeoChatConfig::ShowLinkPreviewChanged, this, &MessageContentModel::updateLinkPreviewer);
}
if (const auto event = eventCast<const Quotient::RoomMessageEvent>(m_event)) {
if (LinkPreviewer::hasPreviewableLinks(event)) {
m_linkPreviewer = new LinkPreviewer(m_room, event, this);
connect(m_linkPreviewer, &LinkPreviewer::loadedChanged, [this]() {
if (m_linkPreviewer->loaded()) {
// HACK: Because DelegateChooser can't switch the delegate on dataChanged it has to think there is a new delegate.
beginResetModel();
m_components[m_components.size() - 1].type = MessageComponentType::LinkPreview;
endResetModel();
}
});
}
}
updateLinkPreviewer();
updateComponents();
}
@@ -155,14 +161,7 @@ QVariant MessageContentModel::data(const QModelIndex &index, int role) const
return eventHandler.getMediaInfo();
}
if (role == FileTransferInfoRole) {
if (auto event = eventCast<const Quotient::RoomMessageEvent>(m_event)) {
if (event->hasFileContent()) {
return QVariant::fromValue(m_room->fileTransferInfo(event->id()));
}
}
if (auto event = eventCast<const Quotient::StickerEvent>(m_event)) {
return QVariant::fromValue(m_room->fileTransferInfo(event->id()));
}
return QVariant::fromValue(fileInfo());
}
if (role == ItineraryModelRole) {
return QVariant::fromValue<ItineraryModel *>(m_itineraryModel);
@@ -270,15 +269,7 @@ void MessageContentModel::updateComponents(bool isEditing)
} else if (eventHandler.messageComponentType() == MessageComponentType::File) {
m_components += MessageComponent{MessageComponentType::File, QString(), {}};
if (m_emptyItinerary) {
Quotient::FileTransferInfo fileTransferInfo;
if (auto event = eventCast<const Quotient::RoomMessageEvent>(m_event)) {
if (event->hasFileContent()) {
fileTransferInfo = m_room->fileTransferInfo(event->id());
}
}
if (auto event = eventCast<const Quotient::StickerEvent>(m_event)) {
fileTransferInfo = m_room->fileTransferInfo(event->id());
}
auto fileTransferInfo = fileInfo();
#ifndef Q_OS_ANDROID
KSyntaxHighlighting::Repository repository;
@@ -319,6 +310,44 @@ void MessageContentModel::updateComponents(bool isEditing)
endResetModel();
}
void MessageContentModel::updateLinkPreviewer()
{
if (m_room == nullptr || m_event == nullptr) {
if (m_linkPreviewer != nullptr) {
m_linkPreviewer->disconnect(this);
m_linkPreviewer = nullptr;
updateComponents();
}
return;
}
if (!m_room->urlPreviewEnabled()) {
if (m_linkPreviewer != nullptr) {
m_linkPreviewer->disconnect(this);
m_linkPreviewer = nullptr;
updateComponents();
}
return;
}
if (const auto event = eventCast<const Quotient::RoomMessageEvent>(m_event)) {
if (LinkPreviewer::hasPreviewableLinks(event)) {
m_linkPreviewer = dynamic_cast<NeoChatConnection *>(m_room->connection())->previewerForLink(LinkPreviewer::linkPreview(event));
updateComponents();
if (m_linkPreviewer != nullptr) {
connect(m_linkPreviewer, &LinkPreviewer::loadedChanged, [this]() {
if (m_linkPreviewer != nullptr && m_linkPreviewer->loaded()) {
// HACK: Because DelegateChooser can't switch the delegate on dataChanged it has to think there is a new delegate.
beginResetModel();
m_components[m_components.size() - 1].type = MessageComponentType::LinkPreview;
endResetModel();
}
});
}
}
}
}
void MessageContentModel::updateItineraryModel()
{
if (m_room == nullptr || m_event == nullptr) {
@@ -327,7 +356,7 @@ void MessageContentModel::updateItineraryModel()
if (auto event = eventCast<const Quotient::RoomMessageEvent>(m_event)) {
if (event->hasFileContent()) {
auto filePath = m_room->fileTransferInfo(event->id()).localPath;
auto filePath = fileInfo().localPath;
if (filePath.isEmpty() && m_itineraryModel != nullptr) {
delete m_itineraryModel;
m_itineraryModel = nullptr;
@@ -354,3 +383,43 @@ void MessageContentModel::updateItineraryModel()
}
}
}
FileTransferInfo MessageContentModel::fileInfo() const
{
if (m_room == nullptr || m_event == nullptr) {
return {};
}
QString mxcUrl;
int total;
if (auto event = eventCast<const Quotient::RoomMessageEvent>(m_event)) {
if (event->hasFileContent()) {
mxcUrl = event->content()->fileInfo()->url().toString();
total = event->content()->fileInfo()->payloadSize;
}
} else if (auto event = eventCast<const Quotient::StickerEvent>(m_event)) {
mxcUrl = event->image().fileInfo()->url().toString();
total = event->image().fileInfo()->payloadSize;
}
auto config = KSharedConfig::openStateConfig(QStringLiteral("neochatdownloads"))->group(QStringLiteral("downloads"));
if (!config.hasKey(mxcUrl.mid(6))) {
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_event->id());
}
// TODO: we could check the hash here
return FileTransferInfo{
.status = FileTransferInfo::Completed,
.isUpload = false,
.progress = total,
.total = total,
.localDir = QUrl(info.dir().path()),
.localPath = QUrl::fromLocalFile(path),
};
}
#include "moc_messagecontentmodel.cpp"

View File

@@ -6,6 +6,8 @@
#include <QAbstractListModel>
#include <QQmlEngine>
#include <Quotient/room.h>
#include "enums/messagecomponenttype.h"
#include "eventhandler.h"
#include "itinerarymodel.h"
@@ -93,9 +95,12 @@ private:
QList<MessageComponent> m_components;
void updateComponents(bool isEditing = false);
LinkPreviewer *m_linkPreviewer = nullptr;
QPointer<LinkPreviewer> m_linkPreviewer;
ItineraryModel *m_itineraryModel = nullptr;
void updateLinkPreviewer();
void updateItineraryModel();
bool m_emptyItinerary = false;
Quotient::FileTransferInfo fileInfo() const;
};

View File

@@ -162,3 +162,5 @@ QString NotificationsModel::nextToken() const
{
return m_nextToken;
}
#include "moc_notificationsmodel.cpp"

View File

@@ -7,6 +7,7 @@
#include "neochatconfig.h"
#include "neochatconnection.h"
#include "neochatroomtype.h"
#include "roommanager.h"
#include "roomtreemodel.h"
#include "spacehierarchycache.h"
@@ -32,6 +33,12 @@ SortFilterRoomTreeModel::SortFilterRoomTreeModel(RoomTreeModel *sourceModel, QOb
});
connect(NeoChatConfig::self(), &NeoChatConfig::CollapsedChanged, this, &SortFilterRoomTreeModel::invalidateFilter);
connect(NeoChatConfig::self(), &NeoChatConfig::AllRoomsInHomeChanged, this, [this]() {
invalidateFilter();
if (NeoChatConfig::self()->allRoomsInHome()) {
RoomManager::instance().resetState();
}
});
}
void SortFilterRoomTreeModel::setRoomSortOrder(SortFilterRoomTreeModel::RoomSortOrder sortOrder)
@@ -154,6 +161,11 @@ bool SortFilterRoomTreeModel::filterAcceptsRow(int source_row, const QModelIndex
return false;
}
static auto config = NeoChatConfig::self();
if (config->allRoomsInHome() && RoomManager::instance().currentSpace().isEmpty()) {
return acceptRoom;
}
if (m_activeSpaceId.isEmpty()) {
if (!SpaceHierarchyCache::instance().isChild(sourceModel()->data(index, RoomTreeModel::RoomIdRole).toString())) {
return acceptRoom;

View File

@@ -95,7 +95,11 @@ void SpaceChildrenModel::refreshModel()
});
}
#if Quotient_VERSION_MINOR >= 9
void SpaceChildrenModel::insertChildren(std::vector<Quotient::GetSpaceHierarchyJob::SpaceHierarchyRoomsChunk> children, const QModelIndex &parent)
#else
void SpaceChildrenModel::insertChildren(std::vector<Quotient::GetSpaceHierarchyJob::ChildRoomsChunk> children, const QModelIndex &parent)
#endif
{
SpaceTreeItem *parentItem = getItem(parent);

View File

@@ -143,5 +143,10 @@ private:
SpaceTreeItem *getItem(const QModelIndex &index) const;
void refreshModel();
#if Quotient_VERSION_MINOR >= 9
void insertChildren(std::vector<Quotient::GetSpaceHierarchyJob::SpaceHierarchyRoomsChunk> children, const QModelIndex &parent = QModelIndex());
#else
void insertChildren(std::vector<Quotient::GetSpaceHierarchyJob::ChildRoomsChunk> children, const QModelIndex &parent = QModelIndex());
#endif
};

View File

@@ -77,12 +77,14 @@ void StateKeysModel::setEventType(const QString &eventType)
loadState();
}
QByteArray StateKeysModel::stateEventJson(const QModelIndex &index)
QByteArray StateKeysModel::stateEventJson(const QString &type, const QString &stateKey)
{
const auto row = index.row();
const auto event = m_stateKeys[row];
const auto json = event->fullJson();
return QJsonDocument(json).toJson();
return QJsonDocument(m_room->currentState().get(type, stateKey)->fullJson()).toJson();
}
QByteArray StateKeysModel::stateEventContentJson(const QString &type, const QString &stateKey)
{
return QJsonDocument(m_room->currentState().get(type, stateKey)->contentJson()).toJson();
}
#include "moc_statekeysmodel.cpp"

View File

@@ -69,7 +69,12 @@ public:
/**
* @brief Get the full JSON for an event.
*/
Q_INVOKABLE QByteArray stateEventJson(const QModelIndex &index);
Q_INVOKABLE QByteArray stateEventJson(const QString &type, const QString &stateKey);
/**
* @brief Get the content JSON for an event.
*/
Q_INVOKABLE QByteArray stateEventContentJson(const QString &type, const QString &stateKey);
Q_SIGNALS:
void roomChanged();

View File

@@ -10,7 +10,11 @@ StateModel::StateModel(QObject *parent)
QHash<int, QByteArray> StateModel::roleNames() const
{
return {{TypeRole, "type"}, {EventCountRole, "eventCount"}};
return {
{TypeRole, "type"},
{EventCountRole, "eventCount"},
{StateKeyRole, "stateKey"},
};
}
QVariant StateModel::data(const QModelIndex &index, int role) const
{
@@ -20,6 +24,8 @@ QVariant StateModel::data(const QModelIndex &index, int role) const
return m_stateEvents.keys()[row];
case EventCountRole:
return m_stateEvents.values()[row].count();
case StateKeyRole:
return m_stateEvents.values()[row][0];
}
return {};
}
@@ -63,14 +69,14 @@ void StateModel::setRoom(NeoChatRoom *room)
});
}
QByteArray StateModel::stateEventJson(const QModelIndex &index)
QByteArray StateModel::stateEventJson(const QString &type, const QString &stateKey)
{
auto row = index.row();
const auto type = m_stateEvents.keys()[row];
const auto stateKey = m_stateEvents.values()[row][0];
const auto event = m_room->currentState().get(type, stateKey);
return QJsonDocument(m_room->currentState().get(type, stateKey)->fullJson()).toJson();
}
return QJsonDocument(event->fullJson()).toJson();
QByteArray StateModel::stateEventContentJson(const QString &type, const QString &stateKey)
{
return QJsonDocument(m_room->currentState().get(type, stateKey)->contentJson()).toJson();
}
#include "moc_statemodel.cpp"

View File

@@ -30,6 +30,7 @@ public:
enum Roles {
TypeRole = 0, /**< The type of the state event. */
EventCountRole, /**< Number of events of this type. */
StateKeyRole, /**<State key. Only valid if there's exactly one event of this type. */
};
Q_ENUM(Roles)
@@ -62,7 +63,12 @@ public:
/**
* @brief Get the full JSON for an event.
*/
Q_INVOKABLE QByteArray stateEventJson(const QModelIndex &index);
Q_INVOKABLE QByteArray stateEventJson(const QString &type, const QString &stateKey);
/**
* @brief Get the content JSON for an event.
*/
Q_INVOKABLE QByteArray stateEventContentJson(const QString &type, const QString &stateKey);
Q_SIGNALS:
void roomChanged();
@@ -71,7 +77,7 @@ private:
QPointer<NeoChatRoom> m_room;
/**
* @brief A map from state event type to number of events of that type
* @brief A map from state event type to state keys
*/
QMap<QString, QList<QString>> m_stateEvents;
void loadState();

View File

@@ -0,0 +1,59 @@
// SPDX-FileCopyrightText: 2024 James Graham <james.h.graham@protonmail.com>
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
#include "threepidmodel.h"
#include "neochatconnection.h"
ThreePIdModel::ThreePIdModel(NeoChatConnection *connection)
: QAbstractListModel(connection)
{
Q_ASSERT(connection);
connect(connection, &NeoChatConnection::stateChanged, this, [this]() {
const auto connection = dynamic_cast<NeoChatConnection *>(this->parent());
if (connection != nullptr && connection->isLoggedIn()) {
const auto threePIdJob = connection->callApi<Quotient::GetAccount3PIDsJob>();
connect(threePIdJob, &Quotient::BaseJob::success, this, [this, threePIdJob]() {
beginResetModel();
m_threePIds = threePIdJob->threepids();
endResetModel();
});
}
});
}
QVariant ThreePIdModel::data(const QModelIndex &index, int role) const
{
if (!index.isValid()) {
return {};
}
if (index.row() >= rowCount()) {
qDebug() << "ThreePIdModel, something's wrong: index.row() >= m_threePIds.count()";
return {};
}
if (role == AddressRole) {
return m_threePIds.at(index.row()).address;
}
if (role == MediumRole) {
return m_threePIds.at(index.row()).medium;
}
return {};
}
int ThreePIdModel::rowCount(const QModelIndex &parent) const
{
Q_UNUSED(parent)
return m_threePIds.count();
}
QHash<int, QByteArray> ThreePIdModel::roleNames() const
{
return {
{AddressRole, QByteArrayLiteral("address")},
{MediumRole, QByteArrayLiteral("medium")},
};
}
#include "moc_threepidmodel.cpp"

View File

@@ -0,0 +1,58 @@
// SPDX-FileCopyrightText: 2024 James Graham <james.h.graham@protonmail.com>
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
#pragma once
#include <QAbstractListModel>
#include <QQmlEngine>
#include <Quotient/csapi/administrative_contact.h>
class NeoChatConnection;
/**
* @class ThreePIdModel
*
* This class defines the model for visualising an account's 3PIDs.
*/
class ThreePIdModel : public QAbstractListModel
{
Q_OBJECT
QML_ELEMENT
QML_UNCREATABLE("")
public:
/**
* @brief Defines the model roles.
*/
enum EventRoles {
AddressRole = Qt::DisplayRole, /**< The third-party identifier address. */
MediumRole, /**< The medium of the third-party identifier. One of: [email, msisdn]. */
};
explicit ThreePIdModel(NeoChatConnection *parent);
/**
* @brief Get the given role value at the given index.
*
* @sa QAbstractItemModel::data
*/
[[nodiscard]] QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
/**
* @brief Number of rows in the model.
*
* @sa QAbstractItemModel::rowCount
*/
[[nodiscard]] int rowCount(const QModelIndex &parent = QModelIndex()) const override;
/**
* @brief Returns a mapping from Role enum values to role names.
*
* @sa EventRoles, QAbstractItemModel::roleNames()
*/
[[nodiscard]] QHash<int, QByteArray> roleNames() const override;
private:
QVector<Quotient::GetAccount3PIDsJob::ThirdPartyIdentifier> m_threePIds;
};

View File

@@ -93,3 +93,5 @@ QHash<int, QByteArray> TimelineEndModel::roleNames() const
{
return {{DelegateTypeRole, "delegateType"}};
}
#include "moc_timelinemodel.cpp"

View File

@@ -5,6 +5,10 @@
http://www.kde.org/standards/kcfg/1.0/kcfg.xsd" >
<kcfgfile name="neochatrc" />
<group name="General">
<entry name="AllRoomsInHome" type="bool">
<label>Show all rooms in the home tab</label>
<default>false</default>
</entry>
<entry name="CollapsedSections" type="IntList">
<label>Collapsed sections in the room list</label>
</entry>
@@ -156,6 +160,12 @@
<default></default>
</entry>
</group>
<group name="Debug">
<entry name="AlwaysVerifyDevice" type="bool">
<label>Always allow device verification</label>
<default>false</default>
</entry>
</group>
<group name="FeatureFlags">
<entry name="Threads" type="bool">
<label>Enable threads</label>

View File

@@ -9,11 +9,14 @@
#include "controller.h"
#include "jobs/neochatchangepasswordjob.h"
#include "jobs/neochatdeactivateaccountjob.h"
#include "linkpreviewer.h"
#include "neochatconfig.h"
#include "neochatroom.h"
#include "roommanager.h"
#include "spacehierarchycache.h"
#include <Quotient/connection.h>
#include <Quotient/jobs/basejob.h>
#include <Quotient/quotient_common.h>
#include <qt6keychain/keychain.h>
@@ -39,12 +42,14 @@ using namespace Qt::StringLiterals;
NeoChatConnection::NeoChatConnection(QObject *parent)
: Connection(parent)
, m_threePIdModel(new ThreePIdModel(this))
{
connectSignals();
}
NeoChatConnection::NeoChatConnection(const QUrl &server, QObject *parent)
: Connection(server, parent)
, m_threePIdModel(new ThreePIdModel(this))
{
connectSignals();
}
@@ -246,6 +251,11 @@ void NeoChatConnection::deactivateAccount(const QString &password)
});
}
ThreePIdModel *NeoChatConnection::threePIdModel() const
{
return m_threePIdModel;
}
void NeoChatConnection::createRoom(const QString &name, const QString &topic, const QString &parent, bool setChildParent)
{
QList<CreateRoomJob::StateEvent> initialStateEvents;
@@ -477,4 +487,20 @@ QString NeoChatConnection::accountDataJsonString(const QString &type) const
return QString::fromUtf8(QJsonDocument(accountDataJson(type)).toJson());
}
LinkPreviewer *NeoChatConnection::previewerForLink(const QUrl &link)
{
if (!NeoChatConfig::showLinkPreview()) {
return nullptr;
}
auto previewer = m_linkPreviewers.value(link, nullptr);
if (previewer != nullptr) {
return previewer;
}
previewer = new LinkPreviewer(link, this);
m_linkPreviewers[link] = previewer;
return previewer;
}
#include "moc_neochatconnection.cpp"

View File

@@ -9,6 +9,10 @@
#include <QCoroTask>
#include <Quotient/connection.h>
#include "models/threepidmodel.h"
class LinkPreviewer;
class NeoChatConnection : public Quotient::Connection
{
Q_OBJECT
@@ -27,6 +31,11 @@ class NeoChatConnection : public Quotient::Connection
Q_PROPERTY(QString deviceKey READ deviceKey CONSTANT)
Q_PROPERTY(QString encryptionKey READ encryptionKey CONSTANT)
/**
* @brief The model with the account's 3PIDs.
*/
Q_PROPERTY(ThreePIdModel *threePIdModel READ threePIdModel CONSTANT)
/**
* @brief The total number of notifications for all direct chats.
*/
@@ -94,6 +103,8 @@ public:
Q_INVOKABLE void deactivateAccount(const QString &password);
ThreePIdModel *threePIdModel() const;
/**
* @brief Create new room for a group chat.
*/
@@ -147,6 +158,8 @@ public:
bool isOnline() const;
LinkPreviewer *previewerForLink(const QUrl &link);
Q_SIGNALS:
void labelChanged();
void directChatNotificationsChanged();
@@ -163,7 +176,11 @@ private:
bool m_isOnline = true;
void setIsOnline(bool isOnline);
ThreePIdModel *m_threePIdModel;
void connectSignals();
int m_badgeNotificationCount = 0;
QHash<QUrl, LinkPreviewer *> m_linkPreviewers;
};

View File

@@ -1979,4 +1979,9 @@ User *NeoChatRoom::invitingUser() const
return connection()->user(currentState().get<RoomMemberEvent>(connection()->userId())->senderId());
}
void NeoChatRoom::setRoomState(const QString &type, const QString &stateKey, const QByteArray &content)
{
setState(type, stateKey, QJsonDocument::fromJson(content).object());
}
#include "moc_neochatroom.cpp"

View File

@@ -659,6 +659,8 @@ public:
* */
Q_INVOKABLE void setCanonicalAlias(const QString &newAlias);
Q_INVOKABLE void setRoomState(const QString &type, const QString &stateKey, const QByteArray &content);
PushNotificationState::State pushNotificationState() const;
void setPushNotificationState(PushNotificationState::State state);

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