Compare commits

..

16 Commits

Author SHA1 Message Date
Carlos De Maine
c58ac6769b try again to set track/risk on extension 2026-02-13 23:43:43 +10:00
Carlos De Maine
1f482000d2 trya nd oveeride with anchor and alias 2026-01-09 09:05:19 +10:00
Carlos De Maine
d0731c9c36 no override-pull: 2026-01-08 21:08:07 +10:00
Carlos De Maine
530fbc212b formatting 2026-01-08 20:51:26 +10:00
Carlos De Maine
ea467a7901 use the CRAFT_SNAP_CHANNEL instead 2026-01-08 20:47:00 +10:00
Carlos De Maine
78ac2681de $KDE_SNAP_CHANNEL 2026-01-08 18:07:38 +10:00
Carlos De Maine
0e16518123 docbook-xml & docbook-xsl 2026-01-08 17:51:26 +10:00
Carlos De Maine
62e204207d patch in qt6.10 support t libquotient 2026-01-08 17:42:36 +10:00
Carlos De Maine
be712a6810 modernise paths, cmake-paths etc 2026-01-08 16:45:45 +10:00
Carlos De Maine
287043ed44 libsecret and qtkeychain are in the kf6-sdk 2026-01-08 16:06:28 +10:00
Carlos De Maine
4675f5f7e7 bump keychain to 0.15.0 2026-01-08 16:01:05 +10:00
Carlos De Maine
305f008796 bump libquotient to 9.5 2026-01-08 15:43:09 +10:00
Carlos De Maine
a4bf3e4da0 duktape-dev 2026-01-08 15:14:35 +10:00
Carlos De Maine
20bd8508e0 after:
- neochat
2026-01-08 15:04:32 +10:00
Carlos De Maine
7406fd9884 add cleanup part with overriding build-snaps: 2026-01-08 15:00:14 +10:00
Carlos De Maine
ff62c74cb3 try building with kf6-extension from edge channel 2026-01-08 14:52:18 +10:00
109 changed files with 11842 additions and 21552 deletions

View File

@@ -4,18 +4,18 @@
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/linux-qt6-next.yml
- /gitlab-templates/windows-qt6.yml
- /gitlab-templates/freebsd-qt6.yml
- /gitlab-templates/flatpak.yml
#- /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/linux-qt6-next.yml
#- /gitlab-templates/windows-qt6.yml
#- /gitlab-templates/freebsd-qt6.yml
#- /gitlab-templates/flatpak.yml
- /gitlab-templates/snap-snapcraft-lxd.yml
- /gitlab-templates/craft-android-qt6-apks.yml
- /gitlab-templates/craft-appimage-qt6.yml
- /gitlab-templates/craft-windows-x86-64-qt6.yml
- /gitlab-templates/craft-windows-appx-qt6.yml
#- /gitlab-templates/craft-android-qt6-apks.yml
#- /gitlab-templates/craft-appimage-qt6.yml
#- /gitlab-templates/craft-windows-x86-64-qt6.yml
#- /gitlab-templates/craft-windows-appx-qt6.yml

View File

@@ -208,7 +208,7 @@ void TimelineMessageModelTest::idToRow()
auto room = new TestUtils::TestRoom(connection, u"#myroom:kde.org"_s, u"test-min-sync.json"_s);
model->setRoom(room);
QCOMPARE(model->indexForEventId(u"$153456789:example.org"_s).row(), 0);
QCOMPARE(model->indexforEventId(u"$153456789:example.org"_s).row(), 0);
}
void TimelineMessageModelTest::cleanup()

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

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

@@ -10,7 +10,7 @@ confinement: strict
apps:
neochat:
extensions:
- kde-neon-6
- kde-neon-6@latest/beta
command: usr/bin/neochat
common-id: org.kde.neochat
desktop: usr/share/applications/org.kde.neochat.desktop
@@ -25,7 +25,7 @@ apps:
- password-manager-service
- accounts-service
environment:
QT_PLUGIN_PATH: "$SNAP/usr/lib/$CRAFT_ARCH_TRIPLET/plugins/snap/kf6-core24/current/usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR/plugins"
QT_PLUGIN_PATH: "$SNAP/usr/lib/$CRAFT_ARCH_TRIPLET/plugins:/snap/kf6-core24/current/usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR/plugins"
QML_IMPORT_PATH: "$SNAP/usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR/qml:/snap/kf6-core24/current/usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR/qml"
compression: lzo
@@ -55,53 +55,11 @@ parts:
- -usr/lib/*/pkgconfig
- -usr/lib/*/cmake
libsecret:
source: https://gitlab.gnome.org/GNOME/libsecret.git
source-tag: '0.21.4'
source-depth: 1
plugin: meson
meson-parameters:
- --prefix=/usr
- -Doptimization=3
- -Ddebug=true
- -Dmanpage=false
- -Dvapi=false
- -Dintrospection=false
- -Dcrypto=disabled
- -Dgtk_doc=false
build-packages:
- meson
- libglib2.0-dev
- libgcrypt20-dev
prime:
- -usr/include
- -usr/lib/*/pkgconfig
qtkeychain:
after: [libsecret]
source: https://github.com/frankosterfeld/qtkeychain.git
source-tag: 0.14.3
source-depth: 1
plugin: cmake
build-environment:
- PATH: /snap/bin:${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
- -DBUILD_TRANSLATIONS=NO
- -DBUILD_WITH_QT6=ON
prime:
- -usr/include
- -usr/lib/*/pkgconfig
- -usr/lib/*/cmake
libquotient:
after:
- olm
- qtkeychain
source: https://github.com/quotient-im/libQuotient.git
source-tag: 0.9.2
source-branch: 0.9.5
source-depth: 1
plugin: cmake
build-environment:
@@ -110,12 +68,16 @@ parts:
- cmake
build-packages:
- libssl-dev
- curl
cmake-parameters:
- -DCMAKE_INSTALL_PREFIX=/usr
- -DCMAKE_BUILD_TYPE=Release
- -DBUILD_TESTING=OFF
- -DQuotient_ENABLE_E2EE=ON
- -DBUILD_WITH_QT6=ON
override-pull: |
craftctl default
curl https://github.com/quotient-im/libQuotient/commit/ea83157eed37ff97ab275a5d14c971f0a5a70595.diff | patch -p1
prime:
- -usr/include
- -usr/lib/*/pkgconfig
@@ -123,38 +85,39 @@ parts:
kquickimageeditor:
source: https://invent.kde.org/libraries/kquickimageeditor.git
source-tag: 'v0.3.0'
source-tag: 'v0.6.0'
source-depth: 1
plugin: cmake
build-environment:
- PATH: /snap/bin:${PATH}
build-environment: &build-environment
- LD_LIBRARY_PATH: "/snap/kde-qt6-core24-sdk/current/usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR:/snap/kde-qt6-core24-sdk/current/usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR/libproxy:/snap/kde-qt6-core24-sdk/current/usr/lib:/snap/kf6-core24-sdk/current/usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR:/snap/kf6-core24-sdk/current/usr/lib:$CRAFT_STAGE/usr/lib:$CRAFT_STAGE/lib/:$CRAFT_STAGE/usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}"
- PATH: /snap/kde-qt6-core24-sdk/current/usr/bin${PATH:+:$PATH}
- PKG_CONFIG_PATH: /snap/kde-qt6-core24-sdk/current/usr/lib/${CRAFT_ARCH_TRIPLET_BUILD_FOR}/pkgconfig${PKG_CONFIG_PATH:+:$PKG_CONFIG_PATH}
- PYTHONPATH: ${CRAFT_STAGE}/lib/python3.12/site-packages:${CRAFT_STAGE}/usr/lib/python3/dist-packages
- LD_LIBRARY_PATH: "/snap/mesa-2404/current/usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR:$CRAFT_STAGE/usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR:/snap/kde-qt6-core24-sdk/current/usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR/libproxy:$LD_LIBRARY_PATH"
cmake-parameters:
- XDG_DATA_DIRS: $CRAFT_STAGE/usr/share:/snap/kde-qt6-core24-sdk/current/usr/share:/usr/share${XDG_DATA_DIRS:+:$XDG_DATA_DIRS}
- XDG_CONFIG_HOME: $CRAFT_STAGE/etc/xdg:/snap/kde-qt6-core24-sdk/current/etc/xdg:/etc/xdg${XDG_CONFIG_HOME:+:$XDG_CONFIG_HOME}
cmake-parameters: &cmake-parameters
- -DCMAKE_INSTALL_PREFIX=/usr
- -DCMAKE_BUILD_TYPE=Release
- -DQT_MAJOR_VERSION=6
- -DBUILD_WITH_QT6=ON
- -DBUILD_TESTING=OFF
prime:
- "-DCMAKE_FIND_ROOT_PATH=$CRAFT_STAGE\\;/snap/kde-qt6-core24-sdk/current\\;/snap/kf6-core24-sdk/current/usr"
- "-DCMAKE_PREFIX_PATH=$CRAFT_STAGE\\;/snap/kde-qt6-core24-sdk/current\\;/snap/kf6-core24-sdk/current/usr"
prime: &prime
- -usr/include
- -usr/lib/*/pkgconfig
- -usr/lib/*/cmake
kunifiedpush:
source: https://invent.kde.org/libraries/kunifiedpush.git
source-branch: release/24.12
plugin: cmake
build-environment:
- PATH: /snap/bin:${PATH}
- PYTHONPATH: ${CRAFT_STAGE}/lib/python3.12/site-packages:${CRAFT_STAGE}/usr/lib/python3/dist-packages
- LD_LIBRARY_PATH: "/snap/mesa-2404/current/usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR:$CRAFT_STAGE/usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR:/snap/kde-qt6-core24-sdk/current/usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR/libproxy:$LD_LIBRARY_PATH"
cmake-parameters:
- -DCMAKE_INSTALL_PREFIX=/usr
- -DCMAKE_BUILD_TYPE=Release
- -DBUILD_TESTING=OFF
build-environment: *build-environment
cmake-parameters: *cmake-parameters
prime: *prime
neochat:
after:
- qtkeychain
- libquotient
- kquickimageeditor
- kunifiedpush
@@ -162,24 +125,20 @@ parts:
- usr/share/metainfo/org.kde.neochat.appdata.xml
source: .
plugin: cmake
build-environment:
- PATH: /snap/bin:${PATH}
- PYTHONPATH: ${CRAFT_STAGE}/lib/python3.12/site-packages:${CRAFT_STAGE}/usr/lib/python3/dist-packages
- LD_LIBRARY_PATH: "/snap/mesa-2404/current/usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR:$CRAFT_STAGE/usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR:/snap/kde-qt6-core24-sdk/current/usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR/libproxy:$LD_LIBRARY_PATH"
build-environment: *build-environment
build-packages:
- cmark
- docbook-xml
- docbook-xsl
- duktape-dev
- libcmark-dev
- libsqlite3-dev
- libvulkan-dev
- libxkbcommon-dev
- libicu-dev
- libpulse0
cmake-parameters:
- -DCMAKE_INSTALL_PREFIX=/usr
- -DCMAKE_BUILD_TYPE=Release
- -DBUILD_TESTING=OFF
prime:
- -usr/share/man
cmake-parameters: *cmake-parameters
prime: *prime
deps:
after: [neochat]
@@ -198,3 +157,14 @@ parts:
${CRAFT_PART_SRC}/bin/gpu-2404-cleanup mesa-2404
prime:
- bin/gpu-2404-wrapper
cleanup:
after:
- neochat
plugin: nil
override-prime: |
set -eux
for snap in "core24" "kf6-core24"; do
cd "/snap/$snap/current" && find . -type f,l -exec rm -rf "${CRAFT_PRIME}/{}" \;
done

View File

@@ -37,8 +37,6 @@ qt_add_library(neochat STATIC
texttospeechhelper.cpp
models/limitermodel.cpp
models/limitermodel.h
supportcontroller.cpp
supportcontroller.h
)
set_source_files_properties(qml/OsmLocationPlugin.qml PROPERTIES
@@ -108,8 +106,6 @@ ecm_add_qml_module(neochat URI org.kde.neochat GENERATE_PLUGIN_SOURCE
qml/NewPollDialog.qml
qml/UserMenu.qml
qml/MeetingDialog.qml
qml/SeenByDialog.qml
qml/SupportDialog.qml
DEPENDENCIES
QtCore
QtQuick
@@ -220,7 +216,6 @@ target_link_libraries(neochat PUBLIC
KF6::ItemModels
KF6::I18nQml
KirigamiApp
KirigamiAddonsComponents
QuotientQt6
Login
Rooms

View File

@@ -11,7 +11,6 @@
#include <KNotification>
#include <KNotificationPermission>
#include <KNotificationReplyAction>
#include <KirigamiAddons/Components/NameUtils>
#include <QPainter>
#include <Quotient/accountregistry.h>
@@ -145,9 +144,16 @@ void NotificationsManager::processNotificationJob(QPointer<NeoChatConnection> co
body = notification["event"_L1]["content"_L1]["body"_L1].toString();
}
QImage avatar_image;
if (!sender.avatarUrl().isEmpty()) {
avatar_image = room->member(sender.id()).avatar(128, 128, {});
} else {
avatar_image = room->avatar(128);
}
postNotification(dynamic_cast<NeoChatRoom *>(room),
room->member(sender.id()),
sender.displayName(),
body,
avatar_image,
notification["event"_L1].toObject()["event_id"_L1].toString(),
true,
pair.first);
@@ -189,8 +195,9 @@ bool NotificationsManager::shouldPostNotification(QPointer<NeoChatConnection> co
}
void NotificationsManager::postNotification(NeoChatRoom *room,
const RoomMember &member,
const QString &sender,
const QString &text,
const QImage &icon,
const QString &replyEventId,
bool canReply,
qint64 timestamp)
@@ -215,11 +222,11 @@ void NotificationsManager::postNotification(NeoChatRoom *room,
if (room->isDirectChat()) {
entry = text.toHtmlEscaped();
} else {
entry = i18n("%1: %2", member.displayName(), text.toHtmlEscaped());
entry = i18n("%1: %2", sender, text.toHtmlEscaped());
}
notification->setText(entry);
notification->setPixmap(createNotificationImage(member, room));
notification->setPixmap(createNotificationImage(icon, room));
auto defaultAction = notification->addDefaultAction(i18n("Open NeoChat in this room"));
connect(defaultAction, &KNotificationAction::activated, this, [notification, room]() {
@@ -287,10 +294,18 @@ void NotificationsManager::doPostInviteNotification(QPointer<NeoChatRoom> room)
}
const auto sender = room->member(roomMemberEvent->senderId());
QImage avatar_image;
if (roomMemberEvent && !room->member(roomMemberEvent->senderId()).avatarUrl().isEmpty()) {
avatar_image = room->member(roomMemberEvent->senderId()).avatar(128, 128, {});
} else {
qWarning() << "using this room's avatar";
avatar_image = room->avatar(128);
}
KNotification *notification = new KNotification(u"invite"_s);
notification->setText(i18n("%1 invited you to a room", sender.htmlSafeDisplayName()));
notification->setTitle(room->displayName());
notification->setPixmap(createNotificationImage(sender, room));
notification->setPixmap(createNotificationImage(avatar_image, nullptr));
auto defaultAction = notification->addDefaultAction(i18n("Open this invitation in NeoChat"));
connect(defaultAction, &KNotificationAction::activated, this, [notification, room]() {
if (!room) {
@@ -396,125 +411,41 @@ void NotificationsManager::postPushNotification(const QByteArray &message)
}
}
QPixmap NotificationsManager::createNotificationImage(const Quotient::RoomMember &member, NeoChatRoom *room)
{
QImage senderIcon = member.avatar(avatarDimension, avatarDimension, {});
bool senderIconIsPlaceholder = false;
if (senderIcon.isNull()) {
senderIcon = createPlaceholderImage(member.displayName());
senderIconIsPlaceholder = true;
}
QImage icon;
if (room->isDirectChat()) {
icon = senderIcon;
} else {
QImage roomIcon = room->avatar(avatarDimension, avatarDimension);
bool roomIconIsPlaceholder = false;
if (roomIcon.isNull()) {
roomIcon = createPlaceholderImage(room->displayName());
roomIconIsPlaceholder = true;
}
icon = createCombinedNotificationImage(senderIcon, senderIconIsPlaceholder, roomIcon, roomIconIsPlaceholder);
}
return QPixmap::fromImage(icon);
}
QImage NotificationsManager::createCombinedNotificationImage(const QImage &senderIcon,
const bool senderIconIsPlaceholder,
const QImage &roomIcon,
const bool roomIconIsPlaceholder)
QPixmap NotificationsManager::createNotificationImage(const QImage &icon, NeoChatRoom *room)
{
// Handle avatars that are lopsided in one dimension
const int biggestDimension = std::max(senderIcon.width(), senderIcon.height());
const QRectF imageRect = QRect{0, 0, biggestDimension, biggestDimension}.toRectF();
const int biggestDimension = std::max(icon.width(), icon.height());
const QRect imageRect{0, 0, biggestDimension, biggestDimension};
QImage roundedImage(imageRect.size().toSize(), QImage::Format_ARGB32);
QImage roundedImage(imageRect.size(), QImage::Format_ARGB32);
roundedImage.fill(Qt::transparent);
QPainter painter(&roundedImage);
painter.setRenderHint(QPainter::SmoothPixmapTransform);
painter.setPen(Qt::NoPen);
if (senderIconIsPlaceholder) {
painter.drawImage(imageRect, senderIcon);
} else {
// Fill background for transparent non-placeholder avatars
painter.setBrush(Qt::white);
painter.drawRoundedRect(imageRect, imageRect.width(), imageRect.height());
// Fill background for transparent avatars
painter.setBrush(Qt::white);
painter.drawRoundedRect(imageRect, imageRect.width(), imageRect.height());
painter.setBrush(senderIcon.scaledToHeight(biggestDimension));
painter.drawRoundedRect(imageRect, imageRect.width(), imageRect.height());
QBrush brush(icon.scaledToHeight(biggestDimension));
painter.setBrush(brush);
painter.drawRoundedRect(imageRect, imageRect.width(), imageRect.height());
if (room != nullptr) {
const QImage roomAvatar = room->avatar(imageRect.width(), imageRect.height());
if (!roomAvatar.isNull() && icon != roomAvatar) {
const QRect lowerQuarter{imageRect.center(), imageRect.size() / 2};
painter.setBrush(Qt::white);
painter.drawRoundedRect(lowerQuarter, lowerQuarter.width(), lowerQuarter.height());
painter.setBrush(roomAvatar.scaled(lowerQuarter.size()));
painter.drawRoundedRect(lowerQuarter, lowerQuarter.width(), lowerQuarter.height());
}
}
const QRectF lowerQuarter{imageRect.center(), imageRect.size() / 2.0};
if (roomIconIsPlaceholder) {
// Ditto for room icons, but we also want to "carve out" the transparent area for readability
painter.setCompositionMode(QPainter::CompositionMode_Clear);
painter.setBrush(Qt::transparent);
painter.drawRoundedRect(lowerQuarter, lowerQuarter.width(), lowerQuarter.height());
painter.setCompositionMode(QPainter::CompositionMode_SourceOver);
painter.drawImage(lowerQuarter, roomIcon);
} else {
painter.setBrush(Qt::white);
painter.drawRoundedRect(lowerQuarter, lowerQuarter.width(), lowerQuarter.height());
painter.setBrush(roomIcon.scaled(lowerQuarter.size().toSize()));
painter.drawRoundedRect(lowerQuarter, lowerQuarter.width(), lowerQuarter.height());
}
return roundedImage;
}
QImage NotificationsManager::createPlaceholderImage(const QString &name)
{
const QColor color = NameUtils().colorsFromString(name);
QImage image(avatarDimension, avatarDimension, QImage::Format_ARGB32);
image.fill(Qt::transparent);
QPainter painter(&image);
painter.setRenderHint(QPainter::Antialiasing);
// Draw background
QColor backgroundColor = color;
backgroundColor.setAlphaF(0.07); // Same as in Kirigami Add-ons.
painter.setBrush(backgroundColor);
painter.setPen(Qt::transparent);
painter.drawRoundedRect(image.rect(), image.width(), image.height());
constexpr float borderWidth = 3.0; // Slightly bigger than in Add-ons so it renders better with QPainter at these dimensions.
// Draw border
painter.setBrush(Qt::transparent);
painter.setPen(QPen(color, borderWidth));
painter.drawRoundedRect(image.rect().toRectF().marginsRemoved(QMarginsF(borderWidth, borderWidth, borderWidth, borderWidth)),
image.width(),
image.height());
const QString initials = NameUtils().initialsFromString(name);
QTextOption option;
option.setAlignment(Qt::AlignCenter);
// Calculation similar to the one found in Kirigami Add-ons.
constexpr int largeSpacing = 8; // Same as what's defined in kirigami.
constexpr int padding = std::max(0, std::min(largeSpacing, avatarDimension - largeSpacing * 2));
QFont font;
font.setPixelSize((avatarDimension - padding) / 2);
painter.setBrush(color);
painter.setPen(color);
painter.setFont(font);
painter.drawText(image.rect(), initials, option);
return image;
return QPixmap::fromImage(roundedImage);
}
#include "moc_notificationsmanager.cpp"

View File

@@ -13,11 +13,6 @@
#include <Quotient/csapi/notifications.h>
#include <Quotient/jobs/basejob.h>
namespace Quotient
{
class RoomMember;
}
class NeoChatConnection;
class KNotification;
class NeoChatRoom;
@@ -72,24 +67,15 @@ private:
QStringList m_connActiveJob;
void startNotificationJob(QPointer<NeoChatConnection> connection);
/**
* @return A combined image of the sender and room's avatar.
*/
static QPixmap createNotificationImage(const Quotient::RoomMember &member, NeoChatRoom *room);
/**
* @return The sender and room icon combined together into one image. Used internally by createNotificationImage.
*/
static QImage createCombinedNotificationImage(const QImage &senderIcon, bool senderIconIsPlaceholder, const QImage &roomIcon, bool roomIconIsPlaceholder);
/**
* @return A placeholder avatar image, similar to the one found in Kirigami Add-ons.
*/
static QImage createPlaceholderImage(const QString &name);
QPixmap createNotificationImage(const QImage &icon, NeoChatRoom *room);
bool shouldPostNotification(QPointer<NeoChatConnection> connection, const QJsonValue &notification);
void
postNotification(NeoChatRoom *room, const Quotient::RoomMember &member, const QString &text, const QString &replyEventId, bool canReply, qint64 timestamp);
void postNotification(NeoChatRoom *room,
const QString &sender,
const QString &text,
const QImage &icon,
const QString &replyEventId,
bool canReply,
qint64 timestamp);
void doPostInviteNotification(QPointer<NeoChatRoom> room);
@@ -98,8 +84,6 @@ private:
bool permissionAsked = false;
static constexpr int avatarDimension = 128;
private Q_SLOTS:
void processNotificationJob(QPointer<NeoChatConnection> connection, Quotient::GetNotificationsJob *job, bool initialization);
};

View File

@@ -5,7 +5,6 @@ pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Controls as QQC2
import QtMultimedia
import org.kde.kirigami as Kirigami
import org.kde.kirigamiaddons.components as KirigamiComponents
@@ -19,10 +18,6 @@ KirigamiComponents.ConvergentContextMenu {
required property NeoChatConnection connection
required property Kirigami.ApplicationWindow window
data: MediaDevices {
id: devices
}
Kirigami.Action {
text: i18nc("@action:button", "Show QR Code")
icon.name: "view-barcode-qr-symbolic"
@@ -38,14 +33,12 @@ KirigamiComponents.ConvergentContextMenu {
}
Kirigami.Action {
text: i18nc("@action:inmenu", "Scan a QR Code")
icon.name: "document-scan-symbolic"
visible: devices.videoInputs.length > 0
onTriggered: (root.Kirigami.PageStack.pageStack as Kirigami.PageRow).pushDialogLayer(Qt.createComponent("org.kde.neochat", "QrScannerPage"), {
text: i18nc("@action:inmenu", "Switch Account")
icon.name: "system-switch-user"
shortcut: "Ctrl+U"
onTriggered: (Qt.createComponent("org.kde.neochat", "AccountSwitchDialog").createObject(QQC2.Overlay.overlay, {
connection: root.connection
}, {
title: i18nc("@title", "Scan a QR Code")
})
}) as Kirigami.Dialog).open();
}
Kirigami.Action {
@@ -62,6 +55,14 @@ KirigamiComponents.ConvergentContextMenu {
}
}
Kirigami.Action {
text: i18nc("@action:inmenu", "Devices")
icon.name: "computer-symbolic"
onTriggered: {
NeoChatSettingsView.open('devices');
}
}
Kirigami.Action {
text: i18nc("@action:inmenu", "Open Developer Tools")
icon.name: "tools"
@@ -75,6 +76,14 @@ KirigamiComponents.ConvergentContextMenu {
})
}
Kirigami.Action {
text: i18nc("@action:inmenu", "Open Secret Backup")
icon.name: "unlock"
onTriggered: root.window.pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat', 'UnlockSSSSDialog'), {}, {
title: i18nc("@title:window", "Open Key Backup")
})
}
Kirigami.Action {
text: i18nc("@action:inmenu", "Verify This Device")
icon.name: "security-low"
@@ -94,25 +103,10 @@ KirigamiComponents.ConvergentContextMenu {
}
Kirigami.Action {
text: i18nc("@action:inmenu Open support dialog", "Support")
icon.name: "help-contents-symbolic"
onTriggered: {
Qt.createComponent("org.kde.neochat", "SupportDialog").createObject(QQC2.Overlay.overlay, {
connection: root.connection,
}).open();
}
}
Kirigami.Action {
separator: true
}
Kirigami.Action {
text: i18nc("@action:inmenu", "Switch Account")
icon.name: "system-switch-user"
shortcut: "Ctrl+U"
onTriggered: (Qt.createComponent("org.kde.neochat", "AccountSwitchDialog").createObject(QQC2.Overlay.overlay, {
text: i18nc("@action:inmenu", "Logout…")
icon.name: "im-kick-user"
onTriggered: (Qt.createComponent("org.kde.neochat", "ConfirmLogoutDialog").createObject(QQC2.Overlay.overlay, {
connection: root.connection
}) as Kirigami.Dialog).open();
}) as Kirigami.Dialog).open()
}
}

View File

@@ -154,10 +154,7 @@ Components.AlbumMaximizeComponent {
onOpened: forceActiveFocus()
onItemRightClicked: {
const event = root.currentRoom.findEvent(root.currentEventId);
RoomManager.viewEventMenu(root.QQC2.Overlay.overlay, event, root.currentRoom)
}
onItemRightClicked: RoomManager.viewEventMenu(root.currentEventId, root.currentRoom)
onSaveItem: {
var dialog = saveAsDialog.createObject(QQC2.Overlay.overlay) as Dialogs.FileDialog;

View File

@@ -6,7 +6,6 @@ import QtQuick.Controls as QQC2
import QtQuick.Layouts
import org.kde.kirigami as Kirigami
import org.kde.neochat
Kirigami.Page {
id: root
@@ -14,8 +13,6 @@ Kirigami.Page {
required property string placeholder
required property string actionText
required property string icon
required property bool reporting
required property NeoChatConnection connection
signal accepted(reason: string)
@@ -24,15 +21,6 @@ Kirigami.Page {
topPadding: 0
bottomPadding: 0
header: Kirigami.InlineMessage {
showCloseButton: false
visible: root.reporting
type: Kirigami.MessageType.Information
position: Kirigami.InlineMessage.Position.Header
text: xi18n("This report will <strong>only</strong> be sent to the administrators of <link>%1</link> (your server).", root.connection.domain)
}
QQC2.TextArea {
id: reason
placeholderText: root.placeholder
@@ -43,7 +31,7 @@ Kirigami.Page {
Keys.onReturnPressed: event => {
if (event.modifiers & Qt.ControlModifier) {
root.accepted(reason.text);
root.Kirigami.PageStack.closeDialog();
root.closeDialog();
}
}
@@ -64,14 +52,14 @@ Kirigami.Page {
QQC2.DialogButtonBox.buttonRole: QQC2.DialogButtonBox.AcceptRole
onClicked: {
root.accepted(reason.text);
root.Kirigami.PageStack.closeDialog();
root.closeDialog();
}
}
QQC2.Button {
icon.name: "dialog-cancel-symbolic"
text: i18nc("@action", "Cancel")
QQC2.DialogButtonBox.buttonRole: QQC2.DialogButtonBox.RejectRole
onClicked: root.Kirigami.PageStack.closeDialog()
onClicked: root.closeDialog()
}
}
}

View File

@@ -75,12 +75,6 @@ Kirigami.Page {
focus: true
padding: 0
onHeightChanged: {
// HACK: See TimelineView for the hack details.
// We get the height change here *first* so we are informed this is because of a window resize and not due to the pinned message.
(timelineViewLoader.item as TimelineView).resetViewSettling();
}
actions: [
Kirigami.Action {
id: jitsiMeetingAction
@@ -119,7 +113,7 @@ Kirigami.Page {
}
},
Kirigami.Action {
visible: Kirigami.Settings.isMobile || !(root.Kirigami.PageStack.pageStack as Kirigami.PageRow)?.wideMode
visible: Kirigami.Settings.isMobile || !(root.Kirigami.PageStack.pageStack as Kirigami.PageRow).wideMode
icon.name: "view-right-new"
onTriggered: (root.QQC2.ApplicationWindow.window as Main).openRoomDrawer()
}
@@ -295,7 +289,7 @@ Kirigami.Page {
footer: Loader {
id: chatBarLoader
height: active ? (item as ChatBar)?.implicitHeight : 0
height: active ? (item as ChatBar).implicitHeight : 0
active: timelineViewLoader.active && !root.currentRoom.readOnly
sourceComponent: ChatBar {
id: chatBar
@@ -355,17 +349,14 @@ Kirigami.Page {
});
}
function onShowDelegateMenu(parent: QtObject, room: NeoChatRoom, eventId: string, author, messageComponentType, plainText: string, richText: string, mimeType: string, progressInfo, selectedText: string, hoveredLink: string) {
(delegateContextMenu.createObject(parent, {
room: room,
function onShowDelegateMenu(eventId: string, author, messageComponentType, plainText: string, richText: string, mimeType: string, progressInfo, isThread: bool, selectedText: string, hoveredLink: string) {
(delegateContextMenu.createObject(root, {
author: author,
eventId: eventId,
plainText: plainText,
mimeType: mimeType,
progressInfo: progressInfo,
messageComponentType: messageComponentType,
selectedText,
hoveredLink,
}) as DelegateContextMenu).popup();
}

View File

@@ -1,90 +0,0 @@
// SPDX-FileCopyrightText: 2026 Joshua Goins <josh@redstrate.com>
// SPDX-License-Identifier: GPL-3.0-only
import QtQuick
import QtQuick.Controls as QQC2
import QtQuick.Layouts
import org.kde.kirigami as Kirigami
import org.kde.kirigamiaddons.components as KirigamiComponents
import org.kde.kirigamiaddons.delegates as Delegates
import org.kde.neochat
Kirigami.Dialog {
id: root
property var model
standardButtons: Kirigami.Dialog.NoButton
width: Math.min(QQC2.ApplicationWindow.window.width, Kirigami.Units.gridUnit * 24)
maximumHeight: Kirigami.Units.gridUnit * 24
title: i18nc("@title:menu Seen by/read marker dialog", "Seen By")
contentItem: ColumnLayout {
spacing: 0
QQC2.ScrollView {
Layout.fillWidth: true
Layout.fillHeight: true
ListView {
id: listView
model: root.model
spacing: Kirigami.Units.smallSpacing
onCountChanged: {
if (listView.count === 0) {
root.close();
}
}
delegate: Delegates.RoundedItemDelegate {
id: userDelegate
required property string displayName
required property url avatarUrl
required property color memberColor
required property string userId
implicitHeight: Kirigami.Units.gridUnit * 2
text: displayName
highlighted: false
onClicked: {
root.close();
RoomManager.resolveResource(userDelegate.userId);
}
contentItem: RowLayout {
spacing: Kirigami.Units.smallSpacing
KirigamiComponents.Avatar {
implicitWidth: height
sourceSize {
height: Kirigami.Units.gridUnit + Kirigami.Units.smallSpacing * 2.5
width: Kirigami.Units.gridUnit + Kirigami.Units.smallSpacing * 2.5
}
source: userDelegate.avatarUrl
name: userDelegate.displayName
color: userDelegate.memberColor
Layout.fillHeight: true
}
QQC2.Label {
text: userDelegate.displayName
textFormat: Text.PlainText
elide: Text.ElideRight
clip: true // Intentional to limit insane Unicode in display names
Layout.fillWidth: true
}
}
}
}
}
}
}

View File

@@ -1,121 +0,0 @@
// SPDX-FileCopyrightText: 2026 Joshua Goins <josh@redstrate.com>
// SPDX-License-Identifier: GPL-3.0-only
import QtQuick
import QtQuick.Controls as QQC2
import QtQuick.Layouts
import org.kde.kirigami as Kirigami
import org.kde.kirigamiaddons.formcard as FormCard
import org.kde.neochat
Kirigami.Dialog {
id: root
required property NeoChatConnection connection
readonly property SupportController supportController: SupportController {
connection: root.connection
}
readonly property bool hasSupportResources: supportController.supportPage.length > 0 && supportController.contacts.length > 0
title: i18nc("@title Support information", "Support")
width: Math.min(Kirigami.Units.gridUnit * 30, QQC2.ApplicationWindow.window.width)
ColumnLayout {
spacing: 0
FormCard.FormTextDelegate {
id: explanationTextDelegate
text: root.hasSupportResources ?
i18nc("@info:label %1 is the domain of the server", "Official support resources provided by %1:", root.connection.domain)
: i18nc("@info:label %1 is the domain of the server", "%1 has no support resources.", root.connection.domain)
}
FormCard.FormDelegateSeparator {
above: explanationTextDelegate
below: openSupportPageDelegate
visible: openSupportPageDelegate.visible
}
FormCard.FormLinkDelegate {
id: openSupportPageDelegate
icon.name: "help-contents-symbolic"
text: i18nc("@action:button Open support webpage", "Open Support")
url: root.supportController.supportPage
visible: root.supportController.supportPage.length > 0
}
FormCard.FormDelegateSeparator {
above: openSupportPageDelegate
visible: root.supportController.contacts.length > 0
}
Repeater {
model: root.supportController.contacts
delegate: FormCard.AbstractFormDelegate {
id: contactDelegate
required property string role
required property string matrixId
required property string emailAddress
background: null
Layout.fillWidth: true
contentItem: RowLayout {
spacing: Kirigami.Units.largeSpacing
Kirigami.Icon {
source: "user"
}
QQC2.Label {
text: {
// Translate known keys
if (contactDelegate.role === "m.role.admin") {
return i18nc("@info:label Adminstrator contact", "Admin")
} else if (contactDelegate.role === "m.role.security") {
return i18nc("@info:label Security contact", "Security")
}
return contactDelegate.role;
}
elide: Text.ElideRight
Layout.fillWidth: true
}
QQC2.ToolButton {
visible: contactDelegate.matrixId.length > 0
icon.name: "document-send-symbolic"
onClicked: {
root.close();
root.connection.requestDirectChat(contactDelegate.matrixId);
}
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
QQC2.ToolTip.visible: hovered
QQC2.ToolTip.text: i18nc("@info:tooltip %1 is a Matrix ID", "Contact via Matrix (%1)", contactDelegate.matrixId)
}
QQC2.ToolButton {
visible: contactDelegate.emailAddress.length > 0
icon.name: "mail-sent-symbolic"
onClicked: Qt.openUrlExternally("mailto:%1".arg(contactDelegate.emailAddress))
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
QQC2.ToolTip.visible: hovered
QQC2.ToolTip.text: i18nc("@info:tooltip %1 is an e-mail address", "Contact via e-mail (%1)", contactDelegate.emailAddress)
}
}
}
}
}
}

View File

@@ -13,7 +13,7 @@ FormCard.FormCardPage {
property bool processing: false
title: i18nc("@title:window", "Manage Secret Backup")
title: i18nc("@title:window", "Load your encrypted messages")
topPadding: Kirigami.Units.gridUnit
leftPadding: 0

View File

@@ -176,11 +176,9 @@ Kirigami.Dialog {
onTriggered: {
let dialog = ((root.QQC2.ApplicationWindow.window as Kirigami.ApplicationWindow).pageStack as Kirigami.PageRow).pushDialogLayer(Qt.createComponent('org.kde.neochat', 'ReasonDialog'), {
title: i18nc("@title:dialog", "Report User"),
placeholder: i18nc("@info:placeholder", "Optionally give a reason for reporting this user"),
placeholder: i18nc("@info:placeholder", "Reason for reporting this user"),
icon: "dialog-warning-symbolic",
actionText: i18nc("@action:button 'Report' as in 'Report this user to the administrators'", "Report"),
reporting: true,
connection: root.connection,
actionText: i18nc("@action:button 'Report' as in 'Report this user to the administrators'", "Report")
}, {
title: i18nc("@title", "Report User"),
width: Kirigami.Units.gridUnit * 25
@@ -234,23 +232,16 @@ Kirigami.Dialog {
actions: [
Kirigami.Action {
visible: {
if (root.room) {
return !root.isSelf && root.room.canSendState("kick") && root.room.containsUser(root.user.id) && root.room.memberEffectivePowerLevel(root.user.id) < root.room.memberEffectivePowerLevel(root.connection.localUserId);
}
return false;
}
visible: !root.isSelf && root.room.canSendState("kick") && root.room.containsUser(root.user.id) && root.room.memberEffectivePowerLevel(root.user.id) < root.room.memberEffectivePowerLevel(root.connection.localUserId)
text: i18nc("@action:button Kick the user from the room", "Kick…")
icon.name: "im-kick-user"
onTriggered: {
let dialog = (root.QQC2.ApplicationWindow.window as Kirigami.ApplicationWindow).pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat', 'ReasonDialog'), {
title: i18nc("@title:dialog", "Kick User"),
placeholder: i18nc("@info:placeholder", "Optionally give a reason for kicking this user"),
placeholder: i18nc("@info:placeholder", "Reason for kicking this user"),
actionText: i18nc("@action:button 'Kick' as in 'Kick this user from the room'", "Kick"),
icon: "im-kick-user",
reporting: false,
connection: root.connection,
icon: "im-kick-user"
}, {
title: i18nc("@title:dialog", "Kick User"),
width: Kirigami.Units.gridUnit * 25
@@ -262,12 +253,7 @@ Kirigami.Dialog {
}
},
Kirigami.Action {
visible: {
if (root.room) {
return !root.isSelf && root.room.canSendState("ban") && !root.room.isUserBanned(root.user.id) && root.room.memberEffectivePowerLevel(root.user.id) < root.room.memberEffectivePowerLevel(root.connection.localUserId);
}
return false;
}
visible: !root.isSelf && root.room.canSendState("ban") && !root.room.isUserBanned(root.user.id) && root.room.memberEffectivePowerLevel(root.user.id) < root.room.memberEffectivePowerLevel(root.connection.localUserId)
text: i18nc("@action:button Ban this user from the room", "Ban…")
icon.name: "im-ban-user"
@@ -275,11 +261,9 @@ Kirigami.Dialog {
onTriggered: {
let dialog = (root.QQC2.ApplicationWindow.window as Kirigami.ApplicationWindow).pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat', 'ReasonDialog'), {
title: i18nc("@title:dialog", "Ban User"),
placeholder: i18nc("@info:placeholder", "Optionally give a reason for banning this user"),
placeholder: i18nc("@info:placeholder", "Reason for banning this user"),
actionText: i18nc("@action:button 'Ban' as in 'Ban this user'", "Ban"),
icon: "im-ban-user",
reporting: false,
connection: root.connection,
icon: "im-ban-user"
}, {
title: i18nc("@title:dialog", "Ban User"),
width: Kirigami.Units.gridUnit * 25
@@ -291,12 +275,7 @@ Kirigami.Dialog {
}
},
Kirigami.Action {
visible: {
if (root.room) {
return !root.isSelf && root.room.canSendState("ban") && root.room.isUserBanned(root.user.id);
}
return false;
}
visible: !root.isSelf && root.room.canSendState("ban") && root.room.isUserBanned(root.user.id)
text: i18nc("@action:button Unban the user from this room", "Unban")
icon.name: "im-irc"
@@ -307,7 +286,7 @@ Kirigami.Dialog {
}
},
Kirigami.Action {
visible: (root.user.id === root.connection.localUserId || (root.room?.canSendState("redact") ?? false))
visible: (root.user.id === root.connection.localUserId || root.room.canSendState("redact"))
text: i18nc("@action:button Remove messages from the user in this room", "Remove Messages…")
icon.name: "delete"
@@ -315,11 +294,9 @@ Kirigami.Dialog {
onTriggered: {
let dialog = ((root.QQC2.ApplicationWindow.window as Kirigami.ApplicationWindow).pageStack as Kirigami.PageRow).pushDialogLayer(Qt.createComponent('org.kde.neochat', 'ReasonDialog'), {
title: i18nc("@title:dialog", "Remove Messages"),
placeholder: i18nc("@info:placeholder", "Optionally give a reason for removing this user's recent messages"),
placeholder: i18nc("@info:placeholder", "Reason for removing this user's recent messages"),
actionText: i18nc("@action:button 'Remove' as in 'Remove these messages'", "Remove"),
icon: "delete",
reporting: false,
connection: root.connection,
icon: "delete"
}, {
title: i18nc("@title", "Remove Messages"),
width: Kirigami.Units.gridUnit * 25
@@ -334,7 +311,7 @@ Kirigami.Dialog {
}
Kirigami.Heading {
text: i18nc("@title Role such as 'Admin' or 'Moderator' for this user", "Power Level")
text: i18nc("@title Role such as 'Admin' or 'Moderator' for this user", "Role")
level: 2
visible: root.isRoomProfile
@@ -352,12 +329,7 @@ Kirigami.Dialog {
}
QQC2.Button {
visible: {
if (root.room) {
return root.room.canSendState("m.room.power_levels") && !(root.room.roomCreatorHasUltimatePowerLevel() && root.room.isCreator(root.user.id));
}
return false;
}
visible: root.room.canSendState("m.room.power_levels") && !(root.room.roomCreatorHasUltimatePowerLevel() && root.room.isCreator(root.user.id))
text: i18nc("@action:button Set the power level (such as 'Admin') for this user", "Set Power Level")
icon.name: "document-edit-symbolic"
display: QQC2.AbstractButton.IconOnly
@@ -429,34 +401,5 @@ Kirigami.Dialog {
color: Kirigami.Theme.disabledTextColor
}
}
Kirigami.Heading {
text: i18nc("@title Private note for this user", "Private Note")
level: 4
Layout.topMargin: Kirigami.Units.largeSpacing
}
QQC2.TextArea {
id: noteText
text: root.connection.noteForUser(root.user.id)
textFormat: TextEdit.PlainText
wrapMode: TextEdit.Wrap
placeholderText: i18nc("@info:placeholder", "Only visible to you")
onTextEdited: editTimer.restart()
Layout.fillWidth: true
Layout.topMargin: Kirigami.Units.smallSpacing
// Prevent unnecessary edits by waiting 1 second
Timer {
id: editTimer
interval: 1000
onTriggered: root.connection.setNoteForUser(root.user.id, noteText.text)
}
}
}
}

View File

@@ -29,28 +29,6 @@
#include <KIO/OpenUrlJob>
#endif
/**
* @brief Stops RoomManager from updating the last room and space config.
*/
class LastRoomBlocker
{
public:
explicit LastRoomBlocker(RoomManager *manager)
: m_manager(manager)
{
Q_ASSERT(manager);
m_manager->m_dontUpdateLastRoom = true;
}
~LastRoomBlocker()
{
m_manager->m_dontUpdateLastRoom = false;
}
private:
RoomManager *m_manager;
};
RoomManager::RoomManager(QObject *parent)
: QObject(parent)
, m_config(KSharedConfig::openStateConfig())
@@ -304,22 +282,26 @@ void RoomManager::viewEventSource(const QString &eventId)
Q_EMIT showEventSource(eventId);
}
void RoomManager::viewEventMenu(QObject *parent, const RoomEvent *event, NeoChatRoom *room, const QString &selectedText, const QString &hoveredLink)
void RoomManager::viewEventMenu(const QString &eventId, NeoChatRoom *room, const QString &selectedText, const QString &hoveredLink)
{
if (!event) {
qWarning() << "Tried to open event menu with empty event";
if (eventId.isEmpty()) {
qWarning() << "Tried to open event menu with empty event id";
return;
}
Q_EMIT showDelegateMenu(parent,
room,
event->id(),
room->qmlSafeMember(event->senderId()),
MessageComponentType::typeForEvent(*event),
EventHandler::plainBody(room, event),
EventHandler::richBody(room, event),
EventHandler::mediaInfo(room, event)["mimeType"_L1].toString(),
room->fileTransferInfo(event->id()),
const auto it = room->findInTimeline(eventId);
if (it == room->historyEdge()) {
// This is probably a pending event
return;
}
const auto &event = **it;
Q_EMIT showDelegateMenu(eventId,
room->qmlSafeMember(event.senderId()),
MessageComponentType::typeForEvent(event),
EventHandler::plainBody(room, &event),
EventHandler::richBody(room, &event),
EventHandler::mediaInfo(room, &event)["mimeType"_L1].toString(),
room->fileTransferInfo(eventId),
selectedText,
hoveredLink);
}
@@ -342,6 +324,17 @@ void RoomManager::loadInitialRoom()
resolveResource(m_arg);
}
if (m_isMobile) {
QString lastSpace = m_lastRoomConfig.readEntry(u"lastSpace"_s, QString());
// We can't have empty keys in KConfig, so we stored it as "Home"
if (lastSpace == u"Home"_s) {
lastSpace.clear();
}
setCurrentSpace(lastSpace, false);
// We don't want to open a room on startup on mobile
return;
}
if (m_currentRoom) {
// we opened a room with the arg parsing already
return;
@@ -354,14 +347,16 @@ void RoomManager::loadInitialRoom()
void RoomManager::openRoomForActiveConnection()
{
Q_ASSERT(m_connection);
if (!m_connection) {
setCurrentRoom({});
setCurrentSpace({}, false);
return;
}
auto lastSpace = m_lastRoomConfig.readEntry(u"lastSpace"_s, QString());
if (lastSpace == u"Home"_s) {
lastSpace.clear();
}
// We don't want to open a room on startup on mobile
setCurrentSpace(lastSpace, !m_isMobile);
setCurrentSpace(lastSpace, true);
}
UriResolveResult RoomManager::visitUser(User *user, const QString &action)
@@ -518,7 +513,7 @@ void RoomManager::setConnection(NeoChatConnection *connection)
Q_EMIT connectionChanged();
}
void RoomManager::setCurrentSpace(const QString &spaceId, bool goToLastUsedRoom)
void RoomManager::setCurrentSpace(const QString &spaceId, bool setRoom)
{
m_currentSpaceId = spaceId;
@@ -538,26 +533,25 @@ void RoomManager::setCurrentSpace(const QString &spaceId, bool goToLastUsedRoom)
m_lastRoomConfig.writeEntry(u"lastSpace"_s, spaceId.isEmpty() ? u"Home"_s : spaceId);
}
// If we requested to change to the last opened room, do so:
if (goToLastUsedRoom) {
// We don't want to needlessly update the last room config here, that should only be done during explicit user action.
LastRoomBlocker blocker(this);
// We can't have empty keys in KConfig, so it's stored as "Home":
if (const auto &lastRoom = m_lastRoomConfig.readEntry(spaceId.isEmpty() ? u"Home"_s : spaceId, QString()); !lastRoom.isEmpty()) {
resolveResource(lastRoom, "no_join"_L1);
return;
}
// If no last room was opened, go to the space home:
if (!spaceId.isEmpty() && spaceId != u"DM"_s) {
resolveResource(spaceId, "no_join"_L1);
return;
}
// Fallback to no room opened:
setCurrentRoom({});
if (!setRoom) {
return;
}
// We intentionally don't want to open the last room on mobile
if (m_isMobile) {
return;
}
// We can't have empty keys in KConfig, so it's stored as "Home"
if (const auto &lastRoom = m_lastRoomConfig.readEntry(spaceId.isEmpty() ? u"Home"_s : spaceId, QString()); !lastRoom.isEmpty()) {
resolveResource(lastRoom, "no_join"_L1);
return;
}
if (!spaceId.isEmpty() && spaceId != u"DM"_s) {
resolveResource(spaceId, "no_join"_L1);
return;
}
setCurrentRoom({});
}
QString RoomManager::findSpaceIdForCurrentRoom() const
@@ -617,23 +611,21 @@ void RoomManager::setCurrentRoom(const QString &roomId)
Q_EMIT currentRoomChanged();
if (!m_dontUpdateLastRoom) {
if (roomId.isEmpty()) {
m_lastRoomConfig.deleteEntry(m_currentSpaceId);
return;
}
if (roomId.isEmpty()) {
m_lastRoomConfig.deleteEntry(m_currentSpaceId);
return;
}
const auto spaceIdForRoom = findSpaceIdForCurrentRoom();
// We can't have empty keys in KConfig, so name it "Home"
if (spaceIdForRoom.isEmpty()) {
m_lastRoomConfig.writeEntry(u"Home"_s, roomId);
} else {
m_lastRoomConfig.writeEntry(spaceIdForRoom, roomId);
}
const auto spaceIdForRoom = findSpaceIdForCurrentRoom();
// We can't have empty keys in KConfig, so name it "Home"
if (spaceIdForRoom.isEmpty()) {
m_lastRoomConfig.writeEntry(u"Home"_s, roomId);
} else {
m_lastRoomConfig.writeEntry(spaceIdForRoom, roomId);
}
if (m_currentSpaceId != spaceIdForRoom) {
setCurrentSpace(spaceIdForRoom, false);
}
if (m_currentSpaceId != spaceIdForRoom) {
setCurrentSpace(spaceIdForRoom, false);
}
}

View File

@@ -233,8 +233,7 @@ public:
/**
* @brief Show a context menu for the given event.
*/
Q_INVOKABLE void
viewEventMenu(QObject *parent, const RoomEvent *event, NeoChatRoom *room, const QString &selectedText = {}, const QString &hoveredLink = {});
Q_INVOKABLE void viewEventMenu(const QString &eventId, NeoChatRoom *room, const QString &selectedText = {}, const QString &hoveredLink = {});
/**
* @brief Set a URL to be loaded as the initial room.
@@ -307,9 +306,7 @@ Q_SIGNALS:
/**
* @brief Request to show a menu for the given event.
*/
void showDelegateMenu(QObject *parent,
NeoChatRoom *room,
const QString &eventId,
void showDelegateMenu(const QString &eventId,
const NeochatRoomMember *author,
MessageComponentType::Type messageComponentType,
const QString &plainText,
@@ -340,11 +337,6 @@ Q_SIGNALS:
void currentSpaceChanged();
protected:
bool m_dontUpdateLastRoom = false; // Don't set directly, use LastRoomBlocker.
friend class LastRoomBlocker;
private:
bool m_isMobile = false;
@@ -390,13 +382,8 @@ private:
*/
QString findSpaceIdForCurrentRoom() const;
/**
* @brief Sets the current space.
*
* @param spaceId The ID of the space, "DM" for direct messages or an empty string for Home.
* @param goToLastUsedRoom If true, we will navigate to the last opened room in this space.
*/
void setCurrentSpace(const QString &spaceId, bool goToLastUsedRoom = true);
// Space ID, "DM", or empty string
void setCurrentSpace(const QString &spaceId, bool setRoom = true);
/**
* @brief Resolve a user URI.

View File

@@ -1,66 +0,0 @@
// SPDX-FileCopyrightText: 2026 Joshua Goins <josh@redstrate.com>
// SPDX-License-Identifier: GPL-3.0-only
#include "supportcontroller.h"
#include <Quotient/csapi/support.h>
#include <QDebug>
using namespace Quotient;
void SupportController::setConnection(NeoChatConnection *connection)
{
if (m_connection != connection) {
m_connection = connection;
Q_EMIT connectionChanged();
load();
}
}
NeoChatConnection *SupportController::connection() const
{
return m_connection;
}
QString SupportController::supportPage() const
{
return m_supportPage;
}
QList<SupportContact> SupportController::contacts() const
{
return m_contacts;
}
void SupportController::load()
{
if (!m_connection) {
qWarning() << "Tried to load support information without a valid connection?";
return;
}
m_connection->callApi<GetWellknownSupportJob>()
.onResult([this](const auto &job) {
m_supportPage = job->supportPage();
m_contacts.reserve(job->contacts().size());
for (const auto &contact : job->contacts()) {
m_contacts.push_back(SupportContact{
.role = contact.role,
.matrixId = contact.matrixId,
.emailAddress = contact.emailAddress,
});
}
Q_EMIT loaded();
})
.onFailure([this](const auto &job) {
Q_UNUSED(job)
// Just do nothing, our properties will be empty.
Q_EMIT loaded();
});
}
#include "moc_supportcontroller.cpp"

View File

@@ -1,48 +0,0 @@
// SPDX-FileCopyrightText: 2026 Joshua Goins <josh@redstrate.com>
// SPDX-License-Identifier: GPL-3.0-only
#pragma once
#include "neochatconnection.h"
class SupportContact
{
Q_GADGET
Q_PROPERTY(QString role MEMBER role)
Q_PROPERTY(QString matrixId MEMBER matrixId)
Q_PROPERTY(QString emailAddress MEMBER emailAddress)
public:
QString role;
QString matrixId;
QString emailAddress;
};
class SupportController : public QObject
{
Q_OBJECT
QML_ELEMENT
Q_PROPERTY(NeoChatConnection *connection READ connection WRITE setConnection NOTIFY connectionChanged REQUIRED)
Q_PROPERTY(QString supportPage READ supportPage NOTIFY loaded)
Q_PROPERTY(QList<SupportContact> contacts READ contacts NOTIFY loaded)
public:
void setConnection(NeoChatConnection *connection);
NeoChatConnection *connection() const;
QString supportPage() const;
QList<SupportContact> contacts() const;
Q_SIGNALS:
void connectionChanged();
void loaded();
private:
void load();
QPointer<NeoChatConnection> m_connection = nullptr;
QList<SupportContact> m_contacts;
QString m_supportPage;
};

View File

@@ -65,7 +65,7 @@ QQC2.Popup {
padding: 2
implicitHeight: Kirigami.Units.gridUnit * 20 + 2 * padding
width: Math.min(contentItem.categoryIconSize * 11 + 2 * padding, QQC2.ApplicationWindow.window?.width)
width: Math.min(contentItem.categoryIconSize * 11 + 2 * padding, QQC2.ApplicationWindow.window.width)
contentItem: EmojiPicker {
id: emojiPicker
height: 400

View File

@@ -150,12 +150,7 @@ Quotient::RoomMember ChatBarCache::relationAuthor() const
if (m_relationId.isEmpty()) {
return room->member(QString());
}
const auto [event, _] = room->getEvent(m_relationId);
if (event != nullptr) {
return room->member(event->senderId());
}
qWarning() << "Failed to find relation" << m_relationId << "in timeline?";
return room->member(QString());
return room->member((*room->findInTimeline(m_relationId))->senderId());
}
bool ChatBarCache::relationAuthorIsPresent() const
@@ -178,8 +173,8 @@ QString ChatBarCache::relationMessage() const
return {};
}
if (auto [event, _] = room->getEvent(m_relationId); event != nullptr) {
return EventHandler::markdownBody(event);
if (auto event = room->findInTimeline(m_relationId); event != room->historyEdge()) {
return EventHandler::markdownBody(&**event);
}
return {};
}
@@ -285,6 +280,11 @@ void ChatBarCache::postMessage()
return;
}
const auto replyIt = room->findInTimeline(replyId());
if (replyIt == room->historyEdge()) {
isReply = false;
}
auto content = std::make_unique<Quotient::EventContent::TextContent>(sendText, u"text/html"_s);
room->post<Quotient::RoomMessageEvent>(text(), *std::get<std::optional<Quotient::RoomMessageEvent::MsgType>>(result), std::move(content), relatesTo);

View File

@@ -619,21 +619,4 @@ bool NeoChatConnection::supportsMatrixSpecVersion(const QString &version)
return supportedMatrixSpecVersions().contains(version);
}
QString NeoChatConnection::noteForUser(const QString &userId)
{
const auto object = accountDataJson(QStringLiteral("org.kde.neochat.user_note"));
return object[userId].toString();
}
void NeoChatConnection::setNoteForUser(const QString &userId, const QString &note)
{
auto object = accountDataJson(QStringLiteral("org.kde.neochat.user_note"));
if (note.isEmpty()) {
object.remove(userId);
} else {
object[userId] = note;
}
setAccountData(QStringLiteral("org.kde.neochat.user_note"), object);
}
#include "moc_neochatconnection.cpp"

View File

@@ -234,16 +234,6 @@ public:
*/
Q_INVOKABLE bool supportsMatrixSpecVersion(const QString &version);
/**
* @return The private note for this user, if set.
*/
Q_INVOKABLE QString noteForUser(const QString &userId);
/**
* @brief Sets the private note for this user.
*/
Q_INVOKABLE void setNoteForUser(const QString &userId, const QString &note);
Q_SIGNALS:
void globalUrlPreviewEnabledChanged();
void labelChanged();

View File

@@ -1186,10 +1186,7 @@ void NeoChatRoom::loadPinnedMessage()
connection()->callApi<GetOneRoomEventJob>(id(), mostRecentEventId).then([this](const auto &job) {
auto event = fromJson<event_ptr_tt<RoomEvent>>(job->jsonData());
if (auto encEv = eventCast<EncryptedEvent>(event.get())) {
auto decryptedMessage = decryptMessage(*encEv);
if (decryptedMessage) {
event = std::move(decryptedMessage);
}
event = decryptMessage(*encEv);
}
m_pinnedMessage = EventHandler::richBody(this, event.get());
Q_EMIT pinnedMessageChanged();
@@ -1657,12 +1654,6 @@ void NeoChatRoom::downloadEventFromServer(const QString &eventId)
}
event_ptr_tt<RoomEvent> event = fromJson<event_ptr_tt<RoomEvent>>(job->jsonData());
if (auto encEv = eventCast<EncryptedEvent>(event.get())) {
auto decryptedEvent = decryptMessage(*encEv);
if (decryptedEvent) {
event = std::move(decryptedEvent);
}
}
m_extraEvents.push_back(std::move(event));
Q_EMIT extraEventLoaded(eventId);
},
@@ -1700,11 +1691,6 @@ std::pair<const Quotient::RoomEvent *, bool> NeoChatRoom::getEvent(const QString
return std::make_pair(extraIt != m_extraEvents.end() ? extraIt->get() : nullptr, false);
}
const RoomEvent *NeoChatRoom::findEvent(const QString &eventId) const
{
return getEvent(eventId).first;
}
const RoomEvent *NeoChatRoom::getReplyForEvent(const RoomEvent &event) const
{
#if Quotient_VERSION_MINOR > 9

View File

@@ -538,13 +538,6 @@ public:
*/
std::pair<const Quotient::RoomEvent *, bool> getEvent(const QString &eventId) const;
/**
* @brief Returns the event object with the given ID if available.
*
* This function works identically to getEvent, except this is usable from QML.
*/
Q_INVOKABLE const Quotient::RoomEvent *findEvent(const QString &eventId) const;
/**
* @brief Returns the event that is being replied to. This includes events that were manually loaded using NeoChatRoom::loadReply.
*/

View File

@@ -130,10 +130,7 @@ QQC2.Control {
TapHandler {
acceptedDevices: PointerDevice.TouchScreen
onLongPressed: {
const event = root.Message.room.findEvent(root.eventId);
RoomManager.viewEventMenu(root.QQC2.Overlay.overlay, event, root.Message.room, root.Message.selectedText, root.Message.hoveredLink);
}
onLongPressed: RoomManager.viewEventMenu(root.eventId, root.Message.room, root.Message.selectedText, root.Message.hoveredLink);
}
background: null

View File

@@ -66,10 +66,7 @@ QQC2.Control {
enabled: !quoteText.hoveredLink
acceptedDevices: PointerDevice.TouchScreen
acceptedButtons: Qt.LeftButton
onLongPressed: {
const event = root.Message.room.findEvent(root.eventId);
RoomManager.viewEventMenu(root.QQC2.Overlay.overlay, event, root.Message.room, root.Message.selectedText, root.Message.hoveredLink);
}
onLongPressed: RoomManager.viewEventMenu(root.eventId, root.Message.room, root.Message.selectedText, root.Message.hoveredLink);
}
}

View File

@@ -38,7 +38,7 @@ RowLayout {
Layout.fillHeight: true
implicitWidth: Kirigami.Units.smallSpacing
color: root.replyContentModel.author?.color ?? Kirigami.Theme.highlightColor
color: root.replyContentModel.author.color
radius: Kirigami.Units.cornerRadius
}
ColumnLayout {

View File

@@ -86,12 +86,11 @@ RowLayout {
QtObject {
id: _private
function showMessageMenu(): void {
function showMessageMenu() {
if (!NeoChatConfig.developerTools) {
return;
}
const event = root.Message.room.findEvent(root.modelData.eventId);
RoomManager.viewEventMenu(root.QQC2.Overlay.overlay, event, root.Message.room, root.author);
RoomManager.viewEventMenu(root.modelData.eventId, root.Message.room, root.author, "", "");
}
}
}

View File

@@ -97,18 +97,12 @@ TextEdit {
enabled: !root.hoveredLink
acceptedButtons: Qt.LeftButton
acceptedDevices: PointerDevice.TouchScreen
onLongPressed: {
const event = root.Message.room.findEvent(root.eventId);
RoomManager.viewEventMenu(root.QQC2.Overlay.overlay, event, root.Message.room, root.Message.selectedText, root.Message.hoveredLink);
}
onLongPressed: RoomManager.viewEventMenu(root.eventId, root.Message.room, root.Message.selectedText, root.Message.hoveredLink);
}
TapHandler {
acceptedButtons: Qt.RightButton
acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad | PointerDevice.Stylus
gesturePolicy: TapHandler.WithinBounds
onTapped: {
const event = root.Message.room.findEvent(root.eventId);
RoomManager.viewEventMenu(root.QQC2.Overlay.overlay, event, root.Message.room, root.Message.selectedText, root.Message.hoveredLink);
}
onTapped: RoomManager.viewEventMenu(root.eventId, root.Message.room, root.Message.selectedText, root.Message.hoveredLink);
}
}

View File

@@ -269,8 +269,6 @@ void EventMessageContentModel::resetModel()
updateItineraryModel();
Q_EMIT componentsUpdated();
// We need QML to re-evaluate author (for example, reply colors) if it was previously null.
Q_EMIT authorChanged();
}
void EventMessageContentModel::resetContent(bool isEditing, bool isThreading)

View File

@@ -87,6 +87,7 @@ QDateTime MessageContentModel::time() const
QString MessageContentModel::timeString() const
{
return time().toLocalTime().toString(u"hh:mm"_s);
;
}
QString MessageContentModel::authorId() const

View File

@@ -8,7 +8,6 @@ import org.kde.kirigami as Kirigami
import org.kde.neochat.libneochat
import org.kde.neochat.timeline as Timeline
import org.kde.neochat.settings as Settings
/**
* @brief Page for holding a room drawer component.
@@ -50,7 +49,7 @@ Kirigami.Page {
text: i18nc("@action:button", "Room settings")
icon.name: 'settings-configure-symbolic'
onTriggered: {
Settings.RoomSettingsView.openRoomSettings(root.room, Settings.RoomSettingsView.Room);
RoomSettingsView.openRoomSettings(root.room, RoomSettingsView.Room);
}
}
]

View File

@@ -52,6 +52,7 @@ QQC2.ScrollView {
delegate: Timeline.MessageDelegate {
alwaysFillWidth: true
cardBackground: false
room: root.room
}
}
@@ -60,6 +61,7 @@ QQC2.ScrollView {
delegate: Timeline.MessageDelegate {
alwaysFillWidth: true
cardBackground: false
room: root.room
}
}
}

View File

@@ -3,8 +3,6 @@
import QtQuick
import org.kde.kirigami as Kirigami
import org.kde.neochat.libneochat
import org.kde.neochat.timeline
@@ -47,15 +45,4 @@ SearchPage {
noResultPlaceholderMessage: i18n("No messages found")
listVerticalLayoutDirection: ListView.BottomToTop
Connections {
target: root.room.mainCache
function onRelationIdChanged(oldEventId: string, newEventId: string): void {
// If we start replying/editing an event, we need to close the search dialog so the user can type.
if (newEventId.length > 0) {
root.Kirigami.PageStack.closeDialog();
}
}
}
}

View File

@@ -15,7 +15,9 @@ import org.kde.neochat
RowLayout {
id: root
property var desiredWidth
property bool collapsed: false
required property NeoChatConnection connection
signal search
@@ -24,6 +26,10 @@ RowLayout {
*/
signal textChanged(string newText)
MediaDevices {
id: devices
}
Kirigami.Heading {
Layout.fillWidth: true
visible: !root.collapsed
@@ -45,4 +51,78 @@ RowLayout {
QQC2.ToolTip.text: text
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
}
QQC2.ToolButton {
id: menuButton
property QQC2.Menu menuItem: undefined
function openMenu(): void {
if (!menuItem || !menuItem.visible) {
menuItem = menu.createObject(menuButton) as QQC2.Menu;
menuItem.closed.connect(menuButton.toggle);
menuItem.open();
} else {
menuItem.dismiss()
}
}
Accessible.role: Accessible.ButtonMenu
display: QQC2.AbstractButton.IconOnly
down: pressed || menuItem.visible
text: i18nc("@action:button", "Show Menu")
icon.name: "application-menu-symbolic"
onPressed: openMenu()
Keys.onReturnPressed: openMenu()
Keys.onEnterPressed: openMenu()
Accessible.onPressAction: openMenu()
QQC2.ToolTip.visible: hovered && !menuItem.visible
QQC2.ToolTip.text: text
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
}
Component {
id: menu
QQC2.Menu {
y: menuButton.height
QQC2.MenuItem {
text: i18n("Find your friends")
icon.name: "list-add-user"
onTriggered: (root.Kirigami.PageStack.pageStack as Kirigami.PageRow).pushDialogLayer(Qt.createComponent('org.kde.neochat', 'UserSearchPage'), {
connection: root.connection
}, {
title: i18nc("@title", "Find your friends")
})
}
QQC2.MenuItem {
text: i18n("Create a Room")
icon.name: "system-users-symbolic"
onTriggered: {
(Qt.createComponent('org.kde.neochat', 'CreateRoomDialog').createObject(root, {
connection: root.connection
}) as CreateRoomDialog).open();
}
Kirigami.Action {
shortcut: StandardKey.New
onTriggered: parent.trigger()
}
}
QQC2.MenuItem {
text: i18n("Scan a QR Code")
icon.name: "view-barcode-qr"
visible: devices.videoInputs.length > 0
onTriggered: (root.Kirigami.PageStack.pageStack as Kirigami.PageRow).pushDialogLayer(Qt.createComponent("org.kde.neochat", "QrScannerPage"), {
connection: root.connection
}, {
title: i18nc("@title", "Scan a QR Code")
})
}
}
}
}

View File

@@ -157,11 +157,9 @@ KirigamiComponents.ConvergentContextMenu {
onTriggered: {
let dialog = (root.Kirigami.PageStack.pageStack as Kirigami.PageRow).pushDialogLayer(Qt.createComponent('org.kde.neochat', 'ReasonDialog'), {
title: i18nc("@title:dialog", "Report Room"),
placeholder: i18nc("@info:placeholder", "Optionally give a reason for reporting this room"),
placeholder: i18nc("@info:placeholder", "Reason for reporting this room"),
icon: "dialog-warning-symbolic",
actionText: i18nc("@action:button 'Report' as in 'Report this room to the administrators'", "Report"),
reporting: true,
connection: root.connection,
actionText: i18nc("@action:button 'Report' as in 'Report this room to the administrators'", "Report")
}, {
title: i18nc("@title", "Report Room"),
width: Kirigami.Units.gridUnit * 25

View File

@@ -270,7 +270,9 @@ Kirigami.Page {
Component {
id: exploreComponent
ExploreComponent {
desiredWidth: root.width - Kirigami.Units.largeSpacing
collapsed: root.collapsed
connection: root.connection
onSearch: root.search()

View File

@@ -46,8 +46,6 @@ QQC2.ItemDelegate {
topPadding: 0
bottomPadding: 0
text: root.collapsed ? "" : root.displayName
onClicked: root.treeView.toggleExpanded(row)
}
QQC2.ToolButton {
id: collapseButton

View File

@@ -9,7 +9,6 @@ import QtQuick.Controls as QQC2
import QtQuick.Layouts
import org.kde.kirigami as Kirigami
import org.kde.kirigamiaddons.components as KirigamiComponents
import org.kde.neochat
@@ -159,7 +158,7 @@ QQC2.Control {
width: Math.max(directChatNotificationCountTextMetrics.advanceWidth + Kirigami.Units.smallSpacing * 2, height)
height: Kirigami.Units.iconSizes.smallMedium
text: visible ? directChatButton.countedNotifications : ""
text: visible ? root.connection.directChatNotifications + root.connection.directChatInvites : ""
visible: directChatButton.hasCountableNotifications && RoomManager.currentSpace !== "DM"
color: Kirigami.Theme.textColor
horizontalAlignment: Text.AlignHCenter
@@ -264,46 +263,21 @@ QQC2.Control {
}
AvatarTabButton {
id: createNewButton
Layout.fillWidth: true
Layout.preferredHeight: width - Kirigami.Units.smallSpacing
Layout.maximumHeight: width - Kirigami.Units.smallSpacing
text: i18nc("@action:button Create a new room or space", "Create")
text: i18n("Create a space")
contentItem: Kirigami.Icon {
source: "list-add"
}
activeFocusOnTab: true
down: menu.opened
onSelected: menu.popup(root.QQC2.Overlay.overlay, createNewButton.mapToGlobal(Qt.point(createNewButton.width, 0)))
KirigamiComponents.ConvergentContextMenu {
id: menu
Kirigami.Action {
text: i18nc("@action:button Create a new room", "New Room…")
icon.name: "list-add-symbolic"
onTriggered: {
(Qt.createComponent('org.kde.neochat', 'CreateRoomDialog').createObject(root, {
connection: root.connection
}) as CreateRoomDialog).open();
}
}
Kirigami.Action {
text: i18nc("@action:button Create a new room", "New Space…")
icon.name: "list-add-symbolic"
onTriggered: {
(Qt.createComponent('org.kde.neochat', 'CreateSpaceDialog').createObject(root, {
connection: root.connection
}) as CreateSpaceDialog).open();
}
}
onSelected: {
(Qt.createComponent('org.kde.neochat', 'CreateSpaceDialog').createObject(root, {
connection: root.connection
}) as CreateSpaceDialog).open();
}
}

View File

@@ -51,7 +51,6 @@ ecm_add_qml_module(Settings GENERATE_PLUGIN_SOURCE
RoomProfile.qml
RoomAdvancedPage.qml
KeyboardShortcutsPage.qml
Members.qml
SOURCES
colorschemer.cpp
threepidaddhelper.cpp

View File

@@ -36,10 +36,9 @@ FormCard.FormCardPage {
devicesModel: root.devicesModel
FormCard.FormButtonDelegate {
icon.name: !root.connection.isVerifiedSession ? "security-low" : "security-high"
icon.name: "security-low"
text: i18nc("@action:button", "Verify This Device")
description: !root.connection.isVerifiedSession ? i18nc("@info:description", "This device is marked as insecure until it's verified by another device. It's recommended to verify as soon as possible.")
: i18nc("@info:description", "This device is marked as secure.")
description: i18nc("@info:description", "This device is marked as insecure until it's verified by another device. It's recommended to verify as soon as possible.")
visible: !root.connection.isVerifiedSession || NeoChatConfig.alwaysVerifyDevice
onClicked: {
root.connection.startSelfVerification();

View File

@@ -1,247 +0,0 @@
// SPDX-FileCopyrightText: 2022 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 ComponentBehavior: Bound
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.kirigamiaddons.delegates as Delegates
import org.kde.kirigamiaddons.labs.components as KirigamiComponents
import org.kde.kitemmodels
import org.kde.neochat
FormCard.FormCardPage {
id: root
property NeoChatRoom room
title: i18nc("@title:window", "Members")
readonly property bool loading: permissions.count === 0 && !root.room.roomCreatorHasUltimatePowerLevel()
readonly property PowerLevelModel powerLevelModel: PowerLevelModel {
showMute: false
}
FormCard.FormHeader {
title: i18nc("@title", "Privileged Members")
visible: !root.loading
}
FormCard.FormCard {
visible: !root.loading
FormCard.AbstractFormDelegate {
id: userListSearchCard
visible: root.room.canSendState("m.room.power_levels")
contentItem: Kirigami.SearchField {
id: userListSearchField
autoAccept: false
Layout.fillWidth: true
Keys.onUpPressed: userListView.decrementCurrentIndex()
Keys.onDownPressed: userListView.incrementCurrentIndex()
onAccepted: (userListView.itemAtIndex(userListView.currentIndex) as Delegates.RoundedItemDelegate).action.trigger()
}
QQC2.Popup {
id: userListSearchPopup
x: userListSearchField.x
y: userListSearchField.y - height
width: userListSearchField.width
height: {
let maxHeight = userListSearchField.mapToGlobal(userListSearchField.x, userListSearchField.y).y - Kirigami.Units.largeSpacing * 3;
let minHeight = Kirigami.Units.gridUnit * 2 + userListSearchPopup.padding * 2;
let filterContentHeight = userListView.contentHeight + userListSearchPopup.padding * 2;
return Math.max(Math.min(filterContentHeight, maxHeight), minHeight);
}
padding: Kirigami.Units.smallSpacing
leftPadding: Kirigami.Units.smallSpacing / 2
rightPadding: Kirigami.Units.smallSpacing / 2
modal: false
onClosed: userListSearchField.text = ""
background: Kirigami.ShadowedRectangle {
property color borderColor: Kirigami.Theme.textColor
Kirigami.Theme.colorSet: Kirigami.Theme.View
Kirigami.Theme.inherit: false
radius: Kirigami.Units.cornerRadius
color: Kirigami.Theme.backgroundColor
border {
color: Qt.rgba(borderColor.r, borderColor.g, borderColor.b, 0.3)
width: 1
}
shadow {
xOffset: 0
yOffset: 4
color: Qt.rgba(0, 0, 0, 0.3)
size: 8
}
}
contentItem: QQC2.ScrollView {
// HACK: Hide unnecessary horizontal scrollbar (https://bugreports.qt.io/browse/QTBUG-83890)
QQC2.ScrollBar.horizontal.policy: QQC2.ScrollBar.AlwaysOff
ListView {
id: userListView
clip: true
model: UserFilterModel {
id: userListFilterModel
sourceModel: RoomManager.userListModel
filterText: userListSearchField.text
onFilterTextChanged: {
if (filterText.length > 0 && !userListSearchPopup.visible) {
userListSearchPopup.open();
} else if (filterText.length <= 0 && userListSearchPopup.visible) {
userListSearchPopup.close();
}
}
}
delegate: Delegates.RoundedItemDelegate {
id: userListItem
required property string userId
required property url avatar
required property string name
required property int powerLevel
required property string powerLevelString
text: name
contentItem: RowLayout {
KirigamiComponents.Avatar {
Layout.preferredWidth: Kirigami.Units.iconSizes.medium
Layout.preferredHeight: Kirigami.Units.iconSizes.medium
source: userListItem.avatar
name: userListItem.name
}
Delegates.SubtitleContentItem {
itemDelegate: userListItem
subtitle: userListItem.userId
labelItem.textFormat: Text.PlainText
subtitleItem.textFormat: Text.PlainText
Layout.fillWidth: true
}
QQC2.Label {
visible: userListItem.powerLevel > 0
text: userListItem.powerLevelString
color: Kirigami.Theme.disabledTextColor
textFormat: Text.PlainText
wrapMode: Text.NoWrap
}
}
onClicked: {
userListSearchPopup.close();
(powerLevelDialog.createObject(root.QQC2.Overlay.overlay, {
room: root.room,
userId: userListItem.userId,
powerLevel: userListItem.powerLevel
}) as PowerLevelDialog).open();
}
Component {
id: powerLevelDialog
PowerLevelDialog {}
}
}
QQC2.Label {
text: i18nc("@info", "No users found.")
visible: userListView.count === 0
anchors {
left: parent.left
leftMargin: Kirigami.Units.mediumSpacing
verticalCenter: parent.verticalCenter
}
}
}
}
}
}
FormCard.FormDelegateSeparator {
above: userListSearchCard
}
Repeater {
id: permissions
model: KSortFilterProxyModel {
sourceModel: RoomManager.userListModel
sortRoleName: "powerLevel"
sortOrder: Qt.DescendingOrder
filterRowCallback: function (source_row, source_parent) {
let powerLevelRole = sourceModel.data(sourceModel.index(source_row, 0, source_parent), UserListModel.PowerLevelRole);
return powerLevelRole != 0;
}
}
delegate: FormCard.FormTextDelegate {
id: privilegedUserDelegate
required property string userId
required property string name
required property int powerLevel
required property string powerLevelString
required property bool isCreator
text: name
textItem.textFormat: Text.PlainText
description: userId
contentItem.children: RowLayout {
spacing: Kirigami.Units.largeSpacing
QQC2.Label {
id: powerLevelLabel
text: privilegedUserDelegate.powerLevelString
visible: (!root.room.canSendState("m.room.power_levels") || (root.room.memberEffectivePowerLevel(root.room.localMember.id) <= privilegedUserDelegate.powerLevel && privilegedUserDelegate.userId != root.room.localMember.id)) || privilegedUserDelegate.isCreator
color: Kirigami.Theme.disabledTextColor
}
QQC2.ComboBox {
focusPolicy: Qt.NoFocus // provided by parent
model: PowerLevelModel {}
textRole: "name"
valueRole: "value"
visible: !powerLevelLabel.visible
Component.onCompleted: {
let index = indexOfValue(privilegedUserDelegate.powerLevel)
if (index === -1) {
displayText = privilegedUserDelegate.powerLevelString;
} else {
currentIndex = index;
}
}
onActivated: {
root.room.setUserPowerLevel(privilegedUserDelegate.userId, currentValue);
}
}
}
}
}
}
Item {
visible: root.loading
Layout.fillWidth: true
implicitHeight: root.height * 0.9
Kirigami.LoadingPlaceholder {
anchors.centerIn: parent
text: i18nc("@placeholder", "Loading…")
}
}
}

View File

@@ -85,19 +85,6 @@ FormCard.FormCardPage {
title: i18nc("@title:group", "Encryption")
}
FormCard.FormCard {
FormCard.FormButtonDelegate {
id: secretBackupDelegate
text: i18nc("@action:inmenu", "Manage Secret Backup")
description: i18nc("@info", "Import or unlock encryption keys from other devices.")
icon.name: "unlock"
onClicked: root.QQC2.ApplicationWindow.window.pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat', 'UnlockSSSSDialog'), {}, {
title: i18nc("@title:window", "Manage Secret Backup")
})
}
FormCard.FormDelegateSeparator {
above: secretBackupDelegate
below: importKeysDelegate
}
FormCard.FormButtonDelegate {
id: importKeysDelegate
text: i18nc("@action:button", "Import Keys")

View File

@@ -33,10 +33,207 @@ FormCard.FormCardPage {
}
FormCard.FormHeader {
title: i18nc("@title", "Power Levels")
title: i18nc("@title", "Privileged Users")
visible: !root.loading
}
FormCard.FormCard {
enabled: root.room.canSendState("m.room.power_levels")
visible: !root.loading
Repeater {
id: permissions
model: KSortFilterProxyModel {
sourceModel: RoomManager.userListModel
sortRoleName: "powerLevel"
sortOrder: Qt.DescendingOrder
filterRowCallback: function (source_row, source_parent) {
let powerLevelRole = sourceModel.data(sourceModel.index(source_row, 0, source_parent), UserListModel.PowerLevelRole);
return powerLevelRole != 0;
}
}
delegate: FormCard.FormTextDelegate {
id: privilegedUserDelegate
required property string userId
required property string name
required property int powerLevel
required property string powerLevelString
required property bool isCreator
text: name
textItem.textFormat: Text.PlainText
description: userId
contentItem.children: RowLayout {
spacing: Kirigami.Units.largeSpacing
QQC2.Label {
id: powerLevelLabel
text: privilegedUserDelegate.powerLevelString
visible: (!root.room.canSendState("m.room.power_levels") || (root.room.memberEffectivePowerLevel(root.room.localMember.id) <= privilegedUserDelegate.powerLevel && privilegedUserDelegate.userId != root.room.localMember.id)) || privilegedUserDelegate.isCreator
color: Kirigami.Theme.disabledTextColor
}
QQC2.ComboBox {
focusPolicy: Qt.NoFocus // provided by parent
model: PowerLevelModel {}
textRole: "name"
valueRole: "value"
visible: !powerLevelLabel.visible
Component.onCompleted: {
let index = indexOfValue(privilegedUserDelegate.powerLevel)
if (index === -1) {
displayText = privilegedUserDelegate.powerLevelString;
} else {
currentIndex = index;
}
}
onActivated: {
root.room.setUserPowerLevel(privilegedUserDelegate.userId, currentValue);
}
}
}
}
}
FormCard.FormDelegateSeparator {
below: userListSearchCard
}
FormCard.AbstractFormDelegate {
id: userListSearchCard
visible: root.room.canSendState("m.room.power_levels")
contentItem: Kirigami.SearchField {
id: userListSearchField
autoAccept: false
Layout.fillWidth: true
Keys.onUpPressed: userListView.decrementCurrentIndex()
Keys.onDownPressed: userListView.incrementCurrentIndex()
onAccepted: (userListView.itemAtIndex(userListView.currentIndex) as Delegates.RoundedItemDelegate).action.trigger()
}
QQC2.Popup {
id: userListSearchPopup
x: userListSearchField.x
y: userListSearchField.y - height
width: userListSearchField.width
height: {
let maxHeight = userListSearchField.mapToGlobal(userListSearchField.x, userListSearchField.y).y - Kirigami.Units.largeSpacing * 3;
let minHeight = Kirigami.Units.gridUnit * 2 + userListSearchPopup.padding * 2;
let filterContentHeight = userListView.contentHeight + userListSearchPopup.padding * 2;
return Math.max(Math.min(filterContentHeight, maxHeight), minHeight);
}
padding: Kirigami.Units.smallSpacing
leftPadding: Kirigami.Units.smallSpacing / 2
rightPadding: Kirigami.Units.smallSpacing / 2
modal: false
onClosed: userListSearchField.text = ""
background: Kirigami.ShadowedRectangle {
property color borderColor: Kirigami.Theme.textColor
Kirigami.Theme.colorSet: Kirigami.Theme.View
Kirigami.Theme.inherit: false
radius: Kirigami.Units.cornerRadius
color: Kirigami.Theme.backgroundColor
border {
color: Qt.rgba(borderColor.r, borderColor.g, borderColor.b, 0.3)
width: 1
}
shadow {
xOffset: 0
yOffset: 4
color: Qt.rgba(0, 0, 0, 0.3)
size: 8
}
}
contentItem: QQC2.ScrollView {
// HACK: Hide unnecessary horizontal scrollbar (https://bugreports.qt.io/browse/QTBUG-83890)
QQC2.ScrollBar.horizontal.policy: QQC2.ScrollBar.AlwaysOff
ListView {
id: userListView
clip: true
model: UserFilterModel {
id: userListFilterModel
sourceModel: RoomManager.userListModel
filterText: userListSearchField.text
onFilterTextChanged: {
if (filterText.length > 0 && !userListSearchPopup.visible) {
userListSearchPopup.open();
} else if (filterText.length <= 0 && userListSearchPopup.visible) {
userListSearchPopup.close();
}
}
}
delegate: Delegates.RoundedItemDelegate {
id: userListItem
required property string userId
required property url avatar
required property string name
required property int powerLevel
required property string powerLevelString
text: name
contentItem: RowLayout {
KirigamiComponents.Avatar {
Layout.preferredWidth: Kirigami.Units.iconSizes.medium
Layout.preferredHeight: Kirigami.Units.iconSizes.medium
source: userListItem.avatar
name: userListItem.name
}
Delegates.SubtitleContentItem {
itemDelegate: userListItem
subtitle: userListItem.userId
labelItem.textFormat: Text.PlainText
subtitleItem.textFormat: Text.PlainText
Layout.fillWidth: true
}
QQC2.Label {
visible: userListItem.powerLevel > 0
text: userListItem.powerLevelString
color: Kirigami.Theme.disabledTextColor
textFormat: Text.PlainText
wrapMode: Text.NoWrap
}
}
onClicked: {
userListSearchPopup.close();
(powerLevelDialog.createObject(root.QQC2.Overlay.overlay, {
room: root.room,
userId: userListItem.userId,
powerLevel: userListItem.powerLevel
}) as PowerLevelDialog).open();
}
Component {
id: powerLevelDialog
PowerLevelDialog {}
}
}
}
}
}
}
}
FormCard.FormHeader {
visible: root.room.canSendState("m.room.power_levels")
title: i18nc("@title", "Default permissions")
}
FormCard.FormCard {
visible: root.room.canSendState("m.room.power_levels")
Repeater {
model: KSortFilterProxyModel {
sourceModel: root.permissionsModel
@@ -72,49 +269,11 @@ FormCard.FormCardPage {
}
FormCard.FormHeader {
title: i18nc("@title", "Messages")
visible: root.room.canSendState("m.room.power_levels")
title: i18nc("@title", "Basic permissions")
}
FormCard.FormCard {
enabled: root.room.canSendState("m.room.power_levels")
Repeater {
model: KSortFilterProxyModel {
sourceModel: root.permissionsModel
filterRowCallback: function (source_row, source_parent) {
return sourceModel.data(sourceModel.index(source_row, 0, source_parent), PermissionsModel.IsMessagePermissionRole);
}
}
delegate: FormCard.FormComboBoxDelegate {
required property string name
required property string subtitle
required property string type
required property int level
required property string levelName
text: name
description: subtitle
textRole: "name"
valueRole: "value"
model: root.powerLevelModel
Component.onCompleted: {
let index = indexOfValue(level)
if (index === -1) {
displayText = levelName;
} else {
currentIndex = index;
}
}
onCurrentValueChanged: if (root.room.canSendState("m.room.power_levels")) {
root.permissionsModel.setPowerLevel(type, currentValue);
}
}
}
}
FormCard.FormHeader {
title: i18nc("@title", "Moderation")
}
FormCard.FormCard {
enabled: root.room.canSendState("m.room.power_levels")
visible: root.room.canSendState("m.room.power_levels")
Repeater {
model: KSortFilterProxyModel {
sourceModel: root.permissionsModel
@@ -150,61 +309,18 @@ FormCard.FormCardPage {
}
FormCard.FormHeader {
title: i18nc("@title", "General")
visible: root.room.canSendState("m.room.power_levels")
title: i18nc("@title", "Event permissions")
}
FormCard.FormCard {
enabled: root.room.canSendState("m.room.power_levels")
visible: root.room.canSendState("m.room.power_levels")
Repeater {
model: KSortFilterProxyModel {
sourceModel: root.permissionsModel
filterRowCallback: function (source_row, source_parent) {
return sourceModel.data(sourceModel.index(source_row, 0, source_parent), PermissionsModel.IsGeneralPermissionRole);
}
}
delegate: FormCard.FormComboBoxDelegate {
required property string name
required property string subtitle
required property string type
required property int level
required property string levelName
text: name
description: subtitle
textRole: "name"
valueRole: "value"
model: root.powerLevelModel
Component.onCompleted: {
let index = indexOfValue(level)
if (index === -1) {
displayText = levelName;
} else {
currentIndex = index;
}
}
onCurrentValueChanged: if (root.room.canSendState("m.room.power_levels")) {
root.permissionsModel.setPowerLevel(type, currentValue);
}
}
}
}
FormCard.FormHeader {
title: i18nc("@title", "Other Events")
}
FormCard.FormCard {
enabled: root.room.canSendState("m.room.power_levels")
Repeater {
id: otherEventsRepeater
model: KSortFilterProxyModel {
sourceModel: root.permissionsModel
filterRowCallback: function (source_row, source_parent) {
let isBasicPermissionRole = sourceModel.data(sourceModel.index(source_row, 0, source_parent), PermissionsModel.IsBasicPermissionRole);
let isDefaultValueRole = sourceModel.data(sourceModel.index(source_row, 0, source_parent), PermissionsModel.IsDefaultValueRole);
let isMessagePermissionRole = sourceModel.data(sourceModel.index(source_row, 0, source_parent), PermissionsModel.IsMessagePermissionRole);
let isGeneralPermissionRole = sourceModel.data(sourceModel.index(source_row, 0, source_parent), PermissionsModel.IsGeneralPermissionRole);
return !isBasicPermissionRole && !isDefaultValueRole && !isMessagePermissionRole && !isGeneralPermissionRole;
return !isBasicPermissionRole && !isDefaultValueRole;
}
}
delegate: FormCard.FormComboBoxDelegate {
@@ -232,13 +348,7 @@ FormCard.FormCardPage {
}
}
}
FormCard.FormDelegateSeparator {
below: addNewEventDelegate
visible: otherEventsRepeater.count > 0
}
FormCard.AbstractFormDelegate {
id: addNewEventDelegate
Layout.fillWidth: true
contentItem: RowLayout {

View File

@@ -28,6 +28,7 @@ FormCard.FormCardPage {
description: root.room.id
contentItem.children: QQC2.Button {
visible: roomIdDelegate.hovered
text: i18nc("@action:button", "Copy room ID to clipboard")
icon.name: "edit-copy"
display: QQC2.AbstractButton.IconOnly
@@ -41,19 +42,14 @@ FormCard.FormCardPage {
QQC2.ToolTip.visible: hovered
}
}
FormCard.FormDelegateSeparator {
above: roomIdDelegate
below: roomVersionDelegate
}
FormCard.FormTextDelegate {
id: roomVersionDelegate
text: i18nc("@info:label", "Room Version")
description: root.room.version
contentItem.children: QQC2.Button {
visible: root.room.canSwitchVersions()
enabled: root.room.version < root.room.maxRoomVersion
text: i18nc("@action:button", "Upgrade Room")
text: i18nc("@action:button", "Upgrade Room")
icon.name: "arrow-up-double"
onClicked: {

View File

@@ -55,17 +55,6 @@ KirigamiSettings.ConfigurationView {
};
}
},
KirigamiSettings.ConfigurationModule {
moduleId: "members"
text: i18nc("@title", "Members")
icon.name: "system-users-symbolic"
page: () => Qt.createComponent("org.kde.neochat.settings", "Members")
initialProperties: () => {
return {
room: root._room
};
}
},
KirigamiSettings.ConfigurationModule {
moduleId: "permissions"
text: i18nc("@title", "Permissions")

View File

@@ -50,16 +50,12 @@ static const QStringList knownPermissions = {
u"m.room.server_acl"_s,
u"m.space.child"_s,
u"m.space.parent"_s,
u"org.matrix.msc3672.beacon_info"_s,
u"org.matrix.msc3381.poll.start"_s,
u"org.matrix.msc3381.poll.response"_s,
u"org.matrix.msc3381.poll.end"_s,
};
// Alternate name text for default permissions.
static const QHash<QString, KLazyLocalizedString> permissionNames = {
{UsersDefaultKey, kli18nc("Room permission type", "Default power level")},
{StateDefaultKey, kli18nc("Room permission type", "Default power level to change room state")},
{UsersDefaultKey, kli18nc("Room permission type", "Default user power level")},
{StateDefaultKey, kli18nc("Room permission type", "Default power level to set the room state")},
{EventsDefaultKey, kli18nc("Room permission type", "Default power level to send messages")},
{InviteKey, kli18nc("Room permission type", "Invite users")},
{KickKey, kli18nc("Room permission type", "Kick users")},
@@ -74,58 +70,25 @@ static const QHash<QString, KLazyLocalizedString> permissionNames = {
{u"m.room.topic"_s, kli18nc("Room permission type", "Change the room topic")},
{u"m.room.encryption"_s, kli18nc("Room permission type", "Enable encryption for the room")},
{u"m.room.history_visibility"_s, kli18nc("Room permission type", "Change the room history visibility")},
{u"m.room.pinned_events"_s, kli18nc("Room permission type", "Pin and unpin messages")},
{u"m.room.pinned_events"_s, kli18nc("Room permission type", "Set pinned events")},
{u"m.room.tombstone"_s, kli18nc("Room permission type", "Upgrade the room")},
{u"m.room.server_acl"_s, kli18nc("Room permission type", "Set the room server access control list (ACL)")},
{u"m.space.child"_s, kli18nc("Room permission type", "Set the children of this space")},
{u"m.space.parent"_s, kli18nc("Room permission type", "Set the parent space of this room")},
{u"org.matrix.msc3672.beacon_info"_s, kli18nc("Room permission type", "Send live location updates")},
{u"org.matrix.msc3381.poll.start"_s, kli18nc("Room permission type", "Start polls")},
{u"org.matrix.msc3381.poll.response"_s, kli18nc("Room permission type", "Vote in polls")},
{u"org.matrix.msc3381.poll.end"_s, kli18nc("Room permission type", "Close polls")},
};
// Subtitles for the default values.
static const QHash<QString, KLazyLocalizedString> permissionSubtitles = {
{UsersDefaultKey, kli18nc("Room permission type", "This is the power level for all new users when joining the room.")},
{StateDefaultKey, kli18nc("Room permission type", "This is used for all state-type events that do not have their own entry.")},
{EventsDefaultKey, kli18nc("Room permission type", "This is used for all message-type events that do not have their own entry.")},
{UsersDefaultKey, kli18nc("Room permission type", "This is the power level for all new users when joining the room")},
{StateDefaultKey, kli18nc("Room permission type", "This is used for all state events that do not have their own entry here")},
{EventsDefaultKey, kli18nc("Room permission type", "This is used for all message events that do not have their own entry here")},
};
// Permissions that should use the message event default.
// Permissions that should use the event default.
static const QStringList eventPermissions = {
u"m.room.message"_s,
u"m.reaction"_s,
u"m.room.redaction"_s,
u"org.matrix.msc3381.poll.start"_s,
u"org.matrix.msc3381.poll.response"_s,
u"org.matrix.msc3381.poll.end"_s,
};
// Permissions related to messaging.
static const QStringList messagingPermissions = {
u"m.reaction"_s,
u"m.room.redaction"_s,
u"org.matrix.msc3672.beacon_info"_s,
u"org.matrix.msc3381.poll.start"_s,
u"org.matrix.msc3381.poll.response"_s,
u"org.matrix.msc3381.poll.end"_s,
};
// Permissions related to general room management.
static const QStringList generalPermissions = {
u"m.room.power_levels"_s,
u"m.room.name"_s,
u"m.room.avatar"_s,
u"m.room.canonical_alias"_s,
u"m.room.topic"_s,
u"m.room.encryption"_s,
u"m.room.history_visibility"_s,
u"m.room.pinned_events"_s,
u"m.room.tombstone"_s,
u"m.room.server_acl"_s,
u"m.space.child"_s,
u"m.space.parent"_s,
};
};
@@ -231,12 +194,6 @@ QVariant PermissionsModel::data(const QModelIndex &index, int role) const
if (role == IsBasicPermissionRole) {
return basicPermissions.contains(permission);
}
if (role == IsMessagePermissionRole) {
return messagingPermissions.contains(permission);
}
if (role == IsGeneralPermissionRole) {
return generalPermissions.contains(permission);
}
return {};
}
@@ -256,8 +213,6 @@ QHash<int, QByteArray> PermissionsModel::roleNames() const
roles[LevelNameRole] = "levelName";
roles[IsDefaultValueRole] = "isDefaultValue";
roles[IsBasicPermissionRole] = "isBasicPermission";
roles[IsMessagePermissionRole] = "isMessagePermission";
roles[IsGeneralPermissionRole] = "isGeneralPermission";
return roles;
}

View File

@@ -36,8 +36,6 @@ public:
LevelNameRole, /**< The current power level for the permission as a string. */
IsDefaultValueRole, /**< Whether the permission is a default value, e.g. for users. */
IsBasicPermissionRole, /**< Whether the permission is one of the basic ones, e.g. kick, ban, etc. */
IsMessagePermissionRole, /** Permissions related to messaging. */
IsGeneralPermissionRole, /** Permissions related to general room management. */
};
Q_ENUM(Roles)

View File

@@ -8,24 +8,16 @@ import QtQuick.Layouts
import org.kde.kirigami as Kirigami
import org.kde.kirigamiaddons.labs.components as KirigamiComponents
import org.kde.neochat
RowLayout {
id: root
property var avatarSize: Kirigami.Units.iconSizes.small
property alias model: root.limiterModel.sourceModel
property alias model: avatarFlowRepeater.model
property string toolTipText
property LimiterModel limiterModel: LimiterModel {
maximumCount: 5
}
spacing: -avatarSize / 2
Repeater {
id: avatarFlowRepeater
model: root.limiterModel
delegate: KirigamiComponents.Avatar {
required property string displayName
required property url avatarUrl
@@ -47,11 +39,11 @@ RowLayout {
Layout.preferredHeight: Kirigami.Units.iconSizes.small + Kirigami.Units.smallSpacing
Layout.fillHeight: true
visible: root.limiterModel.extraCount > 0
visible: text !== ""
color: Kirigami.Theme.textColor
horizontalAlignment: Text.AlignHCenter
text: "+ " + root.limiterModel.extraCount
text: root.model?.excessReadMarkersString ?? ""
background: Kirigami.ShadowedRectangle {
color: Kirigami.Theme.backgroundColor

View File

@@ -132,12 +132,14 @@ KirigamiComponents.ConvergentContextMenu {
root.room.toggleReaction(root.eventId, emoji);
root.close();
});
dialog.closed.connect(() => {
root.close();
});
dialog.open();
return;
}
root.room.toggleReaction(root.eventId, modelData);
root.close();
}
}
}
@@ -159,7 +161,7 @@ KirigamiComponents.ConvergentContextMenu {
Kirigami.Action {
id: replyAction
visible: !root.room.readOnly && (root.messageComponentType !== MessageComponentType.Other || NeoChatConfig.relateAnyEvent)
visible: root.messageComponentType !== MessageComponentType.Other || NeoChatConfig.relateAnyEvent
text: i18nc("@action:inmenu", "Reply")
icon.name: "mail-replied-symbolic"
onTriggered: {
@@ -171,7 +173,7 @@ KirigamiComponents.ConvergentContextMenu {
Kirigami.Action {
id: replyThreadAction
visible: (root.messageComponentType !== MessageComponentType.Other || NeoChatConfig.relateAnyEvent) && NeoChatConfig.threads
visible: root.messageComponentType !== MessageComponentType.Other || NeoChatConfig.relateAnyEvent
text: i18nc("@action:inmenu", "Reply in Thread")
icon.name: "dialog-messages"
onTriggered: {
@@ -202,13 +204,11 @@ KirigamiComponents.ConvergentContextMenu {
icon.name: "edit-delete-remove"
icon.color: "red"
onTriggered: {
let dialog = ((root.QQC2.ApplicationWindow.window as Kirigami.ApplicationWindow).pageStack as Kirigami.PageRow).pushDialogLayer(Qt.createComponent('org.kde.neochat', 'ReasonDialog'), {
let dialog = (root.Kirigami.PageStack.pageStack as Kirigami.PageRow).pushDialogLayer(Qt.createComponent('org.kde.neochat', 'ReasonDialog'), {
title: i18nc("@title:dialog", "Remove Message"),
placeholder: i18nc("@info:placeholder", "Optionally give a reason for removing this message"),
placeholder: i18nc("@info:placeholder", "Reason for removing this message"),
actionText: i18nc("@action:button 'Remove' as in 'Remove this message'", "Remove"),
icon: "delete",
reporting: false,
connection: root.connection,
icon: "delete"
}, {
title: i18nc("@title:dialog", "Remove Message"),
width: Kirigami.Units.gridUnit * 25
@@ -229,7 +229,7 @@ KirigamiComponents.ConvergentContextMenu {
text: i18nc("@action:inmenu As in 'Forward this message'", "Forward…")
icon.name: "mail-forward-symbolic"
onTriggered: {
let page = ((root.QQC2.ApplicationWindow.window as Kirigami.ApplicationWindow).pageStack as Kirigami.PageRow).pushDialogLayer(Qt.createComponent('org.kde.neochat', 'ChooseRoomDialog'), {
let page = (root.Kirigami.PageStack.pageStack as Kirigami.PageRow).pushDialogLayer(Qt.createComponent('org.kde.neochat', 'ChooseRoomDialog'), {
connection: root.connection
}, {
title: i18nc("@title", "Forward Message"),
@@ -328,13 +328,11 @@ KirigamiComponents.ConvergentContextMenu {
icon.name: "dialog-warning-symbolic"
visible: !root.author.isLocalMember
onTriggered: {
let dialog = ((root.QQC2.ApplicationWindow.window as Kirigami.ApplicationWindow).pageStack as Kirigami.PageRow).pushDialogLayer(Qt.createComponent('org.kde.neochat', 'ReasonDialog'), {
let dialog = (root.Kirigami.PageStack.pageStack as Kirigami.PageRow).pushDialogLayer(Qt.createComponent('org.kde.neochat', 'ReasonDialog'), {
title: i18nc("@title:dialog", "Report Message"),
placeholder: i18nc("@info:placeholder", "Optionally give a reason for reporting this message"),
placeholder: i18nc("@info:placeholder", "Reason for reporting this message"),
icon: "dialog-warning-symbolic",
actionText: i18nc("@action:button 'Report' as in 'Report this event to the administrators'", "Report"),
reporting: true,
connection: root.connection,
actionText: i18nc("@action:button 'Report' as in 'Report this event to the administrators'", "Report")
}, {
title: i18nc("@title", "Report Message"),
width: Kirigami.Units.gridUnit * 25
@@ -373,7 +371,7 @@ KirigamiComponents.ConvergentContextMenu {
Kirigami.Action {
text: i18nc("@action:inmenu", "Configure Web Shortcuts…")
icon.name: "configure"
visible: !Controller.isFlatpak && webShortcutModel.enabled && webShortcutModelAction.visible
visible: !Controller.isFlatpak && webShortcutModel.enabled
onTriggered: webShortcutModel.configureWebShortcuts()
}

View File

@@ -21,12 +21,16 @@ DelegateChooser {
DelegateChoice {
roleValue: DelegateType.State
delegate: StateDelegate {}
delegate: StateDelegate {
room: root.room
}
}
DelegateChoice {
roleValue: DelegateType.Message
delegate: MessageDelegate {}
delegate: MessageDelegate {
room: root.room
}
}
DelegateChoice {
@@ -41,17 +45,23 @@ DelegateChooser {
DelegateChoice {
roleValue: DelegateType.Predecessor
delegate: PredecessorDelegate {}
delegate: PredecessorDelegate {
room: root.room
}
}
DelegateChoice {
roleValue: DelegateType.Successor
delegate: SuccessorDelegate {}
delegate: SuccessorDelegate {
room: root.room
}
}
DelegateChoice {
roleValue: DelegateType.TimelineEnd
delegate: TimelineEndDelegate {}
delegate: TimelineEndDelegate {
room: root.room
}
}
DelegateChoice {
@@ -65,7 +75,9 @@ DelegateChooser {
Component {
id: hiddenDelegate
HiddenDelegate {}
HiddenDelegate {
room: root.room
}
}
Component {
id: emptyDelegate

View File

@@ -92,10 +92,8 @@ TimelineDelegate {
QtObject {
id: _private
function showMessageMenu(): void {
let event = root.Message.room.findEvent(root.eventId);
RoomManager.viewEventMenu(root.QQC2.Overlay.overlay, event, root.room, "");
function showMessageMenu() {
RoomManager.viewEventMenu(root.eventId, root.room, "");
}
}
}

View File

@@ -208,15 +208,6 @@ MessageDelegateBase {
readMarkerComponent: AvatarFlow {
model: root.readMarkers
TapHandler {
onTapped: {
const dialog = Qt.createComponent("org.kde.neochat", "SeenByDialog").createObject(root, {
model: root.readMarkers
}) as SeenByDialog;
dialog.open();
}
}
}
compactBackgroundComponent: Rectangle {
@@ -227,15 +218,13 @@ MessageDelegateBase {
quickActionComponent: QuickActions {
room: root.room
eventId: root.eventId
author: root.author
}
QtObject {
id: _private
function showMessageMenu(): void {
let event = root.ListView.view.model.findEvent(root.eventId);
RoomManager.viewEventMenu(root.QQC2.Overlay.overlay, event, root.room, root.Message.selectedText, root.Message.hoveredLink);
function showMessageMenu() {
RoomManager.viewEventMenu(root.eventId, root.room, root.Message.selectedText, root.Message.hoveredLink);
}
}
}

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