Compare commits

..

1 Commits

Author SHA1 Message Date
Allen Winter
ba7ab69a5b CMakeLists.txt - require libquotient v0.9.2 or above
Linking fails with distro provided libquotient v0.9.0 I (see below).
Links ok with self-built libquotient v0.9.2

```
/data/KDE/src/kde/network/neochat/src/models/messagecontentmodel.cpp:679:(.text+0x4f32): undefined reference to `_ZNK8Quotient16RoomMessageEvent3hasITkSt12derived_fromINS_12EventContent4BaseEENS3_15UrlBasedContentINS3_8FileInfoEEEEEbv'
/usr/bin/ld: lib/libneochat.a(messagemodel.cpp.o): in function `MessageModel::data(QModelIndex const&, int) const':
/data/KDE/src/kde/network/neochat/src/models/messagemodel.cpp:195:(.text+0x1a96): undefined reference to `_ZNK8Quotient16RoomMessageEvent3hasITkSt12derived_fromINS_12EventContent4BaseEENS3_15UrlBasedContentINS3_8FileInfoEEEEEbv'
/usr/bin/ld: /data/KDE/src/kde/network/neochat/src/models/messagemodel.cpp:195:(.text+0x1aa6): undefined reference to `_ZNK8Quotient16RoomMessageEvent3hasITkSt12derived_fromINS_12EventContent4BaseEENS3_15UrlBasedContentINS3_9ImageInfoEEEEEbv'
/usr/bin/ld: /data/KDE/src/kde/network/neochat/src/models/messagemodel.cpp:195:(.text+0x1ab6): undefined reference to `_ZNK8Quotient16RoomMessageEvent3hasITkSt12derived_fromINS_12EventContent4BaseEENS3_15PlayableContentINS3_9ImageInfoEEEEEbv'
/usr/bin/ld: /data/KDE/src/kde/network/neochat/src/models/messagemodel.cpp:196:(.text+0x1ac6): undefined reference to `_ZNK8Quotient16RoomMessageEvent3hasITkSt12derived_fromINS_12EventContent4BaseEENS3_15PlayableContentINS3_8FileInfoEEEEEbv'
clang++: error: linker command failed with exit code 1 (use -v to see
invocation)
```
2025-01-14 11:11:22 -05:00
90 changed files with 8065 additions and 11590 deletions

View File

@@ -27,7 +27,7 @@
"name": "kirigamiaddons",
"config-opts": [ "-DBUILD_TESTING=OFF" ],
"buildsystem": "cmake-ninja",
"sources": [ { "type": "git", "url": "https://invent.kde.org/libraries/kirigami-addons.git", "tag": "v1.7.0" } ]
"sources": [ { "type": "git", "url": "https://invent.kde.org/libraries/kirigami-addons.git", "commit": "34d311219e8b7209746a98b3a29b91ded05ff936" } ]
},
{
"name": "kquickimageeditor",

View File

@@ -5,13 +5,10 @@ include:
- project: sysadmin/ci-utilities
file:
- /gitlab-templates/reuse-lint.yml
- /gitlab-templates/json-validation.yml
- /gitlab-templates/xml-lint.yml
- /gitlab-templates/yaml-lint.yml
- /gitlab-templates/android-qt6.yml
- /gitlab-templates/linux-qt6.yml
- /gitlab-templates/windows-qt6.yml
# - /gitlab-templates/freebsd-qt6.yml
# - /gitlab-templates/freebsd-qt6.yml
- /gitlab-templates/flatpak.yml
- /gitlab-templates/snap-snapcraft-lxd.yml
- /gitlab-templates/craft-android-qt6-apks.yml

View File

@@ -2,43 +2,42 @@
# SPDX-License-Identifier: BSD-2-Clause
Dependencies:
- 'on': ['Linux', 'Android', 'FreeBSD', 'Windows']
'require':
'frameworks/extra-cmake-modules': '@latest-kf6'
'frameworks/kcoreaddons': '@latest-kf6'
'frameworks/kirigami': '@latest-kf6'
'frameworks/ki18n': '@latest-kf6'
'frameworks/kconfig': '@latest-kf6'
'frameworks/syntax-highlighting': '@latest-kf6'
'frameworks/kitemmodels': '@latest-kf6'
'frameworks/kquickcharts': '@latest-kf6'
'frameworks/knotifications': '@latest-kf6'
'frameworks/kcolorscheme': '@latest-kf6'
'frameworks/kiconthemes': '@latest-kf6'
'libraries/kquickimageeditor': '@latest-kf6'
'frameworks/sonnet': '@latest-kf6'
'frameworks/prison': '@latest-kf6'
'libraries/kirigami-addons': '@latest-kf6'
'third-party/libquotient': '@latest'
'third-party/qtkeychain': '@latest'
'third-party/cmark': '@latest'
'third-party/qcoro': '@latest'
- 'on': ['Windows', 'Linux', 'FreeBSD']
'require':
'frameworks/qqc2-desktop-style': '@latest-kf6'
'frameworks/kio': '@latest-kf6'
'frameworks/kwindowsystem': '@latest-kf6'
'frameworks/kstatusnotifieritem': '@latest-kf6'
'frameworks/kcrash': '@latest-kf6'
- 'on': ['Linux', 'FreeBSD']
'require':
'frameworks/kdbusaddons': '@latest-kf6'
'frameworks/purpose': '@latest-kf6'
- 'on': ['Linux', 'Android', 'FreeBSD', 'Windows']
'require':
'frameworks/extra-cmake-modules': '@latest-kf6'
'frameworks/kcoreaddons': '@latest-kf6'
'frameworks/kirigami': '@latest-kf6'
'frameworks/ki18n': '@latest-kf6'
'frameworks/kconfig': '@latest-kf6'
'frameworks/syntax-highlighting': '@latest-kf6'
'frameworks/kitemmodels': '@latest-kf6'
'frameworks/kquickcharts': '@latest-kf6'
'frameworks/knotifications': '@latest-kf6'
'frameworks/kcolorscheme': '@latest-kf6'
'libraries/kquickimageeditor': '@latest-kf6'
'frameworks/sonnet': '@latest-kf6'
'frameworks/prison': '@latest-kf6'
'libraries/kirigami-addons': '@latest-kf6'
'third-party/libquotient': '@latest'
'third-party/qtkeychain': '@latest'
'third-party/cmark': '@latest'
'third-party/qcoro': '@latest'
- 'on': ['Windows', 'Linux', 'FreeBSD']
'require':
'frameworks/qqc2-desktop-style': '@latest-kf6'
'frameworks/kio': '@latest-kf6'
'frameworks/kwindowsystem': '@latest-kf6'
'frameworks/kstatusnotifieritem': '@latest-kf6'
'frameworks/kcrash': '@latest-kf6'
- 'on': ['Linux', 'FreeBSD']
'require':
'frameworks/kdbusaddons': '@latest-kf6'
'frameworks/purpose': '@latest-kf6'
- 'on': ['Linux']
'require':
'sdk/selenium-webdriver-at-spi': '@latest-kf6'
- 'on': ['Linux']
'require':
'sdk/selenium-webdriver-at-spi': '@latest-kf6'
Options:
per-test-timeout: 90
require-passing-tests-on: ['Linux', 'Android', 'FreeBSD', 'Windows']
require-passing-tests-on: [ 'Linux', 'Android', 'FreeBSD', 'Windows' ]

View File

@@ -66,7 +66,7 @@ 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 IconThemes ColorScheme)
find_package(KF6 ${KF_MIN_VERSION} COMPONENTS Kirigami I18n Notifications Config CoreAddons Sonnet ItemModels ColorScheme)
set_package_properties(KF6 PROPERTIES
TYPE REQUIRED
PURPOSE "Basic application components"
@@ -107,7 +107,7 @@ if (NOT ANDROID AND NOT WIN32 AND NOT APPLE AND NOT HAIKU)
find_package(KF6DBusAddons ${KF_MIN_VERSION} REQUIRED)
endif()
find_package(QuotientQt6 0.9)
find_package(QuotientQt6 0.9.2)
set_package_properties(QuotientQt6 PROPERTIES
TYPE REQUIRED
DESCRIPTION "Qt wrapper around Matrix API"

View File

@@ -137,11 +137,7 @@ void TimelineMessageModelTest::pendingEvent()
model->setRoom(room);
QCOMPARE(model->rowCount(), 0);
#if Quotient_VERSION_MINOR > 9
auto txnId = room->postText("New plain message"_L1);
#else
auto txnId = room->postPlainText("New plain message"_L1);
#endif
QCOMPARE(model->rowCount(), 1);
QCOMPARE(spyInsert.count(), 1);
@@ -149,11 +145,7 @@ void TimelineMessageModelTest::pendingEvent()
QCOMPARE(model->rowCount(), 0);
QCOMPARE(spyRemove.count(), 1);
#if Quotient_VERSION_MINOR > 9
txnId = room->postText("New plain message"_L1);
#else
txnId = room->postPlainText("New plain message"_L1);
#endif
QCOMPARE(model->rowCount(), 1);
QCOMPARE(spyInsert.count(), 2);

View File

@@ -473,7 +473,6 @@
<content_attribute id="social-chat">intense</content_attribute>
</content_rating>
<releases>
<release version="24.12.2" date="2025-02-06"/>
<release version="24.12.1" date="2025-01-09"/>
<release version="24.12.0" date="2024-12-12"/>
<release version="24.08.3" date="2024-11-07"/>
@@ -650,9 +649,6 @@
<url>https://carlschwan.eu/2020/12/23/announcing-neochat-1.0-the-kde-matrix-client/</url>
</release>
</releases>
<requires>
<display_length compare="ge">360</display_length>
</requires>
<branding>
<color type="primary" scheme_preference="light">#a6e4f3</color>
<color type="primary" scheme_preference="dark">#235670</color>

View File

@@ -119,7 +119,6 @@ Comment[ta]=மேட்ரிக்ஸில் உரையாட உதவு
Comment[tr]=Matrix üzerinde sohbet edin
Comment[uk]=Спілкування у Matrix
Comment[x-test]=xxChat on Matrixxx
Comment[zh_CN]=在 Matrix 上聊天
Comment[zh_TW]=在 Matrix 上聊天
MimeType=x-scheme-handler/matrix;
Exec=neochat %u

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

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

@@ -28,8 +28,8 @@ apps:
compression: lzo
package-repositories:
- type: apt
ppa: ubuntu-toolchain-r/test
- type: apt
ppa: ubuntu-toolchain-r/test
slots:
session-dbus-interface:
@@ -66,7 +66,7 @@ parts:
- -Dcrypto=disabled
- -Dgtk_doc=false
build-packages:
- meson
- meson
- libglib2.0-dev
- libgcrypt20-dev
prime:
@@ -81,8 +81,7 @@ parts:
plugin: cmake
build-environment:
- PATH: /snap/bin:${PATH}
- PKG_CONFIG_PATH: "$CRAFT_STAGE/usr/lib/$CRAFT_ARCH_TRIPLET\
/pkgconfig:$PKG_CONFIG_PATH"
- PKG_CONFIG_PATH: $CRAFT_STAGE/usr/lib/$CRAFT_ARCH_TRIPLET/pkgconfig:$PKG_CONFIG_PATH
cmake-parameters:
- -DCMAKE_INSTALL_PREFIX=/usr
- -DCMAKE_BUILD_TYPE=Release
@@ -116,9 +115,7 @@ parts:
- -DQuotient_ENABLE_E2EE=ON
- -DBUILD_WITH_QT6=ON
override-build: |
"update-alternatives --install /usr/bin/gcc gcc\
/usr/bin/gcc-13 100 --slave /usr/bin/g++ g++ \
/usr/bin/g++-13 --slave /usr/bin/gcov gcov /usr/bin/gcov-13"
update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-13 100 --slave /usr/bin/g++ g++ /usr/bin/g++-13 --slave /usr/bin/gcov gcov /usr/bin/gcov-13
craftctl default
prime:
- -usr/include
@@ -175,3 +172,4 @@ parts:
- libcmark0.30.2
prime:
- usr/lib/*/libcmark.so*

View File

@@ -196,8 +196,6 @@ add_library(neochat STATIC
models/messagecontentfiltermodel.h
models/pinnedmessagemodel.cpp
models/pinnedmessagemodel.h
models/commonroomsmodel.cpp
models/commonroomsmodel.h
)
set_source_files_properties(qml/OsmLocationPlugin.qml PROPERTIES
@@ -423,7 +421,6 @@ target_link_libraries(neochat PUBLIC
KF6::ConfigGui
KF6::CoreAddons
KF6::SonnetCore
KF6::IconThemes
KF6::ColorScheme
KF6::ItemModels
QuotientQt6
@@ -497,7 +494,6 @@ if(ANDROID)
"network-connect"
"list-remove-user"
"org.kde.neochat"
"org.kde.neochat.tray"
"preferences-system-users"
"preferences-desktop-theme-global"
"notifications"
@@ -535,7 +531,6 @@ if(ANDROID)
"object-rotate-left"
"object-rotate-right"
"add-subtitle"
"security-high"
"security-low"
"security-low-symbolic"
"kde"
@@ -543,8 +538,6 @@ if(ANDROID)
"edit-delete"
"user-home-symbolic"
"pin-symbolic"
"kt-restore-defaults-symbolic"
"user-symbolic"
)
ecm_add_android_apk(neochat-app ANDROID_DIR ${CMAKE_SOURCE_DIR}/android)
else()

View File

@@ -168,6 +168,7 @@ void Controller::addConnection(NeoChatConnection *c)
connect(c, &NeoChatConnection::syncDone, this, [this, c]() {
m_notificationsManager.handleNotifications(c);
});
connect(c, &NeoChatConnection::showInviteNotification, &m_notificationsManager, &NotificationsManager::postInviteNotification);
c->sync();

View File

@@ -53,13 +53,9 @@ public:
LinkPreview, /**< A preview of a URL in the message. */
LinkPreviewLoad, /**< A loading dialog for a link preview. */
ChatBar, /**< A text edit for editing a message. */
ThreadRoot, /**< The root message of the thread. */
ThreadBody, /**< The other messages in the thread. */
ReplyButton, /**< A button to reply in the current thread. */
FetchButton, /**< A button to fetch more messages in the current thread. */
Verification, /**< A user verification session start message. */
Loading, /**< The component is loading. */
Separator, /**< A horizontal separator. */
Other, /**< Anything that cannot be classified as another type. */
};
Q_ENUM(Type);

View File

@@ -37,7 +37,6 @@
#include <KCrash>
#endif
#include <KIconTheme>
#include <KLocalizedContext>
#include <KLocalizedString>
@@ -102,7 +101,6 @@ Q_DECL_EXPORT
#endif
int main(int argc, char *argv[])
{
KIconTheme::initTheme();
QNetworkProxyFactory::setUseSystemConfiguration(true);
#ifdef HAVE_WEBVIEW

View File

@@ -136,11 +136,7 @@ QList<ActionsModel::Action> actions{
Action{
u"plain"_s,
[](const QString &text, NeoChatRoom *room, ChatBarCache *) {
#if Quotient_VERSION_MINOR > 9
room->postText(text.toHtmlEscaped());
#else
room->postPlainText(text.toHtmlEscaped());
#endif
return QString();
},
std::nullopt,

View File

@@ -1,74 +0,0 @@
// SPDX-FileCopyrightText: 2025 Joshua Goins <josh@redstrate.com>
// SPDX-License-Identifier: GPL-2.0-or-later
#include "commonroomsmodel.h"
#include "jobs/neochatgetcommonroomsjob.h"
#include <QGuiApplication>
using namespace Quotient;
CommonRoomsModel::CommonRoomsModel(QObject *parent)
: QAbstractListModel(parent)
{
}
NeoChatConnection *CommonRoomsModel::connection() const
{
return m_connection;
}
void CommonRoomsModel::setConnection(NeoChatConnection *connection)
{
m_connection = connection;
Q_EMIT connectionChanged();
reload();
}
QString CommonRoomsModel::userId() const
{
return m_userId;
}
void CommonRoomsModel::setUserId(const QString &userId)
{
m_userId = userId;
Q_EMIT userIdChanged();
reload();
}
QVariant CommonRoomsModel::data(const QModelIndex &index, int roleName) const
{
Q_UNUSED(index)
Q_UNUSED(roleName)
return {};
}
int CommonRoomsModel::rowCount(const QModelIndex &parent) const
{
Q_UNUSED(parent)
return m_commonRooms.size();
}
void CommonRoomsModel::reload()
{
if (!m_connection || m_userId.isEmpty()) {
return;
}
if (!m_connection->canCheckMutualRooms()) {
return;
}
m_connection->callApi<NeochatGetCommonRoomsJob>(m_userId).then([this](const auto job) {
const auto &replyData = job->jsonData();
beginResetModel();
for (const auto &roomId : replyData[u"joined"_s].toArray()) {
m_commonRooms.push_back(roomId.toString());
}
endResetModel();
Q_EMIT countChanged();
});
}
#include "moc_commonroomsmodel.cpp"

View File

@@ -1,58 +0,0 @@
// SPDX-FileCopyrightText: 2025 Joshua Goins <josh@redstrate.com>
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <QAbstractListModel>
#include <QQmlEngine>
#include "neochatconnection.h"
#include "neochatroom.h"
#include <Quotient/events/roommessageevent.h>
#include <Quotient/roommember.h>
/**
* @brief Model to show the common or mutual rooms between you and another user.
*/
class CommonRoomsModel : public QAbstractListModel
{
Q_OBJECT
QML_ELEMENT
Q_PROPERTY(NeoChatConnection *connection WRITE setConnection READ connection NOTIFY connectionChanged REQUIRED)
Q_PROPERTY(QString userId WRITE setUserId READ userId NOTIFY userIdChanged REQUIRED)
Q_PROPERTY(int count READ rowCount NOTIFY countChanged)
public:
enum Roles {
TextRole = Qt::DisplayRole,
LongitudeRole,
LatitudeRole,
AssetRole,
AuthorRole,
};
Q_ENUM(Roles)
explicit CommonRoomsModel(QObject *parent = nullptr);
[[nodiscard]] NeoChatConnection *connection() const;
void setConnection(NeoChatConnection *connection);
[[nodiscard]] QString userId() const;
void setUserId(const QString &userId);
[[nodiscard]] QVariant data(const QModelIndex &index, int roleName) const override;
[[nodiscard]] Q_INVOKABLE int rowCount(const QModelIndex &parent = {}) const override;
Q_SIGNALS:
void connectionChanged();
void userIdChanged();
void countChanged();
private:
void reload();
QPointer<NeoChatConnection> m_connection;
QString m_userId;
QList<QString> m_commonRooms;
};

View File

@@ -340,17 +340,6 @@ QVariant MessageContentModel::data(const QModelIndex &index, int role) const
if (role == ReplyContentModelRole) {
return QVariant::fromValue<MessageContentModel *>(m_replyModel);
}
if (role == ThreadRootRole) {
auto roomMessageEvent = eventCast<const RoomMessageEvent>(event.first);
#if Quotient_VERSION_MINOR > 9 || (Quotient_VERSION_MINOR == 9 && Quotient_VERSION_PATCH > 1)
if (roomMessageEvent && (roomMessageEvent->isThreaded() || m_room->threads().contains(roomMessageEvent->id()))) {
#else
if (roomMessageEvent && roomMessageEvent->isThreaded()) {
#endif
return roomMessageEvent->threadRootEventId();
}
return {};
}
if (role == LinkPreviewerRole) {
if (component.type == MessageComponentType::LinkPreview) {
return QVariant::fromValue<LinkPreviewer *>(
@@ -377,32 +366,27 @@ int MessageContentModel::rowCount(const QModelIndex &parent) const
QHash<int, QByteArray> MessageContentModel::roleNames() const
{
return roleNamesStatic();
}
QHash<int, QByteArray> MessageContentModel::roleNamesStatic()
{
QHash<int, QByteArray> roles;
roles[MessageContentModel::DisplayRole] = "display";
roles[MessageContentModel::ComponentTypeRole] = "componentType";
roles[MessageContentModel::ComponentAttributesRole] = "componentAttributes";
roles[MessageContentModel::EventIdRole] = "eventId";
roles[MessageContentModel::TimeRole] = "time";
roles[MessageContentModel::TimeStringRole] = "timeString";
roles[MessageContentModel::AuthorRole] = "author";
roles[MessageContentModel::MediaInfoRole] = "mediaInfo";
roles[MessageContentModel::FileTransferInfoRole] = "fileTransferInfo";
roles[MessageContentModel::ItineraryModelRole] = "itineraryModel";
roles[MessageContentModel::LatitudeRole] = "latitude";
roles[MessageContentModel::LongitudeRole] = "longitude";
roles[MessageContentModel::AssetRole] = "asset";
roles[MessageContentModel::PollHandlerRole] = "pollHandler";
roles[MessageContentModel::ReplyEventIdRole] = "replyEventId";
roles[MessageContentModel::ReplyAuthorRole] = "replyAuthor";
roles[MessageContentModel::ReplyContentModelRole] = "replyContentModel";
roles[MessageContentModel::ThreadRootRole] = "threadRoot";
roles[MessageContentModel::LinkPreviewerRole] = "linkPreviewer";
roles[MessageContentModel::ChatBarCacheRole] = "chatBarCache";
QHash<int, QByteArray> roles = QAbstractItemModel::roleNames();
roles[DisplayRole] = "display";
roles[ComponentTypeRole] = "componentType";
roles[ComponentAttributesRole] = "componentAttributes";
roles[EventIdRole] = "eventId";
roles[TimeRole] = "time";
roles[TimeStringRole] = "timeString";
roles[AuthorRole] = "author";
roles[MediaInfoRole] = "mediaInfo";
roles[FileTransferInfoRole] = "fileTransferInfo";
roles[ItineraryModelRole] = "itineraryModel";
roles[LatitudeRole] = "latitude";
roles[LongitudeRole] = "longitude";
roles[AssetRole] = "asset";
roles[PollHandlerRole] = "pollHandler";
roles[ReplyEventIdRole] = "replyEventId";
roles[ReplyAuthorRole] = "replyAuthor";
roles[ReplyContentModelRole] = "replyContentModel";
roles[ThreadRootRole] = "threadRoot";
roles[LinkPreviewerRole] = "linkPreviewer";
roles[ChatBarCacheRole] = "chatBarCache";
return roles;
}
@@ -480,16 +464,6 @@ QList<MessageComponent> MessageContentModel::messageContentComponents(bool isEdi
newComponents = addLinkPreviews(newComponents);
}
#if Quotient_VERSION_MINOR > 9 || (Quotient_VERSION_MINOR == 9 && Quotient_VERSION_PATCH > 1)
if (roomMessageEvent && (roomMessageEvent->isThreaded() || m_room->threads().contains(roomMessageEvent->id()))
&& roomMessageEvent->id() == roomMessageEvent->threadRootEventId()) {
#else
if (isThreading && roomMessageEvent && roomMessageEvent->isThreaded() && roomMessageEvent->id() == roomMessageEvent->threadRootEventId()) {
#endif
newComponents += MessageComponent{MessageComponentType::Separator, {}, {}};
newComponents += MessageComponent{MessageComponentType::ThreadBody, u"Thread Body"_s, {}};
}
// If the event is already threaded the ThreadModel will handle displaying a chat bar.
#if Quotient_VERSION_MINOR > 9 || (Quotient_VERSION_MINOR == 9 && Quotient_VERSION_PATCH > 1)
if (isThreading && roomMessageEvent && !(roomMessageEvent->isThreaded() || m_room->threads().contains(roomMessageEvent->id()))) {

View File

@@ -91,8 +91,6 @@ public:
*/
[[nodiscard]] QHash<int, QByteArray> roleNames() const override;
static QHash<int, QByteArray> roleNamesStatic();
/**
* @brief Close the link preview at the given index.
*

View File

@@ -7,7 +7,7 @@
#include <Quotient/events/roommessageevent.h>
#include <Quotient/events/stickerevent.h>
#if Quotient_VERSION_MINOR > 9 || (Quotient_VERSION_MINOR == 9 && Quotient_VERSION_PATCH > 1)
#if Quotient_VERSION_MINOR > 9
#include <Quotient/thread.h>
#endif
@@ -120,10 +120,6 @@ QVariant MessageModel::data(const QModelIndex &idx, int role) const
}
if (role == ContentModelRole) {
auto roomMessageEvent = eventCast<const RoomMessageEvent>(&event.value().get());
if (roomMessageEvent && roomMessageEvent->isThreaded()) {
return QVariant::fromValue<MessageContentModel *>(m_room->contentModelForEvent(roomMessageEvent->threadRootEventId()));
}
return QVariant::fromValue<MessageContentModel *>(m_room->contentModelForEvent(&event->get()));
}
@@ -173,7 +169,7 @@ QVariant MessageModel::data(const QModelIndex &idx, int role) const
}
auto roomMessageEvent = eventCast<const RoomMessageEvent>(&event.value().get());
#if Quotient_VERSION_MINOR > 9 || (Quotient_VERSION_MINOR == 9 && Quotient_VERSION_PATCH > 1)
#if Quotient_VERSION_MINOR > 9
if (roomMessageEvent && (roomMessageEvent->isThreaded() || m_room->threads().contains(event.value().get().id()))) {
const auto &thread = m_room->threads().value(roomMessageEvent->isThreaded() ? roomMessageEvent->threadRootEventId() : event.value().get().id());
if (thread.latestEventId != event.value().get().id()) {

View File

@@ -45,7 +45,6 @@ class MessageModel : public QAbstractListModel
{
Q_OBJECT
QML_ELEMENT
QML_UNCREATABLE("")
/**
* @brief The current room that the model is getting its messages from.

View File

@@ -11,18 +11,18 @@
#include "chatbarcache.h"
#include "eventhandler.h"
#include "messagecomponenttype.h"
#include "messagecontentmodel.h"
#include "neochatroom.h"
ThreadModel::ThreadModel(const QString &threadRootId, NeoChatRoom *room)
: QConcatenateTablesProxyModel(room)
, m_threadRootId(threadRootId)
, m_threadFetchModel(new ThreadFetchModel(this))
, m_threadChatBarModel(new ThreadChatBarModel(this, room))
{
Q_ASSERT(!m_threadRootId.isEmpty());
Q_ASSERT(room);
m_threadRootContentModel = std::unique_ptr<MessageContentModel>(new MessageContentModel(room, threadRootId));
#if Quotient_VERSION_MINOR > 9 || (Quotient_VERSION_MINOR == 9 && Quotient_VERSION_PATCH > 0)
connect(room, &Quotient::Room::pendingEventAdded, this, [this](const Quotient::RoomEvent *event) {
#else
@@ -49,7 +49,7 @@ ThreadModel::ThreadModel(const QString &threadRootId, NeoChatRoom *room)
// If the thread was created by the local user fetchMore() won't find the current
// pending event.
checkPending();
fetchMoreEvents(3);
fetchMore({});
addModels();
}
@@ -73,24 +73,29 @@ QString ThreadModel::threadRootId() const
return m_threadRootId;
}
QHash<int, QByteArray> ThreadModel::roleNames() const
MessageContentModel *ThreadModel::threadRootContentModel() const
{
return MessageContentModel::roleNamesStatic();
return m_threadRootContentModel.get();
}
bool ThreadModel::moreEventsAvailable(const QModelIndex &parent) const
QHash<int, QByteArray> ThreadModel::roleNames() const
{
return m_threadRootContentModel->roleNames();
}
bool ThreadModel::canFetchMore(const QModelIndex &parent) const
{
Q_UNUSED(parent);
return !m_currentJob && m_nextBatch.has_value();
}
void ThreadModel::fetchMoreEvents(int max)
void ThreadModel::fetchMore(const QModelIndex &parent)
{
Q_UNUSED(parent);
if (!m_currentJob && m_nextBatch.has_value()) {
const auto room = dynamic_cast<NeoChatRoom *>(QObject::parent());
const auto connection = room->connection();
m_currentJob = connection->callApi<Quotient::GetRelatingEventsWithRelTypeJob>(room->id(), m_threadRootId, u"m.thread"_s, *m_nextBatch, QString(), max);
Q_EMIT moreEventsAvailableChanged();
m_currentJob = connection->callApi<Quotient::GetRelatingEventsWithRelTypeJob>(room->id(), m_threadRootId, u"m.thread"_s, *m_nextBatch, QString(), 5);
connect(m_currentJob, &Quotient::BaseJob::success, this, [this]() {
auto newEvents = m_currentJob->chunk();
for (auto &event : newEvents) {
@@ -110,7 +115,6 @@ void ThreadModel::fetchMoreEvents(int max)
}
m_currentJob.clear();
Q_EMIT moreEventsAvailableChanged();
});
}
}
@@ -130,11 +134,11 @@ void ThreadModel::addModels()
clearModels();
}
addSourceModel(m_threadRootContentModel.get());
const auto room = dynamic_cast<NeoChatRoom *>(QObject::parent());
if (room == nullptr) {
return;
}
addSourceModel(m_threadFetchModel);
for (auto it = m_events.crbegin(); it != m_events.crend(); ++it) {
const auto contentModel = room->contentModelForEvent(*it);
if (contentModel != nullptr) {
@@ -149,11 +153,12 @@ void ThreadModel::addModels()
void ThreadModel::clearModels()
{
removeSourceModel(m_threadRootContentModel.get());
const auto room = dynamic_cast<NeoChatRoom *>(QObject::parent());
if (room == nullptr) {
return;
}
removeSourceModel(m_threadFetchModel);
for (const auto &model : m_events) {
const auto contentModel = room->contentModelForEvent(model);
if (sourceModels().contains(contentModel)) {
@@ -163,76 +168,6 @@ void ThreadModel::clearModels()
removeSourceModel(m_threadChatBarModel);
}
void ThreadModel::closeLinkPreview(int row)
{
if (row < 0 || row >= rowCount()) {
return;
}
const auto index = this->index(row, 0);
if (!index.isValid()) {
return;
}
const auto sourceIndex = mapToSource(index);
const auto sourceModel = sourceIndex.model();
if (sourceModel == nullptr) {
return;
}
// This is a bit silly but we can only get a const reference to the model from the
// index so we need to search the source models.
for (const auto &model : sourceModels()) {
if (model == sourceModel) {
const auto sourceContentModel = dynamic_cast<MessageContentModel *>(model);
if (sourceContentModel == nullptr) {
return;
}
sourceContentModel->closeLinkPreview(sourceIndex.row());
}
}
}
ThreadFetchModel::ThreadFetchModel(QObject *parent)
: QAbstractListModel(parent)
{
const auto threadModel = dynamic_cast<ThreadModel *>(parent);
Q_ASSERT(threadModel != nullptr);
connect(threadModel, &ThreadModel::moreEventsAvailableChanged, this, [this]() {
beginResetModel();
endResetModel();
});
}
QVariant ThreadFetchModel::data(const QModelIndex &idx, int role) const
{
if (idx.row() < 0 || idx.row() > 1) {
return {};
}
if (role == ComponentTypeRole) {
return MessageComponentType::FetchButton;
}
return {};
}
int ThreadFetchModel::rowCount(const QModelIndex &parent) const
{
Q_UNUSED(parent)
const auto threadModel = dynamic_cast<ThreadModel *>(this->parent());
if (threadModel == nullptr) {
qWarning() << "ThreadFetchModel created with incorrect parent, a ThreadModel must be set as the parent on creation.";
return {};
}
return threadModel->moreEventsAvailable({}) ? 1 : 0;
}
QHash<int, QByteArray> ThreadFetchModel::roleNames() const
{
return {
{ComponentTypeRole, "componentType"},
};
}
ThreadChatBarModel::ThreadChatBarModel(QObject *parent, NeoChatRoom *room)
: QAbstractListModel(parent)
, m_room(room)

View File

@@ -21,52 +21,6 @@
class NeoChatRoom;
class ReactionModel;
/**
* @class ThreadFetchModel
*
* A model to provide a fetch more historical messages button in a thread.
*/
class ThreadFetchModel : public QAbstractListModel
{
Q_OBJECT
public:
/**
* @brief Defines the model roles.
*
* The role values need to match MessageContentModel not to blow up.
*
* @sa MessageContentModel
*/
enum Roles {
ComponentTypeRole = MessageContentModel::ComponentTypeRole, /**< The type of component to visualise the message. */
};
Q_ENUM(Roles)
explicit ThreadFetchModel(QObject *parent);
/**
* @brief Get the given role value at the given index.
*
* @sa QAbstractItemModel::data
*/
[[nodiscard]] QVariant data(const QModelIndex &idx, int role = Qt::DisplayRole) const override;
/**
* @brief 1 or 0, depending on whether there are more messages to download.
*
* @sa QAbstractItemModel::rowCount
*/
[[nodiscard]] int rowCount(const QModelIndex &parent = QModelIndex()) const override;
/**
* @brief Returns a map with ComponentTypeRole it's the only one.
*
* @sa Roles, QAbstractItemModel::roleNames()
*/
[[nodiscard]] QHash<int, QByteArray> roleNames() const override;
};
/**
* @class ThreadChatBarModel
*
@@ -137,6 +91,11 @@ public:
QString threadRootId() const;
/**
* @brief The content model for the thread root event.
*/
MessageContentModel *threadRootContentModel() const;
/**
* @brief Returns a mapping from Role enum values to role names.
*
@@ -145,31 +104,25 @@ public:
[[nodiscard]] QHash<int, QByteArray> roleNames() const override;
/**
* @brief Whether there are more events for the model to fetch.
*/
bool moreEventsAvailable(const QModelIndex &parent) const;
/**
* @brief Fetches the next batch of events if any is available.
*/
Q_INVOKABLE void fetchMoreEvents(int max = 5);
/**
* @brief Close the link preview at the given index.
* @brief Whether there is more data available for the model to fetch.
*
* If the given index is not a link preview component, nothing happens.
* @sa QAbstractItemModel::canFetchMore()
*/
Q_INVOKABLE void closeLinkPreview(int row);
bool canFetchMore(const QModelIndex &parent) const override;
Q_SIGNALS:
void moreEventsAvailableChanged();
/**
* @brief Fetches the next batch of model data if any is available.
*
* @sa QAbstractItemModel::fetchMore()
*/
void fetchMore(const QModelIndex &parent) override;
private:
QString m_threadRootId;
QPointer<MessageContentModel> m_threadRootContentModel;
std::unique_ptr<MessageContentModel> m_threadRootContentModel;
std::deque<QString> m_events;
ThreadFetchModel *m_threadFetchModel;
ThreadChatBarModel *m_threadChatBarModel;
QMap<QString, QSharedPointer<ReactionModel>> m_reactionModels;

View File

@@ -27,20 +27,24 @@ void TimelineMessageModel::connectNewRoom()
}
connect(m_room, &Room::aboutToAddNewMessages, this, [this](RoomEventsRange events) {
for (auto &&event : events) {
Q_EMIT newEventAdded(event.get());
}
m_initialized = true;
beginInsertRows({}, timelineServerIndex(), timelineServerIndex() + int(events.size()) - 1);
});
connect(m_room, &Room::aboutToAddHistoricalMessages, this, [this](RoomEventsRange events) {
for (auto &event : events) {
Q_EMIT newEventAdded(event.get());
}
if (rowCount() > 0) {
rowBelowInserted = rowCount() - 1; // See #312
}
m_initialized = true;
beginInsertRows({}, rowCount(), rowCount() + int(events.size()) - 1);
});
connect(m_room, &Room::addedMessages, this, [this](int lowest, int biggest) {
if (m_initialized) {
for (int i = lowest; i == biggest; ++i) {
const auto event = m_room->findInTimeline(i)->event();
Q_EMIT newEventAdded(event);
}
endInsertRows();
}
if (!m_lastReadEventIndex.isValid()) {

View File

@@ -39,7 +39,7 @@ struct WebShortcutModelPrivate;
* }
* QQC2.MenuSeparator {}
* QQC2.MenuItem {
* text: i18n("Configure Web Shortcuts")
* text: i18n("Configure Web Shortcuts...")
* icon.name: "configure"
* onTriggered: webshortcutmodel.configureWebShortcuts()
* }

View File

@@ -107,6 +107,10 @@ void NeoChatConnection::connectSignals()
Q_EMIT homeHaveHighlightNotificationsChanged();
});
});
connect(this, &NeoChatConnection::invitedRoom, this, [this](Quotient::Room *room) {
auto r = dynamic_cast<NeoChatRoom *>(room);
connect(r, &NeoChatRoom::showInviteNotification, this, &NeoChatConnection::showInviteNotification);
});
connect(this, &NeoChatConnection::leftRoom, this, [this](Room *room, Room *prev) {
Q_UNUSED(room)
if (prev && prev->isDirectChat()) {

View File

@@ -207,6 +207,11 @@ Q_SIGNALS:
*/
void errorOccured(const QString &error);
/**
* @brief Request a notification be shown for an invite to this room.
*/
void showInviteNotification(NeoChatRoom *room);
private:
bool m_isOnline = true;
void setIsOnline(bool isOnline);

View File

@@ -127,6 +127,9 @@ NeoChatRoom::NeoChatRoom(Connection *connection, QString roomId, JoinState joinS
updatePushNotificationState(u"m.push_rules"_s);
Q_EMIT canEncryptRoomChanged();
if (this->joinState() == JoinState::Invite) {
Q_EMIT showInviteNotification(this);
}
},
Qt::SingleShotConnection);
connect(this, &Room::changed, this, [this] {
@@ -1726,6 +1729,10 @@ void NeoChatRoom::setRoomState(const QString &type, const QString &stateKey, con
NeochatRoomMember *NeoChatRoom::qmlSafeMember(const QString &memberId)
{
if (!isMember(memberId)) {
return nullptr;
}
if (!m_memberObjects.contains(memberId)) {
return m_memberObjects.emplace(memberId, std::make_unique<NeochatRoomMember>(this, memberId)).first->second.get();
}

View File

@@ -588,15 +588,6 @@ public:
*/
Quotient::FileTransferInfo cachedFileTransferInfo(const Quotient::RoomEvent *event) const;
/**
* @brief Return a NeochatRoomMember object for the given user ID.
*
* @warning Because we can't guarantee that a member state event is downloaded
* before a message they sent arrives this will create the object unconditionally
* assuming that the state event will turn up later. It is therefor the
* responsibility of the caller to ensure that they only ask for objects
* for real senders.
*/
NeochatRoomMember *qmlSafeMember(const QString &memberId);
/**
@@ -634,7 +625,7 @@ public:
* A model is created is one doesn't exist. Will return nullptr if threadRootId
* is empty.
*/
Q_INVOKABLE ThreadModel *modelForThread(const QString &threadRootId);
ThreadModel *modelForThread(const QString &threadRootId);
private:
bool m_visible = false;
@@ -706,6 +697,14 @@ Q_SIGNALS:
*/
void showMessage(MessageType::Type messageType, const QString &message);
/**
* @brief Request a notification be shown for an invite to this room.
*
* @note This may later be blocked if there are any rules on where invites can
* come from, but this is not NeoChatRoom's responsibility.
*/
void showInviteNotification(NeoChatRoom *room);
public Q_SLOTS:
/**
* @brief Upload a file to the matrix server and post the file to the room.

View File

@@ -127,8 +127,9 @@ void NotificationsManager::processNotificationJob(QPointer<NeoChatConnection> co
}
auto sender = room->member(notification["event"_L1]["sender"_L1].toString());
// Don't display notifications for events in invited rooms
// This should prevent empty notifications from appearing when they shouldn't
if (room->joinState() == JoinState::Invite) {
postInviteNotification(qobject_cast<NeoChatRoom *>(room));
continue;
}
@@ -243,7 +244,7 @@ void NotificationsManager::postNotification(NeoChatRoom *room,
if (canReply) {
std::unique_ptr<KNotificationReplyAction> replyAction(new KNotificationReplyAction(i18n("Reply")));
replyAction->setPlaceholderText(i18n("Reply"));
replyAction->setPlaceholderText(i18n("Reply..."));
connect(replyAction.get(), &KNotificationReplyAction::replied, this, [room, replyEventId](const QString &text) {
TextHandler textHandler;
textHandler.setData(text);

View File

@@ -22,7 +22,7 @@ Labs.MenuBar {
Labs.MenuItem {
enabled: pageStack.layers.currentItem.title !== i18n("Configure NeoChat…")
text: i18nc("menu", "Configure NeoChat")
text: i18nc("menu", "Configure NeoChat...")
shortcut: StandardKey.Preferences
onTriggered: NeoChatSettingsView.open()

View File

@@ -63,7 +63,7 @@ Kirigami.Page {
when: root.session.state === KeyVerificationSession.READY
PropertyChanges {
target: stateLoader
sourceComponent: chooseVerificationComponent
sourceComponent: emojiVerificationComponent
}
},
State {
@@ -152,22 +152,15 @@ Kirigami.Page {
}
Component {
id: chooseVerificationComponent
Item {
ColumnLayout {
anchors.centerIn: parent
spacing: Kirigami.Units.largeSpacing
QQC2.Label {
text: i18nc("@info", "Choose a verification method to continue")
}
QQC2.Button {
id: emojiVerification
text: i18nc("@action:button", "Emoji Verification")
icon.name: "smiley"
onClicked: root.session.sendStartSas()
Layout.alignment: Qt.AlignHCenter
}
id: emojiVerificationComponent
Delegates.RoundedItemDelegate {
id: emojiVerification
text: i18n("Emoji Verification")
contentItem: Delegates.SubtitleContentItem {
subtitle: i18n("Compare a set of emoji on both devices")
itemDelegate: emojiVerification
}
onClicked: root.session.sendStartSas()
}
}
}

View File

@@ -325,13 +325,11 @@ Kirigami.ApplicationWindow {
})
}
function showUserDetail(user, room) {
const dialog = Qt.createComponent("org.kde.neochat", "UserDetailDialog").createObject(root, {
Qt.createComponent("org.kde.neochat", "UserDetailDialog").createObject(root.QQC2.ApplicationWindow.window, {
room: room,
user: user,
connection: root.connection,
});
dialog.parent = QmlUtils.focusedWindowItem(); // Kirigami Dialogs overwrite the parent, so we need to set it again
dialog.open();
connection: root.connection
}).open();
}
function load() {

View File

@@ -124,9 +124,9 @@ DelegateContextMenu {
}
Kirigami.Action {
text: i18n("Configure Web Shortcuts")
text: i18n("Configure Web Shortcuts...")
icon.name: "configure"
visible: !Controller.isFlatpak && webshortcutModel.enabled
onTriggered: webshortcutModel.configureWebShortcuts()
onTriggered: webshortcutmodel.configureWebShortcuts()
}
}

View File

@@ -87,7 +87,6 @@ Kirigami.OverlayDrawer {
Kirigami.Separator {
Layout.fillHeight: true
visible: root.modal
}
ColumnLayout {

View File

@@ -56,8 +56,6 @@ Kirigami.Dialog {
ColumnLayout {
Layout.fillWidth: true
spacing: 0
Kirigami.Heading {
level: 1
Layout.fillWidth: true
@@ -73,19 +71,6 @@ Kirigami.Dialog {
textFormat: TextEdit.PlainText
text: root.user.id
}
QQC2.Label {
property CommonRoomsModel model: CommonRoomsModel {
connection: root.connection
userId: root.user.id
}
text: i18ncp("@info", "One mutual room", "%1 mutual rooms", model.count)
color: Kirigami.Theme.disabledTextColor
visible: model.count > 0
Layout.topMargin: Kirigami.Units.smallSpacing
}
}
QQC2.AbstractButton {
Layout.minimumHeight: avatar.height * 0.75

View File

@@ -156,35 +156,35 @@ FormCard.FormCardPage {
FormCard.FormCard {
FormCard.FormTextDelegate {
visible: root.connection !== undefined && root.connection.canChangePassword === false
text: i18nc("@info", "Your server doesn't support changing your password")
text: i18n("Your server doesn't support changing your password")
}
FormCard.FormDelegateSeparator {
visible: root.connection !== undefined && root.connection.canChangePassword === false
}
FormCard.FormTextFieldDelegate {
id: currentPassword
label: i18nc("@label:textbox", "Current Password:")
label: i18n("Current Password:")
enabled: root.connection !== undefined && root.connection.canChangePassword !== false
echoMode: TextInput.Password
}
FormCard.FormDelegateSeparator {}
FormCard.FormTextFieldDelegate {
id: newPassword
label: i18nc("@label:textbox", "New Password:")
label: i18n("New Password:")
enabled: root.connection !== undefined && root.connection.canChangePassword !== false
echoMode: TextInput.Password
}
FormCard.FormDelegateSeparator {}
FormCard.FormTextFieldDelegate {
id: confirmPassword
label: i18nc("@label:textbox", "Confirm new Password:")
label: i18n("Confirm new Password:")
enabled: root.connection !== undefined && root.connection.canChangePassword !== false
echoMode: TextInput.Password
onTextChanged: if (newPassword.text !== confirmPassword.text && confirmPassword.text.length > 0) {
confirmPassword.status = Kirigami.MessageType.Error;
confirmPassword.statusMessage = i18nc("@info", "Passwords don't match");
confirmPassword.status = FormCard.AbstractFormDelegate.Status.Error;
confirmPassword.statusMessage = i18n("Passwords don't match");
} else {
confirmPassword.status = Kirigami.MessageType.Information;
confirmPassword.status = FormCard.AbstractFormDelegate.Status.Default;
confirmPassword.statusMessage = '';
}
}
@@ -192,10 +192,12 @@ FormCard.FormCardPage {
FormCard.FormButtonDelegate {
text: i18n("Save")
icon.name: "document-save-symbolic"
enabled: currentPassword.text.length > 0 && newPassword.text.length > 0 && confirmPassword.text.length > 0 && newPassword.text === confirmPassword.text
enabled: currentPassword.text.length > 0 && newPassword.text.length > 0 && confirmPassword.text.length > 0
onClicked: {
if (newPassword.text === confirmPassword.text) {
root.connection.changePassword(currentPassword.text, newPassword.text);
} else {
showPassiveNotification(i18n("Passwords do not match"));
}
}
}
@@ -268,14 +270,11 @@ FormCard.FormCardPage {
target: root.connection
function onPasswordStatus(status) {
if (status === NeoChatConnection.Success) {
confirmPassword.status = Kirigami.MessageType.Positive
confirmPassword.statusMessage = i18nc("@info", "Password changed successfully");
showPassiveNotification(i18n("Password changed successfully"));
} else if (status === NeoChatConnection.Wrong) {
confirmPassword.status = Kirigami.MessageType.Error
confirmPassword.statusMessage = i18nc("@info", "Invalid password");
showPassiveNotification(i18n("Wrong password entered"));
} else {
confirmPassword.status = Kirigami.MessageType.Error
confirmPassword.statusMessage = i18nc("@info", "Unknown problem while trying to change password");
showPassiveNotification(i18n("Unknown problem while trying to change password"));
}
}
}

View File

@@ -43,5 +43,4 @@ ecm_add_qml_module(settings GENERATE_PLUGIN_SOURCE
ImportKeysDialog.qml
ExportKeysDialog.qml
RoomSortParameterDialog.qml
RoomProfile.qml
)

View File

@@ -1,61 +0,0 @@
// SPDX-FileCopyrightText: 2025 Joshua Goins <josh@redstrate.com>
// SPDX-License-Identifier: GPL-2.0-or-later
import QtQuick
import QtQuick.Controls as QQC2
import QtQuick.Layouts
import org.kde.kirigami as Kirigami
import org.kde.kirigamiaddons.formcard as FormCard
import org.kde.kitemmodels
import org.kde.neochat
FormCard.FormCardPage {
id: root
property NeoChatRoom room
title: i18nc('@title:window', 'Profile')
function setDisplayName(): void {
root.room.connection.localUser.rename(displayNameDelegate.text, root.room);
}
FormCard.FormCard {
Layout.topMargin: Kirigami.Units.largeSpacing * 4
FormCard.FormTextDelegate {
id: noticeLabel
text: i18nc("@info", "Customize your profile only for this room.")
}
FormCard.FormDelegateSeparator {
above: noticeLabel
below: displayNameDelegate
}
FormCard.FormTextFieldDelegate {
id: displayNameDelegate
label: i18nc("@label:textbox", "Display Name")
text: root.room.member(root.room.connection.localUserId).displayName
}
}
FormCard.FormCard {
Layout.topMargin: Kirigami.Units.largeSpacing * 2
FormCard.FormButtonDelegate {
icon.name: "document-save-symbolic"
text: i18nc("@action:button Save profile", "Save")
onClicked: root.setDisplayName()
}
FormCard.FormDelegateSeparator {}
FormCard.FormButtonDelegate {
icon.name: "kt-restore-defaults-symbolic"
text: i18nc("@action:button", "Reset to Default")
onClicked: {
displayNameDelegate.text = root.room.connection.localUser.displayName;
root.setDisplayName();
}
}
}
}

View File

@@ -78,17 +78,6 @@ KirigamiSettings.ConfigurationView {
room: root._room
};
}
},
KirigamiSettings.ConfigurationModule {
moduleId: "profile"
text: i18nc("@title", "Profile")
icon.name: "user-symbolic"
page: () => Qt.createComponent("org.kde.neochat.settings", "RoomProfile")
initialProperties: () => {
return {
room: root._room
};
}
}
]
}

View File

@@ -1,264 +0,0 @@
// 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 Qt.labs.qmlmodels
import org.kde.kirigami as Kirigami
import org.kde.neochat
/**
* @brief Select a message component based on a MessageComponentType.
*/
DelegateChooser {
id: root
/**
* @brief The NeoChatRoom the delegate is being displayed in.
*/
required property NeoChatRoom room
/**
* @brief The index of the delegate in the model.
*/
required property var index
/**
* @brief The timeline ListView this component is being used in.
*/
required property ListView timeline
/**
* @brief The maximum width that the bubble's content can be.
*/
property real maxContentWidth: -1
/**
* @brief The reply has been clicked.
*/
signal replyClicked(string eventID)
/**
* @brief The user selected text has changed.
*/
signal selectedTextChanged(string selectedText)
/**
* @brief The user hovered link has changed.
*/
signal hoveredLinkChanged(string hoveredLink)
/**
* @brief Request a context menu be show for the message.
*/
signal showMessageMenu
signal removeLinkPreview(int index)
/**
* @brief Request more events in the thread be loaded.
*/
signal fetchMoreEvents()
role: "componentType"
DelegateChoice {
roleValue: MessageComponentType.Author
delegate: AuthorComponent {
maxContentWidth: root.maxContentWidth
}
}
DelegateChoice {
roleValue: MessageComponentType.Text
delegate: TextComponent {
maxContentWidth: root.maxContentWidth
onSelectedTextChanged: root.selectedTextChanged(selectedText)
onHoveredLinkChanged: root.hoveredLinkChanged(hoveredLink)
onShowMessageMenu: root.showMessageMenu()
}
}
DelegateChoice {
roleValue: MessageComponentType.Image
delegate: ImageComponent {
room: root.room
index: root.index
timeline: root.timeline
maxContentWidth: root.maxContentWidth
}
}
DelegateChoice {
roleValue: MessageComponentType.Video
delegate: VideoComponent {
room: root.room
index: root.index
timeline: root.timeline
maxContentWidth: root.maxContentWidth
}
}
DelegateChoice {
roleValue: MessageComponentType.Code
delegate: CodeComponent {
maxContentWidth: root.maxContentWidth
onSelectedTextChanged: selectedText => {
root.selectedTextChanged(selectedText);
}
onShowMessageMenu: root.showMessageMenu()
}
}
DelegateChoice {
roleValue: MessageComponentType.Quote
delegate: QuoteComponent {
maxContentWidth: root.maxContentWidth
onSelectedTextChanged: selectedText => {
root.selectedTextChanged(selectedText);
}
onShowMessageMenu: root.showMessageMenu()
}
}
DelegateChoice {
roleValue: MessageComponentType.Audio
delegate: AudioComponent {
room: root.room
maxContentWidth: root.maxContentWidth
}
}
DelegateChoice {
roleValue: MessageComponentType.File
delegate: FileComponent {
room: root.room
maxContentWidth: root.maxContentWidth
}
}
DelegateChoice {
roleValue: MessageComponentType.Itinerary
delegate: ItineraryComponent {
maxContentWidth: root.maxContentWidth
}
}
DelegateChoice {
roleValue: MessageComponentType.Pdf
delegate: PdfPreviewComponent {
maxContentWidth: root.maxContentWidth
}
}
DelegateChoice {
roleValue: MessageComponentType.Poll
delegate: PollComponent {
room: root.room
maxContentWidth: root.maxContentWidth
}
}
DelegateChoice {
roleValue: MessageComponentType.Location
delegate: LocationComponent {
maxContentWidth: root.maxContentWidth
}
}
DelegateChoice {
roleValue: MessageComponentType.LiveLocation
delegate: LiveLocationComponent {
room: root.room
maxContentWidth: root.maxContentWidth
}
}
DelegateChoice {
roleValue: MessageComponentType.Encrypted
delegate: EncryptedComponent {
maxContentWidth: root.maxContentWidth
}
}
DelegateChoice {
roleValue: MessageComponentType.Reply
delegate: ReplyComponent {
maxContentWidth: root.maxContentWidth
onReplyClicked: eventId => {
root.replyClicked(eventId);
}
}
}
DelegateChoice {
roleValue: MessageComponentType.LinkPreview
delegate: LinkPreviewComponent {
maxContentWidth: root.maxContentWidth
onRemove: index => root.removeLinkPreview(index)
}
}
DelegateChoice {
roleValue: MessageComponentType.LinkPreviewLoad
delegate: LinkPreviewLoadComponent {
type: LinkPreviewLoadComponent.LinkPreview
maxContentWidth: root.maxContentWidth
onRemove: index => root.removeLinkPreview(index)
}
}
DelegateChoice {
roleValue: MessageComponentType.ChatBar
delegate: ChatBarComponent {
room: root.room
maxContentWidth: root.maxContentWidth
}
}
DelegateChoice {
roleValue: MessageComponentType.ReplyButton
delegate: ReplyButtonComponent {
room: root.room
maxContentWidth: root.maxContentWidth
}
}
DelegateChoice {
roleValue: MessageComponentType.FetchButton
delegate: FetchButtonComponent {
maxContentWidth: root.maxContentWidth
onFetchMoreEvents: root.fetchMoreEvents()
}
}
DelegateChoice {
roleValue: MessageComponentType.Verification
delegate: MimeComponent {
mimeIconSource: "security-high"
label: i18n("%1 started a user verification", model.author.htmlSafeDisplayName)
}
}
DelegateChoice {
roleValue: MessageComponentType.Loading
delegate: LoadComponent {
maxContentWidth: root.maxContentWidth
}
}
DelegateChoice {
roleValue: MessageComponentType.Separator
delegate: Kirigami.Separator {
Layout.fillWidth: true
Layout.maximumWidth: root.maxContentWidth
}
}
DelegateChoice {
roleValue: MessageComponentType.Other
delegate: Item {}
}
}

View File

@@ -19,7 +19,6 @@ ecm_add_qml_module(timeline GENERATE_PLUGIN_SOURCE
AvatarFlow.qml
ReactionDelegate.qml
SectionDelegate.qml
BaseMessageComponentChooser.qml
MessageComponentChooser.qml
ReplyMessageComponentChooser.qml
AuthorComponent.qml
@@ -27,7 +26,6 @@ ecm_add_qml_module(timeline GENERATE_PLUGIN_SOURCE
ChatBarComponent.qml
CodeComponent.qml
EncryptedComponent.qml
FetchButtonComponent.qml
FileComponent.qml
ImageComponent.qml
ItineraryComponent.qml
@@ -52,7 +50,6 @@ ecm_add_qml_module(timeline GENERATE_PLUGIN_SOURCE
ReplyComponent.qml
StateComponent.qml
TextComponent.qml
ThreadBodyComponent.qml
VideoComponent.qml
SOURCES
timelinedelegate.cpp

View File

@@ -1,52 +0,0 @@
// SPDX-FileCopyrightText: 2025 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.Controls as QQC2
import QtQuick.Layouts
import org.kde.kirigami as Kirigami
import org.kde.kirigamiaddons.delegates as Delegates
import org.kde.neochat
import org.kde.neochat.chatbar
/**
* @brief A component to show a reply button for threads in a message bubble.
*/
Delegates.RoundedItemDelegate {
id: root
/**
* @brief The maximum width that the bubble's content can be.
*/
property real maxContentWidth: -1
/**
* @brief Request more events in the thread be loaded.
*/
signal fetchMoreEvents()
Layout.fillWidth: true
Layout.maximumWidth: root.maxContentWidth
leftInset: 0
rightInset: 0
highlighted: true
icon.name: "arrow-up"
icon.width: Kirigami.Units.iconSizes.sizeForLabels
icon.height: Kirigami.Units.iconSizes.sizeForLabels
text: i18nc("@action:button", "Fetch More Events")
onClicked: {
root.fetchMoreEvents()
}
contentItem: Kirigami.Icon {
implicitWidth: root.icon.width
implicitHeight: root.icon.height
source: root.icon.name
}
}

View File

@@ -9,16 +9,232 @@ import org.kde.neochat
/**
* @brief Select a message component based on a MessageComponentType.
*/
BaseMessageComponentChooser {
DelegateChooser {
id: root
/**
* @brief The NeoChatRoom the delegate is being displayed in.
*/
required property NeoChatRoom room
/**
* @brief The index of the delegate in the model.
*/
required property var index
/**
* @brief The timeline ListView this component is being used in.
*/
required property ListView timeline
/**
* @brief The maximum width that the bubble's content can be.
*/
property real maxContentWidth: -1
/**
* @brief The reply has been clicked.
*/
signal replyClicked(string eventID)
/**
* @brief The user selected text has changed.
*/
signal selectedTextChanged(string selectedText)
/**
* @brief The user hovered link has changed.
*/
signal hoveredLinkChanged(string hoveredLink)
/**
* @brief Request a context menu be show for the message.
*/
signal showMessageMenu
signal removeLinkPreview(int index)
role: "componentType"
DelegateChoice {
roleValue: MessageComponentType.ThreadBody
delegate: ThreadBodyComponent {
roleValue: MessageComponentType.Author
delegate: AuthorComponent {
maxContentWidth: root.maxContentWidth
}
}
DelegateChoice {
roleValue: MessageComponentType.Text
delegate: TextComponent {
maxContentWidth: root.maxContentWidth
onSelectedTextChanged: root.selectedTextChanged(selectedText)
onHoveredLinkChanged: root.hoveredLinkChanged(hoveredLink)
onShowMessageMenu: root.showMessageMenu()
}
}
DelegateChoice {
roleValue: MessageComponentType.Image
delegate: ImageComponent {
room: root.room
index: root.index
timeline: root.timeline
maxContentWidth: root.maxContentWidth
}
}
DelegateChoice {
roleValue: MessageComponentType.Video
delegate: VideoComponent {
room: root.room
index: root.index
timeline: root.timeline
maxContentWidth: root.maxContentWidth
}
}
DelegateChoice {
roleValue: MessageComponentType.Code
delegate: CodeComponent {
maxContentWidth: root.maxContentWidth
onSelectedTextChanged: selectedText => {
root.selectedTextChanged(selectedText);
}
onShowMessageMenu: root.showMessageMenu()
}
}
DelegateChoice {
roleValue: MessageComponentType.Quote
delegate: QuoteComponent {
maxContentWidth: root.maxContentWidth
onSelectedTextChanged: selectedText => {
root.selectedTextChanged(selectedText);
}
onShowMessageMenu: root.showMessageMenu()
}
}
DelegateChoice {
roleValue: MessageComponentType.Audio
delegate: AudioComponent {
room: root.room
maxContentWidth: root.maxContentWidth
}
}
DelegateChoice {
roleValue: MessageComponentType.File
delegate: FileComponent {
room: root.room
maxContentWidth: root.maxContentWidth
}
}
DelegateChoice {
roleValue: MessageComponentType.Itinerary
delegate: ItineraryComponent {
maxContentWidth: root.maxContentWidth
}
}
DelegateChoice {
roleValue: MessageComponentType.Pdf
delegate: PdfPreviewComponent {
maxContentWidth: root.maxContentWidth
}
}
DelegateChoice {
roleValue: MessageComponentType.Poll
delegate: PollComponent {
room: root.room
maxContentWidth: root.maxContentWidth
}
}
DelegateChoice {
roleValue: MessageComponentType.Location
delegate: LocationComponent {
maxContentWidth: root.maxContentWidth
}
}
DelegateChoice {
roleValue: MessageComponentType.LiveLocation
delegate: LiveLocationComponent {
room: root.room
maxContentWidth: root.maxContentWidth
}
}
DelegateChoice {
roleValue: MessageComponentType.Encrypted
delegate: EncryptedComponent {
maxContentWidth: root.maxContentWidth
}
}
DelegateChoice {
roleValue: MessageComponentType.Reply
delegate: ReplyComponent {
maxContentWidth: root.maxContentWidth
onReplyClicked: eventId => {
root.replyClicked(eventId);
}
}
}
DelegateChoice {
roleValue: MessageComponentType.LinkPreview
delegate: LinkPreviewComponent {
maxContentWidth: root.maxContentWidth
onRemove: index => root.removeLinkPreview(index)
}
}
DelegateChoice {
roleValue: MessageComponentType.LinkPreviewLoad
delegate: LinkPreviewLoadComponent {
type: LinkPreviewLoadComponent.LinkPreview
maxContentWidth: root.maxContentWidth
onRemove: index => root.removeLinkPreview(index)
}
}
DelegateChoice {
roleValue: MessageComponentType.ChatBar
delegate: ChatBarComponent {
room: root.room
maxContentWidth: root.maxContentWidth
}
}
DelegateChoice {
roleValue: MessageComponentType.ReplyButton
delegate: ReplyButtonComponent {
room: root.room
maxContentWidth: root.maxContentWidth
}
}
DelegateChoice {
roleValue: MessageComponentType.Verification
delegate: MimeComponent {
mimeIconSource: "security-high"
label: i18n("%1 started a user verification", model.author.htmlSafeDisplayName)
}
}
DelegateChoice {
roleValue: MessageComponentType.Loading
delegate: LoadComponent {
maxContentWidth: root.maxContentWidth
}
}
DelegateChoice {
roleValue: MessageComponentType.Other
delegate: Item {}
}
}

View File

@@ -289,7 +289,7 @@ TimelineDelegate {
AnchorChanges {
target: bubble
anchors.left: avatar.right
anchors.right: root.isThreaded ? parent.right : undefined
anchors.right: undefined
}
}
]
@@ -300,7 +300,13 @@ TimelineDelegate {
showAuthor: root.showAuthor
isThreaded: root.isThreaded
contentModel: root.contentModel
// HACK: This is stupid but seemingly QConcatenateTablesProxyModel
// can't be passed as a model role, always returning null.
contentModel: if (root.isThreaded && NeoChatConfig.threads) {
return RoomManager.timelineModel.timelineMessageModel.threadModelForRootId(root.threadRoot);
} else {
return root.contentModel;
}
timeline: root.ListView.view
showHighlight: root.showHighlight

View File

@@ -1,93 +0,0 @@
// SPDX-FileCopyrightText: 2025 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.neochat
/**
* @brief A component to visualize a ThreadModel.
*
* @sa ThreadModel
*/
ColumnLayout {
id: root
/**
* @brief The NeoChatRoom the delegate is being displayed in.
*/
required property NeoChatRoom room
/**
* @brief The index of the delegate in the model.
*/
required property var index
/**
* @brief The Matrix ID of the root message in the thread, if any.
*/
required property string threadRoot
/**
* @brief The timeline ListView this component is being used in.
*/
required property ListView timeline
/**
* @brief The maximum width that the bubble's content can be.
*/
property real maxContentWidth: -1
/**
* @brief The reply has been clicked.
*/
signal replyClicked(string eventID)
/**
* @brief The user selected text has changed.
*/
signal selectedTextChanged(string selectedText)
/**
* @brief The user hovered link has changed.
*/
signal hoveredLinkChanged(string hoveredLink)
/**
* @brief Request a context menu be show for the message.
*/
signal showMessageMenu
Layout.fillWidth: true
Layout.fillHeight: true
Layout.maximumWidth: root.maxContentWidth
spacing: Kirigami.Units.smallSpacing
Repeater {
id: threadRepeater
model: root.room.modelForThread(root.threadRoot);
delegate: BaseMessageComponentChooser {
room: root.room
index: root.index
timeline: root.timeline
maxContentWidth: root.maxContentWidth
onReplyClicked: eventId => {
root.replyClicked(eventId);
}
onSelectedTextChanged: selectedText => {
root.selectedTextChanged(selectedText);
}
onHoveredLinkChanged: hoveredLink => {
root.hoveredLinkChanged(hoveredLink);
}
onShowMessageMenu: root.showMessageMenu()
onRemoveLinkPreview: index => threadRepeater.model.closeLinkPreview(index)
onFetchMoreEvents: threadRepeater.model.fetchMoreEvents(5)
}
}
}

View File

@@ -13,7 +13,6 @@
#include <Quotient/connection.h>
#include <QJsonDocument>
#include <QQuickWindow>
using namespace Quotient;
@@ -38,16 +37,6 @@ QColor QmlUtils::getUserColor(qreal hueF)
return QColor::fromHslF(hueF, 1, -0.7 * lightness + 0.9, 1);
}
QQuickItem *QmlUtils::focusedWindowItem()
{
const auto window = qobject_cast<QQuickWindow *>(QGuiApplication::focusWindow());
if (window) {
return window->contentItem();
} else {
return nullptr;
}
}
bool Utils::isEmoji(const QString &text)
{
#ifdef HAVE_ICU

View File

@@ -7,7 +7,6 @@
#include <QGuiApplication>
#include <QPalette>
#include <QQmlEngine>
#include <QQuickItem>
#include <QRegularExpression>
#include <Quotient/user.h>
@@ -37,7 +36,6 @@ public:
Q_INVOKABLE bool isValidJson(const QByteArray &json);
Q_INVOKABLE QString escapeString(const QString &string);
Q_INVOKABLE QColor getUserColor(qreal hueF);
Q_INVOKABLE QQuickItem *focusedWindowItem();
private:
QmlUtils() = default;