Compare commits
47 Commits
work/carlo
...
work/redst
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8ff23239f7 | ||
|
|
5f20a86b62 | ||
|
|
f2d3c9706e | ||
|
|
39de4d10e4 | ||
|
|
4c37dcf518 | ||
|
|
3c77711417 | ||
|
|
136063bd37 | ||
|
|
730a9e97fd | ||
|
|
d5260376d2 | ||
|
|
dc935e09b7 | ||
|
|
5759f7d82b | ||
|
|
ed4b77c184 | ||
|
|
716ee2e494 | ||
|
|
c15860cac3 | ||
|
|
f5c991c55c | ||
|
|
41609749d8 | ||
|
|
644df80090 | ||
|
|
e3307326ef | ||
|
|
74d4e786d3 | ||
|
|
1e461658b8 | ||
|
|
f305cb849f | ||
|
|
e1bbbfe4fd | ||
|
|
7a2211f8e0 | ||
|
|
4155e9116a | ||
|
|
a989ef42b2 | ||
|
|
2babf44b28 | ||
|
|
89e42dbc53 | ||
|
|
ba57570dbf | ||
|
|
1a500a087b | ||
|
|
e54955ec0c | ||
|
|
8608b3b62e | ||
|
|
fea0cfbf4e | ||
|
|
5b6e5a25e5 | ||
|
|
58b47b8711 | ||
|
|
b45967508c | ||
|
|
2ec1fa92fa | ||
|
|
7602a56594 | ||
|
|
e8da02be7d | ||
|
|
71c84be4b4 | ||
|
|
b684fb451d | ||
|
|
3a416990ca | ||
|
|
9b7d16973d | ||
|
|
dd59e63574 | ||
|
|
7f7e48dfd4 | ||
|
|
7119132e4b | ||
|
|
6001cc6d4f | ||
|
|
989f1ad020 |
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
727
po/ar/neochat.po
727
po/ar/neochat.po
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
670
po/az/neochat.po
670
po/az/neochat.po
File diff suppressed because it is too large
Load Diff
655
po/ca/neochat.po
655
po/ca/neochat.po
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
622
po/cs/neochat.po
622
po/cs/neochat.po
File diff suppressed because it is too large
Load Diff
643
po/da/neochat.po
643
po/da/neochat.po
File diff suppressed because it is too large
Load Diff
716
po/de/neochat.po
716
po/de/neochat.po
File diff suppressed because it is too large
Load Diff
702
po/el/neochat.po
702
po/el/neochat.po
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
698
po/eo/neochat.po
698
po/eo/neochat.po
File diff suppressed because it is too large
Load Diff
635
po/es/neochat.po
635
po/es/neochat.po
File diff suppressed because it is too large
Load Diff
724
po/eu/neochat.po
724
po/eu/neochat.po
File diff suppressed because it is too large
Load Diff
710
po/fi/neochat.po
710
po/fi/neochat.po
File diff suppressed because it is too large
Load Diff
738
po/fr/neochat.po
738
po/fr/neochat.po
File diff suppressed because it is too large
Load Diff
562
po/ga/neochat.po
562
po/ga/neochat.po
File diff suppressed because it is too large
Load Diff
702
po/gl/neochat.po
702
po/gl/neochat.po
File diff suppressed because it is too large
Load Diff
643
po/he/neochat.po
643
po/he/neochat.po
File diff suppressed because it is too large
Load Diff
704
po/hi/neochat.po
704
po/hi/neochat.po
File diff suppressed because it is too large
Load Diff
710
po/hu/neochat.po
710
po/hu/neochat.po
File diff suppressed because it is too large
Load Diff
899
po/ia/neochat.po
899
po/ia/neochat.po
File diff suppressed because it is too large
Load Diff
672
po/id/neochat.po
672
po/id/neochat.po
File diff suppressed because it is too large
Load Diff
662
po/ie/neochat.po
662
po/ie/neochat.po
File diff suppressed because it is too large
Load Diff
732
po/it/neochat.po
732
po/it/neochat.po
File diff suppressed because it is too large
Load Diff
562
po/ja/neochat.po
562
po/ja/neochat.po
File diff suppressed because it is too large
Load Diff
654
po/ka/neochat.po
654
po/ka/neochat.po
File diff suppressed because it is too large
Load Diff
710
po/ko/neochat.po
710
po/ko/neochat.po
File diff suppressed because it is too large
Load Diff
651
po/lt/neochat.po
651
po/lt/neochat.po
File diff suppressed because it is too large
Load Diff
710
po/lv/neochat.po
710
po/lv/neochat.po
File diff suppressed because it is too large
Load Diff
684
po/nl/neochat.po
684
po/nl/neochat.po
File diff suppressed because it is too large
Load Diff
692
po/nn/neochat.po
692
po/nn/neochat.po
File diff suppressed because it is too large
Load Diff
663
po/pa/neochat.po
663
po/pa/neochat.po
File diff suppressed because it is too large
Load Diff
724
po/pl/neochat.po
724
po/pl/neochat.po
File diff suppressed because it is too large
Load Diff
672
po/pt/neochat.po
672
po/pt/neochat.po
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
665
po/ro/neochat.po
665
po/ro/neochat.po
File diff suppressed because it is too large
Load Diff
702
po/ru/neochat.po
702
po/ru/neochat.po
File diff suppressed because it is too large
Load Diff
704
po/sa/neochat.po
704
po/sa/neochat.po
File diff suppressed because it is too large
Load Diff
656
po/sk/neochat.po
656
po/sk/neochat.po
File diff suppressed because it is too large
Load Diff
645
po/sl/neochat.po
645
po/sl/neochat.po
File diff suppressed because it is too large
Load Diff
710
po/sv/neochat.po
710
po/sv/neochat.po
File diff suppressed because it is too large
Load Diff
705
po/ta/neochat.po
705
po/ta/neochat.po
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
645
po/tr/neochat.po
645
po/tr/neochat.po
File diff suppressed because it is too large
Load Diff
649
po/uk/neochat.po
649
po/uk/neochat.po
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
110
snapcraft.yaml
110
snapcraft.yaml
@@ -10,7 +10,7 @@ confinement: strict
|
||||
apps:
|
||||
neochat:
|
||||
extensions:
|
||||
- kde-neon-6@latest/beta
|
||||
- kde-neon-6
|
||||
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,11 +55,53 @@ 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-branch: 0.9.5
|
||||
source-tag: 0.9.2
|
||||
source-depth: 1
|
||||
plugin: cmake
|
||||
build-environment:
|
||||
@@ -68,16 +110,12 @@ 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
|
||||
@@ -85,39 +123,38 @@ parts:
|
||||
|
||||
kquickimageeditor:
|
||||
source: https://invent.kde.org/libraries/kquickimageeditor.git
|
||||
source-tag: 'v0.6.0'
|
||||
source-tag: 'v0.3.0'
|
||||
source-depth: 1
|
||||
plugin: cmake
|
||||
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}
|
||||
build-environment:
|
||||
- PATH: /snap/bin:${PATH}
|
||||
- PYTHONPATH: ${CRAFT_STAGE}/lib/python3.12/site-packages:${CRAFT_STAGE}/usr/lib/python3/dist-packages
|
||||
- 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
|
||||
- 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
|
||||
- -DQT_MAJOR_VERSION=6
|
||||
- -DBUILD_WITH_QT6=ON
|
||||
- -DBUILD_TESTING=OFF
|
||||
- "-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
|
||||
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: *build-environment
|
||||
cmake-parameters: *cmake-parameters
|
||||
prime: *prime
|
||||
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
|
||||
|
||||
neochat:
|
||||
after:
|
||||
- qtkeychain
|
||||
- libquotient
|
||||
- kquickimageeditor
|
||||
- kunifiedpush
|
||||
@@ -125,20 +162,24 @@ parts:
|
||||
- usr/share/metainfo/org.kde.neochat.appdata.xml
|
||||
source: .
|
||||
plugin: cmake
|
||||
build-environment: *build-environment
|
||||
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-packages:
|
||||
- cmark
|
||||
- docbook-xml
|
||||
- docbook-xsl
|
||||
- duktape-dev
|
||||
- libcmark-dev
|
||||
- libsqlite3-dev
|
||||
- libvulkan-dev
|
||||
- libxkbcommon-dev
|
||||
- libicu-dev
|
||||
- libpulse0
|
||||
cmake-parameters: *cmake-parameters
|
||||
prime: *prime
|
||||
cmake-parameters:
|
||||
- -DCMAKE_INSTALL_PREFIX=/usr
|
||||
- -DCMAKE_BUILD_TYPE=Release
|
||||
- -DBUILD_TESTING=OFF
|
||||
prime:
|
||||
- -usr/share/man
|
||||
|
||||
deps:
|
||||
after: [neochat]
|
||||
@@ -157,14 +198,3 @@ 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
|
||||
|
||||
|
||||
@@ -37,6 +37,8 @@ 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
|
||||
@@ -106,6 +108,8 @@ 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
|
||||
@@ -216,6 +220,7 @@ target_link_libraries(neochat PUBLIC
|
||||
KF6::ItemModels
|
||||
KF6::I18nQml
|
||||
KirigamiApp
|
||||
KirigamiAddonsComponents
|
||||
QuotientQt6
|
||||
Login
|
||||
Rooms
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
#include <KNotification>
|
||||
#include <KNotificationPermission>
|
||||
#include <KNotificationReplyAction>
|
||||
#include <KirigamiAddons/Components/NameUtils>
|
||||
|
||||
#include <QPainter>
|
||||
#include <Quotient/accountregistry.h>
|
||||
@@ -144,16 +145,9 @@ 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),
|
||||
sender.displayName(),
|
||||
room->member(sender.id()),
|
||||
body,
|
||||
avatar_image,
|
||||
notification["event"_L1].toObject()["event_id"_L1].toString(),
|
||||
true,
|
||||
pair.first);
|
||||
@@ -195,9 +189,8 @@ bool NotificationsManager::shouldPostNotification(QPointer<NeoChatConnection> co
|
||||
}
|
||||
|
||||
void NotificationsManager::postNotification(NeoChatRoom *room,
|
||||
const QString &sender,
|
||||
const RoomMember &member,
|
||||
const QString &text,
|
||||
const QImage &icon,
|
||||
const QString &replyEventId,
|
||||
bool canReply,
|
||||
qint64 timestamp)
|
||||
@@ -222,11 +215,11 @@ void NotificationsManager::postNotification(NeoChatRoom *room,
|
||||
if (room->isDirectChat()) {
|
||||
entry = text.toHtmlEscaped();
|
||||
} else {
|
||||
entry = i18n("%1: %2", sender, text.toHtmlEscaped());
|
||||
entry = i18n("%1: %2", member.displayName(), text.toHtmlEscaped());
|
||||
}
|
||||
|
||||
notification->setText(entry);
|
||||
notification->setPixmap(createNotificationImage(icon, room));
|
||||
notification->setPixmap(createNotificationImage(member, room));
|
||||
|
||||
auto defaultAction = notification->addDefaultAction(i18n("Open NeoChat in this room"));
|
||||
connect(defaultAction, &KNotificationAction::activated, this, [notification, room]() {
|
||||
@@ -294,18 +287,10 @@ 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(avatar_image, nullptr));
|
||||
notification->setPixmap(createNotificationImage(sender, room));
|
||||
auto defaultAction = notification->addDefaultAction(i18n("Open this invitation in NeoChat"));
|
||||
connect(defaultAction, &KNotificationAction::activated, this, [notification, room]() {
|
||||
if (!room) {
|
||||
@@ -411,41 +396,125 @@ void NotificationsManager::postPushNotification(const QByteArray &message)
|
||||
}
|
||||
}
|
||||
|
||||
QPixmap NotificationsManager::createNotificationImage(const QImage &icon, NeoChatRoom *room)
|
||||
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)
|
||||
{
|
||||
// Handle avatars that are lopsided in one dimension
|
||||
const int biggestDimension = std::max(icon.width(), icon.height());
|
||||
const QRect imageRect{0, 0, biggestDimension, biggestDimension};
|
||||
const int biggestDimension = std::max(senderIcon.width(), senderIcon.height());
|
||||
const QRectF imageRect = QRect{0, 0, biggestDimension, biggestDimension}.toRectF();
|
||||
|
||||
QImage roundedImage(imageRect.size(), QImage::Format_ARGB32);
|
||||
QImage roundedImage(imageRect.size().toSize(), QImage::Format_ARGB32);
|
||||
roundedImage.fill(Qt::transparent);
|
||||
|
||||
QPainter painter(&roundedImage);
|
||||
painter.setRenderHint(QPainter::SmoothPixmapTransform);
|
||||
painter.setPen(Qt::NoPen);
|
||||
|
||||
// Fill background for transparent avatars
|
||||
painter.setBrush(Qt::white);
|
||||
painter.drawRoundedRect(imageRect, imageRect.width(), imageRect.height());
|
||||
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());
|
||||
|
||||
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());
|
||||
}
|
||||
painter.setBrush(senderIcon.scaledToHeight(biggestDimension));
|
||||
painter.drawRoundedRect(imageRect, imageRect.width(), imageRect.height());
|
||||
}
|
||||
|
||||
return QPixmap::fromImage(roundedImage);
|
||||
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;
|
||||
}
|
||||
|
||||
#include "moc_notificationsmanager.cpp"
|
||||
|
||||
@@ -13,6 +13,11 @@
|
||||
#include <Quotient/csapi/notifications.h>
|
||||
#include <Quotient/jobs/basejob.h>
|
||||
|
||||
namespace Quotient
|
||||
{
|
||||
class RoomMember;
|
||||
}
|
||||
|
||||
class NeoChatConnection;
|
||||
class KNotification;
|
||||
class NeoChatRoom;
|
||||
@@ -67,15 +72,24 @@ private:
|
||||
QStringList m_connActiveJob;
|
||||
void startNotificationJob(QPointer<NeoChatConnection> connection);
|
||||
|
||||
QPixmap createNotificationImage(const QImage &icon, NeoChatRoom *room);
|
||||
/**
|
||||
* @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);
|
||||
|
||||
bool shouldPostNotification(QPointer<NeoChatConnection> connection, const QJsonValue ¬ification);
|
||||
void postNotification(NeoChatRoom *room,
|
||||
const QString &sender,
|
||||
const QString &text,
|
||||
const QImage &icon,
|
||||
const QString &replyEventId,
|
||||
bool canReply,
|
||||
qint64 timestamp);
|
||||
void
|
||||
postNotification(NeoChatRoom *room, const Quotient::RoomMember &member, const QString &text, const QString &replyEventId, bool canReply, qint64 timestamp);
|
||||
|
||||
void doPostInviteNotification(QPointer<NeoChatRoom> room);
|
||||
|
||||
@@ -84,6 +98,8 @@ private:
|
||||
|
||||
bool permissionAsked = false;
|
||||
|
||||
static constexpr int avatarDimension = 128;
|
||||
|
||||
private Q_SLOTS:
|
||||
void processNotificationJob(QPointer<NeoChatConnection> connection, Quotient::GetNotificationsJob *job, bool initialization);
|
||||
};
|
||||
|
||||
@@ -5,6 +5,7 @@ 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
|
||||
@@ -18,6 +19,10 @@ 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"
|
||||
@@ -33,12 +38,14 @@ KirigamiComponents.ConvergentContextMenu {
|
||||
}
|
||||
|
||||
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", "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"), {
|
||||
connection: root.connection
|
||||
}) as Kirigami.Dialog).open();
|
||||
}, {
|
||||
title: i18nc("@title", "Scan a QR Code")
|
||||
})
|
||||
}
|
||||
|
||||
Kirigami.Action {
|
||||
@@ -55,14 +62,6 @@ 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"
|
||||
@@ -76,14 +75,6 @@ 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"
|
||||
@@ -103,10 +94,25 @@ KirigamiComponents.ConvergentContextMenu {
|
||||
}
|
||||
|
||||
Kirigami.Action {
|
||||
text: i18nc("@action:inmenu", "Logout…")
|
||||
icon.name: "im-kick-user"
|
||||
onTriggered: (Qt.createComponent("org.kde.neochat", "ConfirmLogoutDialog").createObject(QQC2.Overlay.overlay, {
|
||||
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, {
|
||||
connection: root.connection
|
||||
}) as Kirigami.Dialog).open()
|
||||
}) as Kirigami.Dialog).open();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -154,7 +154,10 @@ Components.AlbumMaximizeComponent {
|
||||
|
||||
onOpened: forceActiveFocus()
|
||||
|
||||
onItemRightClicked: RoomManager.viewEventMenu(root.currentEventId, root.currentRoom)
|
||||
onItemRightClicked: {
|
||||
const event = root.currentRoom.findEvent(root.currentEventId);
|
||||
RoomManager.viewEventMenu(root.QQC2.Overlay.overlay, event, root.currentRoom)
|
||||
}
|
||||
|
||||
onSaveItem: {
|
||||
var dialog = saveAsDialog.createObject(QQC2.Overlay.overlay) as Dialogs.FileDialog;
|
||||
|
||||
@@ -6,6 +6,7 @@ import QtQuick.Controls as QQC2
|
||||
import QtQuick.Layouts
|
||||
|
||||
import org.kde.kirigami as Kirigami
|
||||
import org.kde.neochat
|
||||
|
||||
Kirigami.Page {
|
||||
id: root
|
||||
@@ -13,6 +14,8 @@ 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)
|
||||
|
||||
@@ -21,6 +24,15 @@ 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
|
||||
@@ -31,7 +43,7 @@ Kirigami.Page {
|
||||
Keys.onReturnPressed: event => {
|
||||
if (event.modifiers & Qt.ControlModifier) {
|
||||
root.accepted(reason.text);
|
||||
root.closeDialog();
|
||||
root.Kirigami.PageStack.closeDialog();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,14 +64,14 @@ Kirigami.Page {
|
||||
QQC2.DialogButtonBox.buttonRole: QQC2.DialogButtonBox.AcceptRole
|
||||
onClicked: {
|
||||
root.accepted(reason.text);
|
||||
root.closeDialog();
|
||||
root.Kirigami.PageStack.closeDialog();
|
||||
}
|
||||
}
|
||||
QQC2.Button {
|
||||
icon.name: "dialog-cancel-symbolic"
|
||||
text: i18nc("@action", "Cancel")
|
||||
QQC2.DialogButtonBox.buttonRole: QQC2.DialogButtonBox.RejectRole
|
||||
onClicked: root.closeDialog()
|
||||
onClicked: root.Kirigami.PageStack.closeDialog()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,6 +75,12 @@ 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
|
||||
@@ -113,7 +119,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()
|
||||
}
|
||||
@@ -289,7 +295,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
|
||||
@@ -349,14 +355,17 @@ Kirigami.Page {
|
||||
});
|
||||
}
|
||||
|
||||
function onShowDelegateMenu(eventId: string, author, messageComponentType, plainText: string, richText: string, mimeType: string, progressInfo, isThread: bool, selectedText: string, hoveredLink: string) {
|
||||
(delegateContextMenu.createObject(root, {
|
||||
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,
|
||||
author: author,
|
||||
eventId: eventId,
|
||||
plainText: plainText,
|
||||
mimeType: mimeType,
|
||||
progressInfo: progressInfo,
|
||||
messageComponentType: messageComponentType,
|
||||
selectedText,
|
||||
hoveredLink,
|
||||
}) as DelegateContextMenu).popup();
|
||||
}
|
||||
|
||||
|
||||
90
src/app/qml/SeenByDialog.qml
Normal file
90
src/app/qml/SeenByDialog.qml
Normal file
@@ -0,0 +1,90 @@
|
||||
// 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
121
src/app/qml/SupportDialog.qml
Normal file
121
src/app/qml/SupportDialog.qml
Normal file
@@ -0,0 +1,121 @@
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,7 @@ FormCard.FormCardPage {
|
||||
|
||||
property bool processing: false
|
||||
|
||||
title: i18nc("@title:window", "Load your encrypted messages")
|
||||
title: i18nc("@title:window", "Manage Secret Backup")
|
||||
|
||||
topPadding: Kirigami.Units.gridUnit
|
||||
leftPadding: 0
|
||||
|
||||
@@ -176,9 +176,11 @@ 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", "Reason for reporting this user"),
|
||||
placeholder: i18nc("@info:placeholder", "Optionally give a reason for reporting this user"),
|
||||
icon: "dialog-warning-symbolic",
|
||||
actionText: i18nc("@action:button 'Report' as in 'Report this user to the administrators'", "Report")
|
||||
actionText: i18nc("@action:button 'Report' as in 'Report this user to the administrators'", "Report"),
|
||||
reporting: true,
|
||||
connection: root.connection,
|
||||
}, {
|
||||
title: i18nc("@title", "Report User"),
|
||||
width: Kirigami.Units.gridUnit * 25
|
||||
@@ -232,16 +234,23 @@ Kirigami.Dialog {
|
||||
|
||||
actions: [
|
||||
Kirigami.Action {
|
||||
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)
|
||||
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;
|
||||
}
|
||||
|
||||
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", "Reason for kicking this user"),
|
||||
placeholder: i18nc("@info:placeholder", "Optionally give a reason for kicking this user"),
|
||||
actionText: i18nc("@action:button 'Kick' as in 'Kick this user from the room'", "Kick"),
|
||||
icon: "im-kick-user"
|
||||
icon: "im-kick-user",
|
||||
reporting: false,
|
||||
connection: root.connection,
|
||||
}, {
|
||||
title: i18nc("@title:dialog", "Kick User"),
|
||||
width: Kirigami.Units.gridUnit * 25
|
||||
@@ -253,7 +262,12 @@ Kirigami.Dialog {
|
||||
}
|
||||
},
|
||||
Kirigami.Action {
|
||||
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)
|
||||
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;
|
||||
}
|
||||
|
||||
text: i18nc("@action:button Ban this user from the room", "Ban…")
|
||||
icon.name: "im-ban-user"
|
||||
@@ -261,9 +275,11 @@ 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", "Reason for banning this user"),
|
||||
placeholder: i18nc("@info:placeholder", "Optionally give a reason for banning this user"),
|
||||
actionText: i18nc("@action:button 'Ban' as in 'Ban this user'", "Ban"),
|
||||
icon: "im-ban-user"
|
||||
icon: "im-ban-user",
|
||||
reporting: false,
|
||||
connection: root.connection,
|
||||
}, {
|
||||
title: i18nc("@title:dialog", "Ban User"),
|
||||
width: Kirigami.Units.gridUnit * 25
|
||||
@@ -275,7 +291,12 @@ Kirigami.Dialog {
|
||||
}
|
||||
},
|
||||
Kirigami.Action {
|
||||
visible: !root.isSelf && root.room.canSendState("ban") && root.room.isUserBanned(root.user.id)
|
||||
visible: {
|
||||
if (root.room) {
|
||||
return !root.isSelf && root.room.canSendState("ban") && root.room.isUserBanned(root.user.id);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
text: i18nc("@action:button Unban the user from this room", "Unban")
|
||||
icon.name: "im-irc"
|
||||
@@ -286,7 +307,7 @@ Kirigami.Dialog {
|
||||
}
|
||||
},
|
||||
Kirigami.Action {
|
||||
visible: (root.user.id === root.connection.localUserId || root.room.canSendState("redact"))
|
||||
visible: (root.user.id === root.connection.localUserId || (root.room?.canSendState("redact") ?? false))
|
||||
|
||||
text: i18nc("@action:button Remove messages from the user in this room", "Remove Messages…")
|
||||
icon.name: "delete"
|
||||
@@ -294,9 +315,11 @@ 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", "Reason for removing this user's recent messages"),
|
||||
placeholder: i18nc("@info:placeholder", "Optionally give a reason for removing this user's recent messages"),
|
||||
actionText: i18nc("@action:button 'Remove' as in 'Remove these messages'", "Remove"),
|
||||
icon: "delete"
|
||||
icon: "delete",
|
||||
reporting: false,
|
||||
connection: root.connection,
|
||||
}, {
|
||||
title: i18nc("@title", "Remove Messages"),
|
||||
width: Kirigami.Units.gridUnit * 25
|
||||
@@ -311,7 +334,7 @@ Kirigami.Dialog {
|
||||
}
|
||||
|
||||
Kirigami.Heading {
|
||||
text: i18nc("@title Role such as 'Admin' or 'Moderator' for this user", "Role")
|
||||
text: i18nc("@title Role such as 'Admin' or 'Moderator' for this user", "Power Level")
|
||||
level: 2
|
||||
visible: root.isRoomProfile
|
||||
|
||||
@@ -329,7 +352,12 @@ Kirigami.Dialog {
|
||||
}
|
||||
|
||||
QQC2.Button {
|
||||
visible: root.room.canSendState("m.room.power_levels") && !(root.room.roomCreatorHasUltimatePowerLevel() && root.room.isCreator(root.user.id))
|
||||
visible: {
|
||||
if (root.room) {
|
||||
return root.room.canSendState("m.room.power_levels") && !(root.room.roomCreatorHasUltimatePowerLevel() && root.room.isCreator(root.user.id));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
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
|
||||
@@ -401,5 +429,34 @@ 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,6 +29,28 @@
|
||||
#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())
|
||||
@@ -282,26 +304,22 @@ void RoomManager::viewEventSource(const QString &eventId)
|
||||
Q_EMIT showEventSource(eventId);
|
||||
}
|
||||
|
||||
void RoomManager::viewEventMenu(const QString &eventId, NeoChatRoom *room, const QString &selectedText, const QString &hoveredLink)
|
||||
void RoomManager::viewEventMenu(QObject *parent, const RoomEvent *event, NeoChatRoom *room, const QString &selectedText, const QString &hoveredLink)
|
||||
{
|
||||
if (eventId.isEmpty()) {
|
||||
qWarning() << "Tried to open event menu with empty event id";
|
||||
if (!event) {
|
||||
qWarning() << "Tried to open event menu with empty event";
|
||||
return;
|
||||
}
|
||||
|
||||
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),
|
||||
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()),
|
||||
selectedText,
|
||||
hoveredLink);
|
||||
}
|
||||
@@ -324,17 +342,6 @@ 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;
|
||||
@@ -347,16 +354,14 @@ void RoomManager::loadInitialRoom()
|
||||
|
||||
void RoomManager::openRoomForActiveConnection()
|
||||
{
|
||||
if (!m_connection) {
|
||||
setCurrentRoom({});
|
||||
setCurrentSpace({}, false);
|
||||
return;
|
||||
}
|
||||
Q_ASSERT(m_connection);
|
||||
|
||||
auto lastSpace = m_lastRoomConfig.readEntry(u"lastSpace"_s, QString());
|
||||
if (lastSpace == u"Home"_s) {
|
||||
lastSpace.clear();
|
||||
}
|
||||
setCurrentSpace(lastSpace, true);
|
||||
// We don't want to open a room on startup on mobile
|
||||
setCurrentSpace(lastSpace, !m_isMobile);
|
||||
}
|
||||
|
||||
UriResolveResult RoomManager::visitUser(User *user, const QString &action)
|
||||
@@ -513,7 +518,7 @@ void RoomManager::setConnection(NeoChatConnection *connection)
|
||||
Q_EMIT connectionChanged();
|
||||
}
|
||||
|
||||
void RoomManager::setCurrentSpace(const QString &spaceId, bool setRoom)
|
||||
void RoomManager::setCurrentSpace(const QString &spaceId, bool goToLastUsedRoom)
|
||||
{
|
||||
m_currentSpaceId = spaceId;
|
||||
|
||||
@@ -533,25 +538,26 @@ void RoomManager::setCurrentSpace(const QString &spaceId, bool setRoom)
|
||||
m_lastRoomConfig.writeEntry(u"lastSpace"_s, spaceId.isEmpty() ? u"Home"_s : spaceId);
|
||||
}
|
||||
|
||||
if (!setRoom) {
|
||||
return;
|
||||
}
|
||||
// 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 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;
|
||||
}
|
||||
|
||||
// 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 (!spaceId.isEmpty() && spaceId != u"DM"_s) {
|
||||
resolveResource(spaceId, "no_join"_L1);
|
||||
return;
|
||||
}
|
||||
setCurrentRoom({});
|
||||
}
|
||||
|
||||
QString RoomManager::findSpaceIdForCurrentRoom() const
|
||||
@@ -611,21 +617,23 @@ void RoomManager::setCurrentRoom(const QString &roomId)
|
||||
|
||||
Q_EMIT currentRoomChanged();
|
||||
|
||||
if (roomId.isEmpty()) {
|
||||
m_lastRoomConfig.deleteEntry(m_currentSpaceId);
|
||||
return;
|
||||
}
|
||||
if (!m_dontUpdateLastRoom) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -233,7 +233,8 @@ public:
|
||||
/**
|
||||
* @brief Show a context menu for the given event.
|
||||
*/
|
||||
Q_INVOKABLE void viewEventMenu(const QString &eventId, NeoChatRoom *room, const QString &selectedText = {}, const QString &hoveredLink = {});
|
||||
Q_INVOKABLE void
|
||||
viewEventMenu(QObject *parent, const RoomEvent *event, NeoChatRoom *room, const QString &selectedText = {}, const QString &hoveredLink = {});
|
||||
|
||||
/**
|
||||
* @brief Set a URL to be loaded as the initial room.
|
||||
@@ -306,7 +307,9 @@ Q_SIGNALS:
|
||||
/**
|
||||
* @brief Request to show a menu for the given event.
|
||||
*/
|
||||
void showDelegateMenu(const QString &eventId,
|
||||
void showDelegateMenu(QObject *parent,
|
||||
NeoChatRoom *room,
|
||||
const QString &eventId,
|
||||
const NeochatRoomMember *author,
|
||||
MessageComponentType::Type messageComponentType,
|
||||
const QString &plainText,
|
||||
@@ -337,6 +340,11 @@ Q_SIGNALS:
|
||||
|
||||
void currentSpaceChanged();
|
||||
|
||||
protected:
|
||||
bool m_dontUpdateLastRoom = false; // Don't set directly, use LastRoomBlocker.
|
||||
|
||||
friend class LastRoomBlocker;
|
||||
|
||||
private:
|
||||
bool m_isMobile = false;
|
||||
|
||||
@@ -382,8 +390,13 @@ private:
|
||||
*/
|
||||
QString findSpaceIdForCurrentRoom() const;
|
||||
|
||||
// Space ID, "DM", or empty string
|
||||
void setCurrentSpace(const QString &spaceId, bool setRoom = true);
|
||||
/**
|
||||
* @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);
|
||||
|
||||
/**
|
||||
* @brief Resolve a user URI.
|
||||
|
||||
66
src/app/supportcontroller.cpp
Normal file
66
src/app/supportcontroller.cpp
Normal file
@@ -0,0 +1,66 @@
|
||||
// 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"
|
||||
48
src/app/supportcontroller.h
Normal file
48
src/app/supportcontroller.h
Normal file
@@ -0,0 +1,48 @@
|
||||
// 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;
|
||||
};
|
||||
@@ -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
|
||||
|
||||
@@ -150,7 +150,12 @@ Quotient::RoomMember ChatBarCache::relationAuthor() const
|
||||
if (m_relationId.isEmpty()) {
|
||||
return room->member(QString());
|
||||
}
|
||||
return room->member((*room->findInTimeline(m_relationId))->senderId());
|
||||
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());
|
||||
}
|
||||
|
||||
bool ChatBarCache::relationAuthorIsPresent() const
|
||||
@@ -173,8 +178,8 @@ QString ChatBarCache::relationMessage() const
|
||||
return {};
|
||||
}
|
||||
|
||||
if (auto event = room->findInTimeline(m_relationId); event != room->historyEdge()) {
|
||||
return EventHandler::markdownBody(&**event);
|
||||
if (auto [event, _] = room->getEvent(m_relationId); event != nullptr) {
|
||||
return EventHandler::markdownBody(event);
|
||||
}
|
||||
return {};
|
||||
}
|
||||
@@ -280,11 +285,6 @@ 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);
|
||||
|
||||
@@ -619,4 +619,21 @@ 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 ¬e)
|
||||
{
|
||||
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"
|
||||
|
||||
@@ -234,6 +234,16 @@ 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 ¬e);
|
||||
|
||||
Q_SIGNALS:
|
||||
void globalUrlPreviewEnabledChanged();
|
||||
void labelChanged();
|
||||
|
||||
@@ -1186,7 +1186,10 @@ 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())) {
|
||||
event = decryptMessage(*encEv);
|
||||
auto decryptedMessage = decryptMessage(*encEv);
|
||||
if (decryptedMessage) {
|
||||
event = std::move(decryptedMessage);
|
||||
}
|
||||
}
|
||||
m_pinnedMessage = EventHandler::richBody(this, event.get());
|
||||
Q_EMIT pinnedMessageChanged();
|
||||
@@ -1654,6 +1657,12 @@ 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);
|
||||
},
|
||||
@@ -1691,6 +1700,11 @@ 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
|
||||
|
||||
@@ -538,6 +538,13 @@ 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.
|
||||
*/
|
||||
|
||||
@@ -130,7 +130,10 @@ QQC2.Control {
|
||||
|
||||
TapHandler {
|
||||
acceptedDevices: PointerDevice.TouchScreen
|
||||
onLongPressed: RoomManager.viewEventMenu(root.eventId, root.Message.room, root.Message.selectedText, root.Message.hoveredLink);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
background: null
|
||||
|
||||
@@ -66,7 +66,10 @@ QQC2.Control {
|
||||
enabled: !quoteText.hoveredLink
|
||||
acceptedDevices: PointerDevice.TouchScreen
|
||||
acceptedButtons: Qt.LeftButton
|
||||
onLongPressed: RoomManager.viewEventMenu(root.eventId, root.Message.room, root.Message.selectedText, root.Message.hoveredLink);
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ RowLayout {
|
||||
Layout.fillHeight: true
|
||||
|
||||
implicitWidth: Kirigami.Units.smallSpacing
|
||||
color: root.replyContentModel.author.color
|
||||
color: root.replyContentModel.author?.color ?? Kirigami.Theme.highlightColor
|
||||
radius: Kirigami.Units.cornerRadius
|
||||
}
|
||||
ColumnLayout {
|
||||
|
||||
@@ -86,11 +86,12 @@ RowLayout {
|
||||
QtObject {
|
||||
id: _private
|
||||
|
||||
function showMessageMenu() {
|
||||
function showMessageMenu(): void {
|
||||
if (!NeoChatConfig.developerTools) {
|
||||
return;
|
||||
}
|
||||
RoomManager.viewEventMenu(root.modelData.eventId, root.Message.room, root.author, "", "");
|
||||
const event = root.Message.room.findEvent(root.modelData.eventId);
|
||||
RoomManager.viewEventMenu(root.QQC2.Overlay.overlay, event, root.Message.room, root.author);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -97,12 +97,18 @@ TextEdit {
|
||||
enabled: !root.hoveredLink
|
||||
acceptedButtons: Qt.LeftButton
|
||||
acceptedDevices: PointerDevice.TouchScreen
|
||||
onLongPressed: RoomManager.viewEventMenu(root.eventId, root.Message.room, root.Message.selectedText, root.Message.hoveredLink);
|
||||
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);
|
||||
}
|
||||
}
|
||||
TapHandler {
|
||||
acceptedButtons: Qt.RightButton
|
||||
acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad | PointerDevice.Stylus
|
||||
gesturePolicy: TapHandler.WithinBounds
|
||||
onTapped: RoomManager.viewEventMenu(root.eventId, root.Message.room, root.Message.selectedText, root.Message.hoveredLink);
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -269,6 +269,8 @@ 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)
|
||||
|
||||
@@ -87,7 +87,6 @@ QDateTime MessageContentModel::time() const
|
||||
QString MessageContentModel::timeString() const
|
||||
{
|
||||
return time().toLocalTime().toString(u"hh:mm"_s);
|
||||
;
|
||||
}
|
||||
|
||||
QString MessageContentModel::authorId() const
|
||||
|
||||
@@ -8,6 +8,7 @@ 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.
|
||||
@@ -49,7 +50,7 @@ Kirigami.Page {
|
||||
text: i18nc("@action:button", "Room settings")
|
||||
icon.name: 'settings-configure-symbolic'
|
||||
onTriggered: {
|
||||
RoomSettingsView.openRoomSettings(root.room, RoomSettingsView.Room);
|
||||
Settings.RoomSettingsView.openRoomSettings(root.room, Settings.RoomSettingsView.Room);
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -52,7 +52,6 @@ QQC2.ScrollView {
|
||||
delegate: Timeline.MessageDelegate {
|
||||
alwaysFillWidth: true
|
||||
cardBackground: false
|
||||
room: root.room
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,7 +60,6 @@ QQC2.ScrollView {
|
||||
delegate: Timeline.MessageDelegate {
|
||||
alwaysFillWidth: true
|
||||
cardBackground: false
|
||||
room: root.room
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
|
||||
import QtQuick
|
||||
|
||||
import org.kde.kirigami as Kirigami
|
||||
|
||||
import org.kde.neochat.libneochat
|
||||
import org.kde.neochat.timeline
|
||||
|
||||
@@ -45,4 +47,15 @@ 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,9 +15,7 @@ import org.kde.neochat
|
||||
RowLayout {
|
||||
id: root
|
||||
|
||||
property var desiredWidth
|
||||
property bool collapsed: false
|
||||
required property NeoChatConnection connection
|
||||
|
||||
signal search
|
||||
|
||||
@@ -26,10 +24,6 @@ RowLayout {
|
||||
*/
|
||||
signal textChanged(string newText)
|
||||
|
||||
MediaDevices {
|
||||
id: devices
|
||||
}
|
||||
|
||||
Kirigami.Heading {
|
||||
Layout.fillWidth: true
|
||||
visible: !root.collapsed
|
||||
@@ -51,78 +45,4 @@ 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")
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -157,9 +157,11 @@ 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", "Reason for reporting this room"),
|
||||
placeholder: i18nc("@info:placeholder", "Optionally give a reason for reporting this room"),
|
||||
icon: "dialog-warning-symbolic",
|
||||
actionText: i18nc("@action:button 'Report' as in 'Report this room to the administrators'", "Report")
|
||||
actionText: i18nc("@action:button 'Report' as in 'Report this room to the administrators'", "Report"),
|
||||
reporting: true,
|
||||
connection: root.connection,
|
||||
}, {
|
||||
title: i18nc("@title", "Report Room"),
|
||||
width: Kirigami.Units.gridUnit * 25
|
||||
|
||||
@@ -270,9 +270,7 @@ Kirigami.Page {
|
||||
Component {
|
||||
id: exploreComponent
|
||||
ExploreComponent {
|
||||
desiredWidth: root.width - Kirigami.Units.largeSpacing
|
||||
collapsed: root.collapsed
|
||||
connection: root.connection
|
||||
|
||||
onSearch: root.search()
|
||||
|
||||
|
||||
@@ -46,6 +46,8 @@ QQC2.ItemDelegate {
|
||||
topPadding: 0
|
||||
bottomPadding: 0
|
||||
text: root.collapsed ? "" : root.displayName
|
||||
|
||||
onClicked: root.treeView.toggleExpanded(row)
|
||||
}
|
||||
QQC2.ToolButton {
|
||||
id: collapseButton
|
||||
|
||||
@@ -9,6 +9,7 @@ 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
|
||||
|
||||
@@ -158,7 +159,7 @@ QQC2.Control {
|
||||
width: Math.max(directChatNotificationCountTextMetrics.advanceWidth + Kirigami.Units.smallSpacing * 2, height)
|
||||
height: Kirigami.Units.iconSizes.smallMedium
|
||||
|
||||
text: visible ? root.connection.directChatNotifications + root.connection.directChatInvites : ""
|
||||
text: visible ? directChatButton.countedNotifications : ""
|
||||
visible: directChatButton.hasCountableNotifications && RoomManager.currentSpace !== "DM"
|
||||
color: Kirigami.Theme.textColor
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
@@ -263,21 +264,46 @@ QQC2.Control {
|
||||
}
|
||||
|
||||
AvatarTabButton {
|
||||
id: createNewButton
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: width - Kirigami.Units.smallSpacing
|
||||
Layout.maximumHeight: width - Kirigami.Units.smallSpacing
|
||||
|
||||
text: i18n("Create a space")
|
||||
text: i18nc("@action:button Create a new room or space", "Create")
|
||||
contentItem: Kirigami.Icon {
|
||||
source: "list-add"
|
||||
}
|
||||
|
||||
activeFocusOnTab: true
|
||||
down: menu.opened
|
||||
|
||||
onSelected: {
|
||||
(Qt.createComponent('org.kde.neochat', 'CreateSpaceDialog').createObject(root, {
|
||||
connection: root.connection
|
||||
}) as CreateSpaceDialog).open();
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -51,6 +51,7 @@ ecm_add_qml_module(Settings GENERATE_PLUGIN_SOURCE
|
||||
RoomProfile.qml
|
||||
RoomAdvancedPage.qml
|
||||
KeyboardShortcutsPage.qml
|
||||
Members.qml
|
||||
SOURCES
|
||||
colorschemer.cpp
|
||||
threepidaddhelper.cpp
|
||||
|
||||
@@ -36,9 +36,10 @@ FormCard.FormCardPage {
|
||||
devicesModel: root.devicesModel
|
||||
|
||||
FormCard.FormButtonDelegate {
|
||||
icon.name: "security-low"
|
||||
icon.name: !root.connection.isVerifiedSession ? "security-low" : "security-high"
|
||||
text: i18nc("@action:button", "Verify This Device")
|
||||
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.")
|
||||
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.")
|
||||
visible: !root.connection.isVerifiedSession || NeoChatConfig.alwaysVerifyDevice
|
||||
onClicked: {
|
||||
root.connection.startSelfVerification();
|
||||
|
||||
247
src/settings/Members.qml
Normal file
247
src/settings/Members.qml
Normal file
@@ -0,0 +1,247 @@
|
||||
// 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…")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -85,6 +85,19 @@ 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")
|
||||
|
||||
@@ -33,207 +33,10 @@ FormCard.FormCardPage {
|
||||
}
|
||||
|
||||
FormCard.FormHeader {
|
||||
title: i18nc("@title", "Privileged Users")
|
||||
visible: !root.loading
|
||||
title: i18nc("@title", "Power Levels")
|
||||
}
|
||||
FormCard.FormCard {
|
||||
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")
|
||||
enabled: root.room.canSendState("m.room.power_levels")
|
||||
Repeater {
|
||||
model: KSortFilterProxyModel {
|
||||
sourceModel: root.permissionsModel
|
||||
@@ -269,11 +72,49 @@ FormCard.FormCardPage {
|
||||
}
|
||||
|
||||
FormCard.FormHeader {
|
||||
visible: root.room.canSendState("m.room.power_levels")
|
||||
title: i18nc("@title", "Basic permissions")
|
||||
title: i18nc("@title", "Messages")
|
||||
}
|
||||
FormCard.FormCard {
|
||||
visible: root.room.canSendState("m.room.power_levels")
|
||||
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")
|
||||
Repeater {
|
||||
model: KSortFilterProxyModel {
|
||||
sourceModel: root.permissionsModel
|
||||
@@ -309,18 +150,15 @@ FormCard.FormCardPage {
|
||||
}
|
||||
|
||||
FormCard.FormHeader {
|
||||
visible: root.room.canSendState("m.room.power_levels")
|
||||
title: i18nc("@title", "Event permissions")
|
||||
title: i18nc("@title", "General")
|
||||
}
|
||||
FormCard.FormCard {
|
||||
visible: root.room.canSendState("m.room.power_levels")
|
||||
enabled: root.room.canSendState("m.room.power_levels")
|
||||
Repeater {
|
||||
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);
|
||||
return !isBasicPermissionRole && !isDefaultValueRole;
|
||||
return sourceModel.data(sourceModel.index(source_row, 0, source_parent), PermissionsModel.IsGeneralPermissionRole);
|
||||
}
|
||||
}
|
||||
delegate: FormCard.FormComboBoxDelegate {
|
||||
@@ -348,7 +186,59 @@ FormCard.FormCardPage {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
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.FormDelegateSeparator {
|
||||
below: addNewEventDelegate
|
||||
visible: otherEventsRepeater.count > 0
|
||||
}
|
||||
FormCard.AbstractFormDelegate {
|
||||
id: addNewEventDelegate
|
||||
|
||||
Layout.fillWidth: true
|
||||
|
||||
contentItem: RowLayout {
|
||||
|
||||
@@ -28,7 +28,6 @@ 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
|
||||
@@ -42,14 +41,19 @@ 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: {
|
||||
|
||||
@@ -55,6 +55,17 @@ 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")
|
||||
|
||||
@@ -50,12 +50,16 @@ 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 user power level")},
|
||||
{StateDefaultKey, kli18nc("Room permission type", "Default power level to set the room state")},
|
||||
{UsersDefaultKey, kli18nc("Room permission type", "Default power level")},
|
||||
{StateDefaultKey, kli18nc("Room permission type", "Default power level to change 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")},
|
||||
@@ -70,25 +74,58 @@ 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", "Set pinned events")},
|
||||
{u"m.room.pinned_events"_s, kli18nc("Room permission type", "Pin and unpin messages")},
|
||||
{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 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")},
|
||||
{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.")},
|
||||
};
|
||||
|
||||
// Permissions that should use the event default.
|
||||
// Permissions that should use the message 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,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -194,6 +231,12 @@ 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 {};
|
||||
}
|
||||
|
||||
@@ -213,6 +256,8 @@ QHash<int, QByteArray> PermissionsModel::roleNames() const
|
||||
roles[LevelNameRole] = "levelName";
|
||||
roles[IsDefaultValueRole] = "isDefaultValue";
|
||||
roles[IsBasicPermissionRole] = "isBasicPermission";
|
||||
roles[IsMessagePermissionRole] = "isMessagePermission";
|
||||
roles[IsGeneralPermissionRole] = "isGeneralPermission";
|
||||
return roles;
|
||||
}
|
||||
|
||||
|
||||
@@ -36,6 +36,8 @@ 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)
|
||||
|
||||
|
||||
@@ -8,16 +8,24 @@ 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: avatarFlowRepeater.model
|
||||
property alias model: root.limiterModel.sourceModel
|
||||
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
|
||||
@@ -39,11 +47,11 @@ RowLayout {
|
||||
Layout.preferredHeight: Kirigami.Units.iconSizes.small + Kirigami.Units.smallSpacing
|
||||
Layout.fillHeight: true
|
||||
|
||||
visible: text !== ""
|
||||
visible: root.limiterModel.extraCount > 0
|
||||
color: Kirigami.Theme.textColor
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
|
||||
text: root.model?.excessReadMarkersString ?? ""
|
||||
text: "+ " + root.limiterModel.extraCount
|
||||
|
||||
background: Kirigami.ShadowedRectangle {
|
||||
color: Kirigami.Theme.backgroundColor
|
||||
|
||||
@@ -132,14 +132,12 @@ 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -161,7 +159,7 @@ KirigamiComponents.ConvergentContextMenu {
|
||||
|
||||
Kirigami.Action {
|
||||
id: replyAction
|
||||
visible: root.messageComponentType !== MessageComponentType.Other || NeoChatConfig.relateAnyEvent
|
||||
visible: !root.room.readOnly && (root.messageComponentType !== MessageComponentType.Other || NeoChatConfig.relateAnyEvent)
|
||||
text: i18nc("@action:inmenu", "Reply")
|
||||
icon.name: "mail-replied-symbolic"
|
||||
onTriggered: {
|
||||
@@ -173,7 +171,7 @@ KirigamiComponents.ConvergentContextMenu {
|
||||
|
||||
Kirigami.Action {
|
||||
id: replyThreadAction
|
||||
visible: root.messageComponentType !== MessageComponentType.Other || NeoChatConfig.relateAnyEvent
|
||||
visible: (root.messageComponentType !== MessageComponentType.Other || NeoChatConfig.relateAnyEvent) && NeoChatConfig.threads
|
||||
text: i18nc("@action:inmenu", "Reply in Thread")
|
||||
icon.name: "dialog-messages"
|
||||
onTriggered: {
|
||||
@@ -204,11 +202,13 @@ KirigamiComponents.ConvergentContextMenu {
|
||||
icon.name: "edit-delete-remove"
|
||||
icon.color: "red"
|
||||
onTriggered: {
|
||||
let dialog = (root.Kirigami.PageStack.pageStack as Kirigami.PageRow).pushDialogLayer(Qt.createComponent('org.kde.neochat', 'ReasonDialog'), {
|
||||
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 Message"),
|
||||
placeholder: i18nc("@info:placeholder", "Reason for removing this message"),
|
||||
placeholder: i18nc("@info:placeholder", "Optionally give a reason for removing this message"),
|
||||
actionText: i18nc("@action:button 'Remove' as in 'Remove this message'", "Remove"),
|
||||
icon: "delete"
|
||||
icon: "delete",
|
||||
reporting: false,
|
||||
connection: root.connection,
|
||||
}, {
|
||||
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.Kirigami.PageStack.pageStack as Kirigami.PageRow).pushDialogLayer(Qt.createComponent('org.kde.neochat', 'ChooseRoomDialog'), {
|
||||
let page = ((root.QQC2.ApplicationWindow.window as Kirigami.ApplicationWindow).pageStack as Kirigami.PageRow).pushDialogLayer(Qt.createComponent('org.kde.neochat', 'ChooseRoomDialog'), {
|
||||
connection: root.connection
|
||||
}, {
|
||||
title: i18nc("@title", "Forward Message"),
|
||||
@@ -328,11 +328,13 @@ KirigamiComponents.ConvergentContextMenu {
|
||||
icon.name: "dialog-warning-symbolic"
|
||||
visible: !root.author.isLocalMember
|
||||
onTriggered: {
|
||||
let dialog = (root.Kirigami.PageStack.pageStack as Kirigami.PageRow).pushDialogLayer(Qt.createComponent('org.kde.neochat', 'ReasonDialog'), {
|
||||
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 Message"),
|
||||
placeholder: i18nc("@info:placeholder", "Reason for reporting this message"),
|
||||
placeholder: i18nc("@info:placeholder", "Optionally give a reason for reporting this message"),
|
||||
icon: "dialog-warning-symbolic",
|
||||
actionText: i18nc("@action:button 'Report' as in 'Report this event to the administrators'", "Report")
|
||||
actionText: i18nc("@action:button 'Report' as in 'Report this event to the administrators'", "Report"),
|
||||
reporting: true,
|
||||
connection: root.connection,
|
||||
}, {
|
||||
title: i18nc("@title", "Report Message"),
|
||||
width: Kirigami.Units.gridUnit * 25
|
||||
@@ -371,7 +373,7 @@ KirigamiComponents.ConvergentContextMenu {
|
||||
Kirigami.Action {
|
||||
text: i18nc("@action:inmenu", "Configure Web Shortcuts…")
|
||||
icon.name: "configure"
|
||||
visible: !Controller.isFlatpak && webShortcutModel.enabled
|
||||
visible: !Controller.isFlatpak && webShortcutModel.enabled && webShortcutModelAction.visible
|
||||
onTriggered: webShortcutModel.configureWebShortcuts()
|
||||
}
|
||||
|
||||
|
||||
@@ -21,16 +21,12 @@ DelegateChooser {
|
||||
|
||||
DelegateChoice {
|
||||
roleValue: DelegateType.State
|
||||
delegate: StateDelegate {
|
||||
room: root.room
|
||||
}
|
||||
delegate: StateDelegate {}
|
||||
}
|
||||
|
||||
DelegateChoice {
|
||||
roleValue: DelegateType.Message
|
||||
delegate: MessageDelegate {
|
||||
room: root.room
|
||||
}
|
||||
delegate: MessageDelegate {}
|
||||
}
|
||||
|
||||
DelegateChoice {
|
||||
@@ -45,23 +41,17 @@ DelegateChooser {
|
||||
|
||||
DelegateChoice {
|
||||
roleValue: DelegateType.Predecessor
|
||||
delegate: PredecessorDelegate {
|
||||
room: root.room
|
||||
}
|
||||
delegate: PredecessorDelegate {}
|
||||
}
|
||||
|
||||
DelegateChoice {
|
||||
roleValue: DelegateType.Successor
|
||||
delegate: SuccessorDelegate {
|
||||
room: root.room
|
||||
}
|
||||
delegate: SuccessorDelegate {}
|
||||
}
|
||||
|
||||
DelegateChoice {
|
||||
roleValue: DelegateType.TimelineEnd
|
||||
delegate: TimelineEndDelegate {
|
||||
room: root.room
|
||||
}
|
||||
delegate: TimelineEndDelegate {}
|
||||
}
|
||||
|
||||
DelegateChoice {
|
||||
@@ -75,9 +65,7 @@ DelegateChooser {
|
||||
|
||||
Component {
|
||||
id: hiddenDelegate
|
||||
HiddenDelegate {
|
||||
room: root.room
|
||||
}
|
||||
HiddenDelegate {}
|
||||
}
|
||||
Component {
|
||||
id: emptyDelegate
|
||||
|
||||
@@ -92,8 +92,10 @@ TimelineDelegate {
|
||||
|
||||
QtObject {
|
||||
id: _private
|
||||
function showMessageMenu() {
|
||||
RoomManager.viewEventMenu(root.eventId, root.room, "");
|
||||
|
||||
function showMessageMenu(): void {
|
||||
let event = root.Message.room.findEvent(root.eventId);
|
||||
RoomManager.viewEventMenu(root.QQC2.Overlay.overlay, event, root.room, "");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -208,6 +208,15 @@ 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 {
|
||||
@@ -218,13 +227,15 @@ MessageDelegateBase {
|
||||
quickActionComponent: QuickActions {
|
||||
room: root.room
|
||||
eventId: root.eventId
|
||||
author: root.author
|
||||
}
|
||||
|
||||
QtObject {
|
||||
id: _private
|
||||
|
||||
function showMessageMenu() {
|
||||
RoomManager.viewEventMenu(root.eventId, root.room, root.Message.selectedText, root.Message.hoveredLink);
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user