Compare commits
16 Commits
work/redst
...
work/carlo
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c58ac6769b | ||
|
|
1f482000d2 | ||
|
|
d0731c9c36 | ||
|
|
530fbc212b | ||
|
|
ea467a7901 | ||
|
|
78ac2681de | ||
|
|
0e16518123 | ||
|
|
62e204207d | ||
|
|
be712a6810 | ||
|
|
287043ed44 | ||
|
|
4675f5f7e7 | ||
|
|
305f008796 | ||
|
|
a4bf3e4da0 | ||
|
|
20bd8508e0 | ||
|
|
7406fd9884 | ||
|
|
ff62c74cb3 |
@@ -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()
|
||||
|
||||
711
po/ar/neochat.po
711
po/ar/neochat.po
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
660
po/az/neochat.po
660
po/az/neochat.po
File diff suppressed because it is too large
Load Diff
637
po/ca/neochat.po
637
po/ca/neochat.po
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
612
po/cs/neochat.po
612
po/cs/neochat.po
File diff suppressed because it is too large
Load Diff
625
po/da/neochat.po
625
po/da/neochat.po
File diff suppressed because it is too large
Load Diff
698
po/de/neochat.po
698
po/de/neochat.po
File diff suppressed because it is too large
Load Diff
692
po/el/neochat.po
692
po/el/neochat.po
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
686
po/eo/neochat.po
686
po/eo/neochat.po
File diff suppressed because it is too large
Load Diff
619
po/es/neochat.po
619
po/es/neochat.po
File diff suppressed because it is too large
Load Diff
712
po/eu/neochat.po
712
po/eu/neochat.po
File diff suppressed because it is too large
Load Diff
698
po/fi/neochat.po
698
po/fi/neochat.po
File diff suppressed because it is too large
Load Diff
726
po/fr/neochat.po
726
po/fr/neochat.po
File diff suppressed because it is too large
Load Diff
540
po/ga/neochat.po
540
po/ga/neochat.po
File diff suppressed because it is too large
Load Diff
690
po/gl/neochat.po
690
po/gl/neochat.po
File diff suppressed because it is too large
Load Diff
627
po/he/neochat.po
627
po/he/neochat.po
File diff suppressed because it is too large
Load Diff
692
po/hi/neochat.po
692
po/hi/neochat.po
File diff suppressed because it is too large
Load Diff
698
po/hu/neochat.po
698
po/hu/neochat.po
File diff suppressed because it is too large
Load Diff
887
po/ia/neochat.po
887
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
652
po/ie/neochat.po
652
po/ie/neochat.po
File diff suppressed because it is too large
Load Diff
714
po/it/neochat.po
714
po/it/neochat.po
File diff suppressed because it is too large
Load Diff
540
po/ja/neochat.po
540
po/ja/neochat.po
File diff suppressed because it is too large
Load Diff
638
po/ka/neochat.po
638
po/ka/neochat.po
File diff suppressed because it is too large
Load Diff
698
po/ko/neochat.po
698
po/ko/neochat.po
File diff suppressed because it is too large
Load Diff
631
po/lt/neochat.po
631
po/lt/neochat.po
File diff suppressed because it is too large
Load Diff
698
po/lv/neochat.po
698
po/lv/neochat.po
File diff suppressed because it is too large
Load Diff
662
po/nl/neochat.po
662
po/nl/neochat.po
File diff suppressed because it is too large
Load Diff
680
po/nn/neochat.po
680
po/nn/neochat.po
File diff suppressed because it is too large
Load Diff
653
po/pa/neochat.po
653
po/pa/neochat.po
File diff suppressed because it is too large
Load Diff
712
po/pl/neochat.po
712
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
639
po/ro/neochat.po
639
po/ro/neochat.po
File diff suppressed because it is too large
Load Diff
690
po/ru/neochat.po
690
po/ru/neochat.po
File diff suppressed because it is too large
Load Diff
692
po/sa/neochat.po
692
po/sa/neochat.po
File diff suppressed because it is too large
Load Diff
642
po/sk/neochat.po
642
po/sk/neochat.po
File diff suppressed because it is too large
Load Diff
629
po/sl/neochat.po
629
po/sl/neochat.po
File diff suppressed because it is too large
Load Diff
698
po/sv/neochat.po
698
po/sv/neochat.po
File diff suppressed because it is too large
Load Diff
693
po/ta/neochat.po
693
po/ta/neochat.po
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
629
po/tr/neochat.po
629
po/tr/neochat.po
File diff suppressed because it is too large
Load Diff
633
po/uk/neochat.po
633
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
|
||||
- kde-neon-6@latest/beta
|
||||
command: usr/bin/neochat
|
||||
common-id: org.kde.neochat
|
||||
desktop: usr/share/applications/org.kde.neochat.desktop
|
||||
@@ -25,7 +25,7 @@ apps:
|
||||
- password-manager-service
|
||||
- accounts-service
|
||||
environment:
|
||||
QT_PLUGIN_PATH: "$SNAP/usr/lib/$CRAFT_ARCH_TRIPLET/plugins/snap/kf6-core24/current/usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR/plugins"
|
||||
QT_PLUGIN_PATH: "$SNAP/usr/lib/$CRAFT_ARCH_TRIPLET/plugins:/snap/kf6-core24/current/usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR/plugins"
|
||||
QML_IMPORT_PATH: "$SNAP/usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR/qml:/snap/kf6-core24/current/usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR/qml"
|
||||
|
||||
compression: lzo
|
||||
@@ -55,53 +55,11 @@ parts:
|
||||
- -usr/lib/*/pkgconfig
|
||||
- -usr/lib/*/cmake
|
||||
|
||||
libsecret:
|
||||
source: https://gitlab.gnome.org/GNOME/libsecret.git
|
||||
source-tag: '0.21.4'
|
||||
source-depth: 1
|
||||
plugin: meson
|
||||
meson-parameters:
|
||||
- --prefix=/usr
|
||||
- -Doptimization=3
|
||||
- -Ddebug=true
|
||||
- -Dmanpage=false
|
||||
- -Dvapi=false
|
||||
- -Dintrospection=false
|
||||
- -Dcrypto=disabled
|
||||
- -Dgtk_doc=false
|
||||
build-packages:
|
||||
- meson
|
||||
- libglib2.0-dev
|
||||
- libgcrypt20-dev
|
||||
prime:
|
||||
- -usr/include
|
||||
- -usr/lib/*/pkgconfig
|
||||
|
||||
qtkeychain:
|
||||
after: [libsecret]
|
||||
source: https://github.com/frankosterfeld/qtkeychain.git
|
||||
source-tag: 0.14.3
|
||||
source-depth: 1
|
||||
plugin: cmake
|
||||
build-environment:
|
||||
- PATH: /snap/bin:${PATH}
|
||||
- PKG_CONFIG_PATH: "$CRAFT_STAGE/usr/lib/$CRAFT_ARCH_TRIPLET/pkgconfig:$PKG_CONFIG_PATH"
|
||||
cmake-parameters:
|
||||
- -DCMAKE_INSTALL_PREFIX=/usr
|
||||
- -DCMAKE_BUILD_TYPE=Release
|
||||
- -DBUILD_TRANSLATIONS=NO
|
||||
- -DBUILD_WITH_QT6=ON
|
||||
prime:
|
||||
- -usr/include
|
||||
- -usr/lib/*/pkgconfig
|
||||
- -usr/lib/*/cmake
|
||||
|
||||
libquotient:
|
||||
after:
|
||||
- olm
|
||||
- qtkeychain
|
||||
source: https://github.com/quotient-im/libQuotient.git
|
||||
source-tag: 0.9.2
|
||||
source-branch: 0.9.5
|
||||
source-depth: 1
|
||||
plugin: cmake
|
||||
build-environment:
|
||||
@@ -110,12 +68,16 @@ parts:
|
||||
- cmake
|
||||
build-packages:
|
||||
- libssl-dev
|
||||
- curl
|
||||
cmake-parameters:
|
||||
- -DCMAKE_INSTALL_PREFIX=/usr
|
||||
- -DCMAKE_BUILD_TYPE=Release
|
||||
- -DBUILD_TESTING=OFF
|
||||
- -DQuotient_ENABLE_E2EE=ON
|
||||
- -DBUILD_WITH_QT6=ON
|
||||
override-pull: |
|
||||
craftctl default
|
||||
curl https://github.com/quotient-im/libQuotient/commit/ea83157eed37ff97ab275a5d14c971f0a5a70595.diff | patch -p1
|
||||
prime:
|
||||
- -usr/include
|
||||
- -usr/lib/*/pkgconfig
|
||||
@@ -123,38 +85,39 @@ parts:
|
||||
|
||||
kquickimageeditor:
|
||||
source: https://invent.kde.org/libraries/kquickimageeditor.git
|
||||
source-tag: 'v0.3.0'
|
||||
source-tag: 'v0.6.0'
|
||||
source-depth: 1
|
||||
plugin: cmake
|
||||
build-environment:
|
||||
- PATH: /snap/bin:${PATH}
|
||||
build-environment: &build-environment
|
||||
- LD_LIBRARY_PATH: "/snap/kde-qt6-core24-sdk/current/usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR:/snap/kde-qt6-core24-sdk/current/usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR/libproxy:/snap/kde-qt6-core24-sdk/current/usr/lib:/snap/kf6-core24-sdk/current/usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR:/snap/kf6-core24-sdk/current/usr/lib:$CRAFT_STAGE/usr/lib:$CRAFT_STAGE/lib/:$CRAFT_STAGE/usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}"
|
||||
- PATH: /snap/kde-qt6-core24-sdk/current/usr/bin${PATH:+:$PATH}
|
||||
- PKG_CONFIG_PATH: /snap/kde-qt6-core24-sdk/current/usr/lib/${CRAFT_ARCH_TRIPLET_BUILD_FOR}/pkgconfig${PKG_CONFIG_PATH:+:$PKG_CONFIG_PATH}
|
||||
- PYTHONPATH: ${CRAFT_STAGE}/lib/python3.12/site-packages:${CRAFT_STAGE}/usr/lib/python3/dist-packages
|
||||
- LD_LIBRARY_PATH: "/snap/mesa-2404/current/usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR:$CRAFT_STAGE/usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR:/snap/kde-qt6-core24-sdk/current/usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR/libproxy:$LD_LIBRARY_PATH"
|
||||
cmake-parameters:
|
||||
- XDG_DATA_DIRS: $CRAFT_STAGE/usr/share:/snap/kde-qt6-core24-sdk/current/usr/share:/usr/share${XDG_DATA_DIRS:+:$XDG_DATA_DIRS}
|
||||
- XDG_CONFIG_HOME: $CRAFT_STAGE/etc/xdg:/snap/kde-qt6-core24-sdk/current/etc/xdg:/etc/xdg${XDG_CONFIG_HOME:+:$XDG_CONFIG_HOME}
|
||||
cmake-parameters: &cmake-parameters
|
||||
- -DCMAKE_INSTALL_PREFIX=/usr
|
||||
- -DCMAKE_BUILD_TYPE=Release
|
||||
- -DQT_MAJOR_VERSION=6
|
||||
- -DBUILD_WITH_QT6=ON
|
||||
- -DBUILD_TESTING=OFF
|
||||
prime:
|
||||
- "-DCMAKE_FIND_ROOT_PATH=$CRAFT_STAGE\\;/snap/kde-qt6-core24-sdk/current\\;/snap/kf6-core24-sdk/current/usr"
|
||||
- "-DCMAKE_PREFIX_PATH=$CRAFT_STAGE\\;/snap/kde-qt6-core24-sdk/current\\;/snap/kf6-core24-sdk/current/usr"
|
||||
prime: &prime
|
||||
- -usr/include
|
||||
- -usr/lib/*/pkgconfig
|
||||
- -usr/lib/*/cmake
|
||||
|
||||
kunifiedpush:
|
||||
source: https://invent.kde.org/libraries/kunifiedpush.git
|
||||
source-branch: release/24.12
|
||||
plugin: cmake
|
||||
build-environment:
|
||||
- PATH: /snap/bin:${PATH}
|
||||
- PYTHONPATH: ${CRAFT_STAGE}/lib/python3.12/site-packages:${CRAFT_STAGE}/usr/lib/python3/dist-packages
|
||||
- LD_LIBRARY_PATH: "/snap/mesa-2404/current/usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR:$CRAFT_STAGE/usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR:/snap/kde-qt6-core24-sdk/current/usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR/libproxy:$LD_LIBRARY_PATH"
|
||||
cmake-parameters:
|
||||
- -DCMAKE_INSTALL_PREFIX=/usr
|
||||
- -DCMAKE_BUILD_TYPE=Release
|
||||
- -DBUILD_TESTING=OFF
|
||||
build-environment: *build-environment
|
||||
cmake-parameters: *cmake-parameters
|
||||
prime: *prime
|
||||
|
||||
neochat:
|
||||
after:
|
||||
- qtkeychain
|
||||
- libquotient
|
||||
- kquickimageeditor
|
||||
- kunifiedpush
|
||||
@@ -162,24 +125,20 @@ parts:
|
||||
- usr/share/metainfo/org.kde.neochat.appdata.xml
|
||||
source: .
|
||||
plugin: cmake
|
||||
build-environment:
|
||||
- PATH: /snap/bin:${PATH}
|
||||
- PYTHONPATH: ${CRAFT_STAGE}/lib/python3.12/site-packages:${CRAFT_STAGE}/usr/lib/python3/dist-packages
|
||||
- LD_LIBRARY_PATH: "/snap/mesa-2404/current/usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR:$CRAFT_STAGE/usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR:/snap/kde-qt6-core24-sdk/current/usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR/libproxy:$LD_LIBRARY_PATH"
|
||||
build-environment: *build-environment
|
||||
build-packages:
|
||||
- cmark
|
||||
- docbook-xml
|
||||
- docbook-xsl
|
||||
- duktape-dev
|
||||
- libcmark-dev
|
||||
- libsqlite3-dev
|
||||
- libvulkan-dev
|
||||
- libxkbcommon-dev
|
||||
- libicu-dev
|
||||
- libpulse0
|
||||
cmake-parameters:
|
||||
- -DCMAKE_INSTALL_PREFIX=/usr
|
||||
- -DCMAKE_BUILD_TYPE=Release
|
||||
- -DBUILD_TESTING=OFF
|
||||
prime:
|
||||
- -usr/share/man
|
||||
cmake-parameters: *cmake-parameters
|
||||
prime: *prime
|
||||
|
||||
deps:
|
||||
after: [neochat]
|
||||
@@ -198,3 +157,14 @@ parts:
|
||||
${CRAFT_PART_SRC}/bin/gpu-2404-cleanup mesa-2404
|
||||
prime:
|
||||
- bin/gpu-2404-wrapper
|
||||
|
||||
cleanup:
|
||||
after:
|
||||
- neochat
|
||||
plugin: nil
|
||||
override-prime: |
|
||||
set -eux
|
||||
for snap in "core24" "kf6-core24"; do
|
||||
cd "/snap/$snap/current" && find . -type f,l -exec rm -rf "${CRAFT_PRIME}/{}" \;
|
||||
done
|
||||
|
||||
|
||||
@@ -37,8 +37,6 @@ qt_add_library(neochat STATIC
|
||||
texttospeechhelper.cpp
|
||||
models/limitermodel.cpp
|
||||
models/limitermodel.h
|
||||
supportcontroller.cpp
|
||||
supportcontroller.h
|
||||
)
|
||||
|
||||
set_source_files_properties(qml/OsmLocationPlugin.qml PROPERTIES
|
||||
@@ -108,8 +106,6 @@ ecm_add_qml_module(neochat URI org.kde.neochat GENERATE_PLUGIN_SOURCE
|
||||
qml/NewPollDialog.qml
|
||||
qml/UserMenu.qml
|
||||
qml/MeetingDialog.qml
|
||||
qml/SeenByDialog.qml
|
||||
qml/SupportDialog.qml
|
||||
DEPENDENCIES
|
||||
QtCore
|
||||
QtQuick
|
||||
@@ -220,7 +216,6 @@ target_link_libraries(neochat PUBLIC
|
||||
KF6::ItemModels
|
||||
KF6::I18nQml
|
||||
KirigamiApp
|
||||
KirigamiAddonsComponents
|
||||
QuotientQt6
|
||||
Login
|
||||
Rooms
|
||||
|
||||
@@ -11,7 +11,6 @@
|
||||
#include <KNotification>
|
||||
#include <KNotificationPermission>
|
||||
#include <KNotificationReplyAction>
|
||||
#include <KirigamiAddons/Components/NameUtils>
|
||||
|
||||
#include <QPainter>
|
||||
#include <Quotient/accountregistry.h>
|
||||
@@ -145,9 +144,16 @@ void NotificationsManager::processNotificationJob(QPointer<NeoChatConnection> co
|
||||
body = notification["event"_L1]["content"_L1]["body"_L1].toString();
|
||||
}
|
||||
|
||||
QImage avatar_image;
|
||||
if (!sender.avatarUrl().isEmpty()) {
|
||||
avatar_image = room->member(sender.id()).avatar(128, 128, {});
|
||||
} else {
|
||||
avatar_image = room->avatar(128);
|
||||
}
|
||||
postNotification(dynamic_cast<NeoChatRoom *>(room),
|
||||
room->member(sender.id()),
|
||||
sender.displayName(),
|
||||
body,
|
||||
avatar_image,
|
||||
notification["event"_L1].toObject()["event_id"_L1].toString(),
|
||||
true,
|
||||
pair.first);
|
||||
@@ -189,8 +195,9 @@ bool NotificationsManager::shouldPostNotification(QPointer<NeoChatConnection> co
|
||||
}
|
||||
|
||||
void NotificationsManager::postNotification(NeoChatRoom *room,
|
||||
const RoomMember &member,
|
||||
const QString &sender,
|
||||
const QString &text,
|
||||
const QImage &icon,
|
||||
const QString &replyEventId,
|
||||
bool canReply,
|
||||
qint64 timestamp)
|
||||
@@ -215,11 +222,11 @@ void NotificationsManager::postNotification(NeoChatRoom *room,
|
||||
if (room->isDirectChat()) {
|
||||
entry = text.toHtmlEscaped();
|
||||
} else {
|
||||
entry = i18n("%1: %2", member.displayName(), text.toHtmlEscaped());
|
||||
entry = i18n("%1: %2", sender, text.toHtmlEscaped());
|
||||
}
|
||||
|
||||
notification->setText(entry);
|
||||
notification->setPixmap(createNotificationImage(member, room));
|
||||
notification->setPixmap(createNotificationImage(icon, room));
|
||||
|
||||
auto defaultAction = notification->addDefaultAction(i18n("Open NeoChat in this room"));
|
||||
connect(defaultAction, &KNotificationAction::activated, this, [notification, room]() {
|
||||
@@ -287,10 +294,18 @@ void NotificationsManager::doPostInviteNotification(QPointer<NeoChatRoom> room)
|
||||
}
|
||||
const auto sender = room->member(roomMemberEvent->senderId());
|
||||
|
||||
QImage avatar_image;
|
||||
if (roomMemberEvent && !room->member(roomMemberEvent->senderId()).avatarUrl().isEmpty()) {
|
||||
avatar_image = room->member(roomMemberEvent->senderId()).avatar(128, 128, {});
|
||||
} else {
|
||||
qWarning() << "using this room's avatar";
|
||||
avatar_image = room->avatar(128);
|
||||
}
|
||||
|
||||
KNotification *notification = new KNotification(u"invite"_s);
|
||||
notification->setText(i18n("%1 invited you to a room", sender.htmlSafeDisplayName()));
|
||||
notification->setTitle(room->displayName());
|
||||
notification->setPixmap(createNotificationImage(sender, room));
|
||||
notification->setPixmap(createNotificationImage(avatar_image, nullptr));
|
||||
auto defaultAction = notification->addDefaultAction(i18n("Open this invitation in NeoChat"));
|
||||
connect(defaultAction, &KNotificationAction::activated, this, [notification, room]() {
|
||||
if (!room) {
|
||||
@@ -396,125 +411,41 @@ void NotificationsManager::postPushNotification(const QByteArray &message)
|
||||
}
|
||||
}
|
||||
|
||||
QPixmap NotificationsManager::createNotificationImage(const Quotient::RoomMember &member, NeoChatRoom *room)
|
||||
{
|
||||
QImage senderIcon = member.avatar(avatarDimension, avatarDimension, {});
|
||||
bool senderIconIsPlaceholder = false;
|
||||
if (senderIcon.isNull()) {
|
||||
senderIcon = createPlaceholderImage(member.displayName());
|
||||
senderIconIsPlaceholder = true;
|
||||
}
|
||||
|
||||
QImage icon;
|
||||
if (room->isDirectChat()) {
|
||||
icon = senderIcon;
|
||||
} else {
|
||||
QImage roomIcon = room->avatar(avatarDimension, avatarDimension);
|
||||
bool roomIconIsPlaceholder = false;
|
||||
if (roomIcon.isNull()) {
|
||||
roomIcon = createPlaceholderImage(room->displayName());
|
||||
roomIconIsPlaceholder = true;
|
||||
}
|
||||
|
||||
icon = createCombinedNotificationImage(senderIcon, senderIconIsPlaceholder, roomIcon, roomIconIsPlaceholder);
|
||||
}
|
||||
|
||||
return QPixmap::fromImage(icon);
|
||||
}
|
||||
|
||||
QImage NotificationsManager::createCombinedNotificationImage(const QImage &senderIcon,
|
||||
const bool senderIconIsPlaceholder,
|
||||
const QImage &roomIcon,
|
||||
const bool roomIconIsPlaceholder)
|
||||
QPixmap NotificationsManager::createNotificationImage(const QImage &icon, NeoChatRoom *room)
|
||||
{
|
||||
// Handle avatars that are lopsided in one dimension
|
||||
const int biggestDimension = std::max(senderIcon.width(), senderIcon.height());
|
||||
const QRectF imageRect = QRect{0, 0, biggestDimension, biggestDimension}.toRectF();
|
||||
const int biggestDimension = std::max(icon.width(), icon.height());
|
||||
const QRect imageRect{0, 0, biggestDimension, biggestDimension};
|
||||
|
||||
QImage roundedImage(imageRect.size().toSize(), QImage::Format_ARGB32);
|
||||
QImage roundedImage(imageRect.size(), QImage::Format_ARGB32);
|
||||
roundedImage.fill(Qt::transparent);
|
||||
|
||||
QPainter painter(&roundedImage);
|
||||
painter.setRenderHint(QPainter::SmoothPixmapTransform);
|
||||
painter.setPen(Qt::NoPen);
|
||||
|
||||
if (senderIconIsPlaceholder) {
|
||||
painter.drawImage(imageRect, senderIcon);
|
||||
} else {
|
||||
// Fill background for transparent non-placeholder avatars
|
||||
painter.setBrush(Qt::white);
|
||||
painter.drawRoundedRect(imageRect, imageRect.width(), imageRect.height());
|
||||
// Fill background for transparent avatars
|
||||
painter.setBrush(Qt::white);
|
||||
painter.drawRoundedRect(imageRect, imageRect.width(), imageRect.height());
|
||||
|
||||
painter.setBrush(senderIcon.scaledToHeight(biggestDimension));
|
||||
painter.drawRoundedRect(imageRect, imageRect.width(), imageRect.height());
|
||||
QBrush brush(icon.scaledToHeight(biggestDimension));
|
||||
painter.setBrush(brush);
|
||||
painter.drawRoundedRect(imageRect, imageRect.width(), imageRect.height());
|
||||
|
||||
if (room != nullptr) {
|
||||
const QImage roomAvatar = room->avatar(imageRect.width(), imageRect.height());
|
||||
if (!roomAvatar.isNull() && icon != roomAvatar) {
|
||||
const QRect lowerQuarter{imageRect.center(), imageRect.size() / 2};
|
||||
|
||||
painter.setBrush(Qt::white);
|
||||
painter.drawRoundedRect(lowerQuarter, lowerQuarter.width(), lowerQuarter.height());
|
||||
|
||||
painter.setBrush(roomAvatar.scaled(lowerQuarter.size()));
|
||||
painter.drawRoundedRect(lowerQuarter, lowerQuarter.width(), lowerQuarter.height());
|
||||
}
|
||||
}
|
||||
|
||||
const QRectF lowerQuarter{imageRect.center(), imageRect.size() / 2.0};
|
||||
|
||||
if (roomIconIsPlaceholder) {
|
||||
// Ditto for room icons, but we also want to "carve out" the transparent area for readability
|
||||
painter.setCompositionMode(QPainter::CompositionMode_Clear);
|
||||
painter.setBrush(Qt::transparent);
|
||||
painter.drawRoundedRect(lowerQuarter, lowerQuarter.width(), lowerQuarter.height());
|
||||
|
||||
painter.setCompositionMode(QPainter::CompositionMode_SourceOver);
|
||||
painter.drawImage(lowerQuarter, roomIcon);
|
||||
} else {
|
||||
painter.setBrush(Qt::white);
|
||||
painter.drawRoundedRect(lowerQuarter, lowerQuarter.width(), lowerQuarter.height());
|
||||
|
||||
painter.setBrush(roomIcon.scaled(lowerQuarter.size().toSize()));
|
||||
painter.drawRoundedRect(lowerQuarter, lowerQuarter.width(), lowerQuarter.height());
|
||||
}
|
||||
|
||||
return roundedImage;
|
||||
}
|
||||
|
||||
QImage NotificationsManager::createPlaceholderImage(const QString &name)
|
||||
{
|
||||
const QColor color = NameUtils().colorsFromString(name);
|
||||
|
||||
QImage image(avatarDimension, avatarDimension, QImage::Format_ARGB32);
|
||||
image.fill(Qt::transparent);
|
||||
|
||||
QPainter painter(&image);
|
||||
painter.setRenderHint(QPainter::Antialiasing);
|
||||
|
||||
// Draw background
|
||||
QColor backgroundColor = color;
|
||||
backgroundColor.setAlphaF(0.07); // Same as in Kirigami Add-ons.
|
||||
|
||||
painter.setBrush(backgroundColor);
|
||||
painter.setPen(Qt::transparent);
|
||||
painter.drawRoundedRect(image.rect(), image.width(), image.height());
|
||||
|
||||
constexpr float borderWidth = 3.0; // Slightly bigger than in Add-ons so it renders better with QPainter at these dimensions.
|
||||
|
||||
// Draw border
|
||||
painter.setBrush(Qt::transparent);
|
||||
painter.setPen(QPen(color, borderWidth));
|
||||
painter.drawRoundedRect(image.rect().toRectF().marginsRemoved(QMarginsF(borderWidth, borderWidth, borderWidth, borderWidth)),
|
||||
image.width(),
|
||||
image.height());
|
||||
|
||||
const QString initials = NameUtils().initialsFromString(name);
|
||||
|
||||
QTextOption option;
|
||||
option.setAlignment(Qt::AlignCenter);
|
||||
|
||||
// Calculation similar to the one found in Kirigami Add-ons.
|
||||
constexpr int largeSpacing = 8; // Same as what's defined in kirigami.
|
||||
constexpr int padding = std::max(0, std::min(largeSpacing, avatarDimension - largeSpacing * 2));
|
||||
|
||||
QFont font;
|
||||
font.setPixelSize((avatarDimension - padding) / 2);
|
||||
|
||||
painter.setBrush(color);
|
||||
painter.setPen(color);
|
||||
painter.setFont(font);
|
||||
painter.drawText(image.rect(), initials, option);
|
||||
|
||||
return image;
|
||||
return QPixmap::fromImage(roundedImage);
|
||||
}
|
||||
|
||||
#include "moc_notificationsmanager.cpp"
|
||||
|
||||
@@ -13,11 +13,6 @@
|
||||
#include <Quotient/csapi/notifications.h>
|
||||
#include <Quotient/jobs/basejob.h>
|
||||
|
||||
namespace Quotient
|
||||
{
|
||||
class RoomMember;
|
||||
}
|
||||
|
||||
class NeoChatConnection;
|
||||
class KNotification;
|
||||
class NeoChatRoom;
|
||||
@@ -72,24 +67,15 @@ private:
|
||||
QStringList m_connActiveJob;
|
||||
void startNotificationJob(QPointer<NeoChatConnection> connection);
|
||||
|
||||
/**
|
||||
* @return A combined image of the sender and room's avatar.
|
||||
*/
|
||||
static QPixmap createNotificationImage(const Quotient::RoomMember &member, NeoChatRoom *room);
|
||||
|
||||
/**
|
||||
* @return The sender and room icon combined together into one image. Used internally by createNotificationImage.
|
||||
*/
|
||||
static QImage createCombinedNotificationImage(const QImage &senderIcon, bool senderIconIsPlaceholder, const QImage &roomIcon, bool roomIconIsPlaceholder);
|
||||
|
||||
/**
|
||||
* @return A placeholder avatar image, similar to the one found in Kirigami Add-ons.
|
||||
*/
|
||||
static QImage createPlaceholderImage(const QString &name);
|
||||
|
||||
QPixmap createNotificationImage(const QImage &icon, NeoChatRoom *room);
|
||||
bool shouldPostNotification(QPointer<NeoChatConnection> connection, const QJsonValue ¬ification);
|
||||
void
|
||||
postNotification(NeoChatRoom *room, const Quotient::RoomMember &member, const QString &text, const QString &replyEventId, bool canReply, qint64 timestamp);
|
||||
void postNotification(NeoChatRoom *room,
|
||||
const QString &sender,
|
||||
const QString &text,
|
||||
const QImage &icon,
|
||||
const QString &replyEventId,
|
||||
bool canReply,
|
||||
qint64 timestamp);
|
||||
|
||||
void doPostInviteNotification(QPointer<NeoChatRoom> room);
|
||||
|
||||
@@ -98,8 +84,6 @@ private:
|
||||
|
||||
bool permissionAsked = false;
|
||||
|
||||
static constexpr int avatarDimension = 128;
|
||||
|
||||
private Q_SLOTS:
|
||||
void processNotificationJob(QPointer<NeoChatConnection> connection, Quotient::GetNotificationsJob *job, bool initialization);
|
||||
};
|
||||
|
||||
@@ -5,7 +5,6 @@ pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls as QQC2
|
||||
import QtMultimedia
|
||||
|
||||
import org.kde.kirigami as Kirigami
|
||||
import org.kde.kirigamiaddons.components as KirigamiComponents
|
||||
@@ -19,10 +18,6 @@ KirigamiComponents.ConvergentContextMenu {
|
||||
required property NeoChatConnection connection
|
||||
required property Kirigami.ApplicationWindow window
|
||||
|
||||
data: MediaDevices {
|
||||
id: devices
|
||||
}
|
||||
|
||||
Kirigami.Action {
|
||||
text: i18nc("@action:button", "Show QR Code")
|
||||
icon.name: "view-barcode-qr-symbolic"
|
||||
@@ -38,14 +33,12 @@ KirigamiComponents.ConvergentContextMenu {
|
||||
}
|
||||
|
||||
Kirigami.Action {
|
||||
text: i18nc("@action:inmenu", "Scan a QR Code")
|
||||
icon.name: "document-scan-symbolic"
|
||||
visible: devices.videoInputs.length > 0
|
||||
onTriggered: (root.Kirigami.PageStack.pageStack as Kirigami.PageRow).pushDialogLayer(Qt.createComponent("org.kde.neochat", "QrScannerPage"), {
|
||||
text: i18nc("@action:inmenu", "Switch Account")
|
||||
icon.name: "system-switch-user"
|
||||
shortcut: "Ctrl+U"
|
||||
onTriggered: (Qt.createComponent("org.kde.neochat", "AccountSwitchDialog").createObject(QQC2.Overlay.overlay, {
|
||||
connection: root.connection
|
||||
}, {
|
||||
title: i18nc("@title", "Scan a QR Code")
|
||||
})
|
||||
}) as Kirigami.Dialog).open();
|
||||
}
|
||||
|
||||
Kirigami.Action {
|
||||
@@ -62,6 +55,14 @@ KirigamiComponents.ConvergentContextMenu {
|
||||
}
|
||||
}
|
||||
|
||||
Kirigami.Action {
|
||||
text: i18nc("@action:inmenu", "Devices")
|
||||
icon.name: "computer-symbolic"
|
||||
onTriggered: {
|
||||
NeoChatSettingsView.open('devices');
|
||||
}
|
||||
}
|
||||
|
||||
Kirigami.Action {
|
||||
text: i18nc("@action:inmenu", "Open Developer Tools")
|
||||
icon.name: "tools"
|
||||
@@ -75,6 +76,14 @@ KirigamiComponents.ConvergentContextMenu {
|
||||
})
|
||||
}
|
||||
|
||||
Kirigami.Action {
|
||||
text: i18nc("@action:inmenu", "Open Secret Backup")
|
||||
icon.name: "unlock"
|
||||
onTriggered: root.window.pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat', 'UnlockSSSSDialog'), {}, {
|
||||
title: i18nc("@title:window", "Open Key Backup")
|
||||
})
|
||||
}
|
||||
|
||||
Kirigami.Action {
|
||||
text: i18nc("@action:inmenu", "Verify This Device")
|
||||
icon.name: "security-low"
|
||||
@@ -94,25 +103,10 @@ KirigamiComponents.ConvergentContextMenu {
|
||||
}
|
||||
|
||||
Kirigami.Action {
|
||||
text: i18nc("@action:inmenu Open support dialog", "Support")
|
||||
icon.name: "help-contents-symbolic"
|
||||
onTriggered: {
|
||||
Qt.createComponent("org.kde.neochat", "SupportDialog").createObject(QQC2.Overlay.overlay, {
|
||||
connection: root.connection,
|
||||
}).open();
|
||||
}
|
||||
}
|
||||
|
||||
Kirigami.Action {
|
||||
separator: true
|
||||
}
|
||||
|
||||
Kirigami.Action {
|
||||
text: i18nc("@action:inmenu", "Switch Account")
|
||||
icon.name: "system-switch-user"
|
||||
shortcut: "Ctrl+U"
|
||||
onTriggered: (Qt.createComponent("org.kde.neochat", "AccountSwitchDialog").createObject(QQC2.Overlay.overlay, {
|
||||
text: i18nc("@action:inmenu", "Logout…")
|
||||
icon.name: "im-kick-user"
|
||||
onTriggered: (Qt.createComponent("org.kde.neochat", "ConfirmLogoutDialog").createObject(QQC2.Overlay.overlay, {
|
||||
connection: root.connection
|
||||
}) as Kirigami.Dialog).open();
|
||||
}) as Kirigami.Dialog).open()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -154,10 +154,7 @@ Components.AlbumMaximizeComponent {
|
||||
|
||||
onOpened: forceActiveFocus()
|
||||
|
||||
onItemRightClicked: {
|
||||
const event = root.currentRoom.findEvent(root.currentEventId);
|
||||
RoomManager.viewEventMenu(root.QQC2.Overlay.overlay, event, root.currentRoom)
|
||||
}
|
||||
onItemRightClicked: RoomManager.viewEventMenu(root.currentEventId, root.currentRoom)
|
||||
|
||||
onSaveItem: {
|
||||
var dialog = saveAsDialog.createObject(QQC2.Overlay.overlay) as Dialogs.FileDialog;
|
||||
|
||||
@@ -6,7 +6,6 @@ import QtQuick.Controls as QQC2
|
||||
import QtQuick.Layouts
|
||||
|
||||
import org.kde.kirigami as Kirigami
|
||||
import org.kde.neochat
|
||||
|
||||
Kirigami.Page {
|
||||
id: root
|
||||
@@ -14,8 +13,6 @@ Kirigami.Page {
|
||||
required property string placeholder
|
||||
required property string actionText
|
||||
required property string icon
|
||||
required property bool reporting
|
||||
required property NeoChatConnection connection
|
||||
|
||||
signal accepted(reason: string)
|
||||
|
||||
@@ -24,15 +21,6 @@ Kirigami.Page {
|
||||
topPadding: 0
|
||||
bottomPadding: 0
|
||||
|
||||
header: Kirigami.InlineMessage {
|
||||
showCloseButton: false
|
||||
visible: root.reporting
|
||||
type: Kirigami.MessageType.Information
|
||||
position: Kirigami.InlineMessage.Position.Header
|
||||
|
||||
text: xi18n("This report will <strong>only</strong> be sent to the administrators of <link>%1</link> (your server).", root.connection.domain)
|
||||
}
|
||||
|
||||
QQC2.TextArea {
|
||||
id: reason
|
||||
placeholderText: root.placeholder
|
||||
@@ -43,7 +31,7 @@ Kirigami.Page {
|
||||
Keys.onReturnPressed: event => {
|
||||
if (event.modifiers & Qt.ControlModifier) {
|
||||
root.accepted(reason.text);
|
||||
root.Kirigami.PageStack.closeDialog();
|
||||
root.closeDialog();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,14 +52,14 @@ Kirigami.Page {
|
||||
QQC2.DialogButtonBox.buttonRole: QQC2.DialogButtonBox.AcceptRole
|
||||
onClicked: {
|
||||
root.accepted(reason.text);
|
||||
root.Kirigami.PageStack.closeDialog();
|
||||
root.closeDialog();
|
||||
}
|
||||
}
|
||||
QQC2.Button {
|
||||
icon.name: "dialog-cancel-symbolic"
|
||||
text: i18nc("@action", "Cancel")
|
||||
QQC2.DialogButtonBox.buttonRole: QQC2.DialogButtonBox.RejectRole
|
||||
onClicked: root.Kirigami.PageStack.closeDialog()
|
||||
onClicked: root.closeDialog()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,12 +75,6 @@ Kirigami.Page {
|
||||
focus: true
|
||||
padding: 0
|
||||
|
||||
onHeightChanged: {
|
||||
// HACK: See TimelineView for the hack details.
|
||||
// We get the height change here *first* so we are informed this is because of a window resize and not due to the pinned message.
|
||||
(timelineViewLoader.item as TimelineView).resetViewSettling();
|
||||
}
|
||||
|
||||
actions: [
|
||||
Kirigami.Action {
|
||||
id: jitsiMeetingAction
|
||||
@@ -119,7 +113,7 @@ Kirigami.Page {
|
||||
}
|
||||
},
|
||||
Kirigami.Action {
|
||||
visible: Kirigami.Settings.isMobile || !(root.Kirigami.PageStack.pageStack as Kirigami.PageRow)?.wideMode
|
||||
visible: Kirigami.Settings.isMobile || !(root.Kirigami.PageStack.pageStack as Kirigami.PageRow).wideMode
|
||||
icon.name: "view-right-new"
|
||||
onTriggered: (root.QQC2.ApplicationWindow.window as Main).openRoomDrawer()
|
||||
}
|
||||
@@ -295,7 +289,7 @@ Kirigami.Page {
|
||||
|
||||
footer: Loader {
|
||||
id: chatBarLoader
|
||||
height: active ? (item as ChatBar)?.implicitHeight : 0
|
||||
height: active ? (item as ChatBar).implicitHeight : 0
|
||||
active: timelineViewLoader.active && !root.currentRoom.readOnly
|
||||
sourceComponent: ChatBar {
|
||||
id: chatBar
|
||||
@@ -355,17 +349,14 @@ Kirigami.Page {
|
||||
});
|
||||
}
|
||||
|
||||
function onShowDelegateMenu(parent: QtObject, room: NeoChatRoom, eventId: string, author, messageComponentType, plainText: string, richText: string, mimeType: string, progressInfo, selectedText: string, hoveredLink: string) {
|
||||
(delegateContextMenu.createObject(parent, {
|
||||
room: room,
|
||||
function onShowDelegateMenu(eventId: string, author, messageComponentType, plainText: string, richText: string, mimeType: string, progressInfo, isThread: bool, selectedText: string, hoveredLink: string) {
|
||||
(delegateContextMenu.createObject(root, {
|
||||
author: author,
|
||||
eventId: eventId,
|
||||
plainText: plainText,
|
||||
mimeType: mimeType,
|
||||
progressInfo: progressInfo,
|
||||
messageComponentType: messageComponentType,
|
||||
selectedText,
|
||||
hoveredLink,
|
||||
}) as DelegateContextMenu).popup();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,90 +0,0 @@
|
||||
// SPDX-FileCopyrightText: 2026 Joshua Goins <josh@redstrate.com>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls as QQC2
|
||||
import QtQuick.Layouts
|
||||
|
||||
import org.kde.kirigami as Kirigami
|
||||
import org.kde.kirigamiaddons.components as KirigamiComponents
|
||||
import org.kde.kirigamiaddons.delegates as Delegates
|
||||
|
||||
import org.kde.neochat
|
||||
|
||||
Kirigami.Dialog {
|
||||
id: root
|
||||
|
||||
property var model
|
||||
|
||||
standardButtons: Kirigami.Dialog.NoButton
|
||||
|
||||
width: Math.min(QQC2.ApplicationWindow.window.width, Kirigami.Units.gridUnit * 24)
|
||||
maximumHeight: Kirigami.Units.gridUnit * 24
|
||||
title: i18nc("@title:menu Seen by/read marker dialog", "Seen By")
|
||||
|
||||
contentItem: ColumnLayout {
|
||||
spacing: 0
|
||||
|
||||
QQC2.ScrollView {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
|
||||
ListView {
|
||||
id: listView
|
||||
|
||||
model: root.model
|
||||
spacing: Kirigami.Units.smallSpacing
|
||||
|
||||
onCountChanged: {
|
||||
if (listView.count === 0) {
|
||||
root.close();
|
||||
}
|
||||
}
|
||||
|
||||
delegate: Delegates.RoundedItemDelegate {
|
||||
id: userDelegate
|
||||
|
||||
required property string displayName
|
||||
required property url avatarUrl
|
||||
required property color memberColor
|
||||
required property string userId
|
||||
|
||||
implicitHeight: Kirigami.Units.gridUnit * 2
|
||||
|
||||
text: displayName
|
||||
highlighted: false
|
||||
|
||||
onClicked: {
|
||||
root.close();
|
||||
RoomManager.resolveResource(userDelegate.userId);
|
||||
}
|
||||
|
||||
contentItem: RowLayout {
|
||||
spacing: Kirigami.Units.smallSpacing
|
||||
|
||||
KirigamiComponents.Avatar {
|
||||
implicitWidth: height
|
||||
sourceSize {
|
||||
height: Kirigami.Units.gridUnit + Kirigami.Units.smallSpacing * 2.5
|
||||
width: Kirigami.Units.gridUnit + Kirigami.Units.smallSpacing * 2.5
|
||||
}
|
||||
source: userDelegate.avatarUrl
|
||||
name: userDelegate.displayName
|
||||
color: userDelegate.memberColor
|
||||
|
||||
Layout.fillHeight: true
|
||||
}
|
||||
QQC2.Label {
|
||||
text: userDelegate.displayName
|
||||
textFormat: Text.PlainText
|
||||
elide: Text.ElideRight
|
||||
clip: true // Intentional to limit insane Unicode in display names
|
||||
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,121 +0,0 @@
|
||||
// SPDX-FileCopyrightText: 2026 Joshua Goins <josh@redstrate.com>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls as QQC2
|
||||
import QtQuick.Layouts
|
||||
|
||||
import org.kde.kirigami as Kirigami
|
||||
import org.kde.kirigamiaddons.formcard as FormCard
|
||||
|
||||
import org.kde.neochat
|
||||
|
||||
Kirigami.Dialog {
|
||||
id: root
|
||||
|
||||
required property NeoChatConnection connection
|
||||
|
||||
readonly property SupportController supportController: SupportController {
|
||||
connection: root.connection
|
||||
}
|
||||
readonly property bool hasSupportResources: supportController.supportPage.length > 0 && supportController.contacts.length > 0
|
||||
|
||||
title: i18nc("@title Support information", "Support")
|
||||
width: Math.min(Kirigami.Units.gridUnit * 30, QQC2.ApplicationWindow.window.width)
|
||||
|
||||
ColumnLayout {
|
||||
spacing: 0
|
||||
|
||||
FormCard.FormTextDelegate {
|
||||
id: explanationTextDelegate
|
||||
|
||||
text: root.hasSupportResources ?
|
||||
i18nc("@info:label %1 is the domain of the server", "Official support resources provided by %1:", root.connection.domain)
|
||||
: i18nc("@info:label %1 is the domain of the server", "%1 has no support resources.", root.connection.domain)
|
||||
}
|
||||
|
||||
FormCard.FormDelegateSeparator {
|
||||
above: explanationTextDelegate
|
||||
below: openSupportPageDelegate
|
||||
visible: openSupportPageDelegate.visible
|
||||
}
|
||||
|
||||
FormCard.FormLinkDelegate {
|
||||
id: openSupportPageDelegate
|
||||
|
||||
icon.name: "help-contents-symbolic"
|
||||
text: i18nc("@action:button Open support webpage", "Open Support")
|
||||
url: root.supportController.supportPage
|
||||
visible: root.supportController.supportPage.length > 0
|
||||
}
|
||||
|
||||
FormCard.FormDelegateSeparator {
|
||||
above: openSupportPageDelegate
|
||||
visible: root.supportController.contacts.length > 0
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: root.supportController.contacts
|
||||
|
||||
delegate: FormCard.AbstractFormDelegate {
|
||||
id: contactDelegate
|
||||
|
||||
required property string role
|
||||
required property string matrixId
|
||||
required property string emailAddress
|
||||
|
||||
background: null
|
||||
|
||||
Layout.fillWidth: true
|
||||
|
||||
contentItem: RowLayout {
|
||||
spacing: Kirigami.Units.largeSpacing
|
||||
|
||||
Kirigami.Icon {
|
||||
source: "user"
|
||||
}
|
||||
|
||||
QQC2.Label {
|
||||
text: {
|
||||
// Translate known keys
|
||||
if (contactDelegate.role === "m.role.admin") {
|
||||
return i18nc("@info:label Adminstrator contact", "Admin")
|
||||
} else if (contactDelegate.role === "m.role.security") {
|
||||
return i18nc("@info:label Security contact", "Security")
|
||||
}
|
||||
return contactDelegate.role;
|
||||
}
|
||||
elide: Text.ElideRight
|
||||
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
QQC2.ToolButton {
|
||||
visible: contactDelegate.matrixId.length > 0
|
||||
icon.name: "document-send-symbolic"
|
||||
|
||||
onClicked: {
|
||||
root.close();
|
||||
root.connection.requestDirectChat(contactDelegate.matrixId);
|
||||
}
|
||||
|
||||
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||
QQC2.ToolTip.visible: hovered
|
||||
QQC2.ToolTip.text: i18nc("@info:tooltip %1 is a Matrix ID", "Contact via Matrix (%1)", contactDelegate.matrixId)
|
||||
}
|
||||
|
||||
QQC2.ToolButton {
|
||||
visible: contactDelegate.emailAddress.length > 0
|
||||
icon.name: "mail-sent-symbolic"
|
||||
|
||||
onClicked: Qt.openUrlExternally("mailto:%1".arg(contactDelegate.emailAddress))
|
||||
|
||||
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||
QQC2.ToolTip.visible: hovered
|
||||
QQC2.ToolTip.text: i18nc("@info:tooltip %1 is an e-mail address", "Contact via e-mail (%1)", contactDelegate.emailAddress)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,7 @@ FormCard.FormCardPage {
|
||||
|
||||
property bool processing: false
|
||||
|
||||
title: i18nc("@title:window", "Manage Secret Backup")
|
||||
title: i18nc("@title:window", "Load your encrypted messages")
|
||||
|
||||
topPadding: Kirigami.Units.gridUnit
|
||||
leftPadding: 0
|
||||
|
||||
@@ -176,11 +176,9 @@ Kirigami.Dialog {
|
||||
onTriggered: {
|
||||
let dialog = ((root.QQC2.ApplicationWindow.window as Kirigami.ApplicationWindow).pageStack as Kirigami.PageRow).pushDialogLayer(Qt.createComponent('org.kde.neochat', 'ReasonDialog'), {
|
||||
title: i18nc("@title:dialog", "Report User"),
|
||||
placeholder: i18nc("@info:placeholder", "Optionally give a reason for reporting this user"),
|
||||
placeholder: i18nc("@info:placeholder", "Reason for reporting this user"),
|
||||
icon: "dialog-warning-symbolic",
|
||||
actionText: i18nc("@action:button 'Report' as in 'Report this user to the administrators'", "Report"),
|
||||
reporting: true,
|
||||
connection: root.connection,
|
||||
actionText: i18nc("@action:button 'Report' as in 'Report this user to the administrators'", "Report")
|
||||
}, {
|
||||
title: i18nc("@title", "Report User"),
|
||||
width: Kirigami.Units.gridUnit * 25
|
||||
@@ -234,23 +232,16 @@ Kirigami.Dialog {
|
||||
|
||||
actions: [
|
||||
Kirigami.Action {
|
||||
visible: {
|
||||
if (root.room) {
|
||||
return !root.isSelf && root.room.canSendState("kick") && root.room.containsUser(root.user.id) && root.room.memberEffectivePowerLevel(root.user.id) < root.room.memberEffectivePowerLevel(root.connection.localUserId);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
visible: !root.isSelf && root.room.canSendState("kick") && root.room.containsUser(root.user.id) && root.room.memberEffectivePowerLevel(root.user.id) < root.room.memberEffectivePowerLevel(root.connection.localUserId)
|
||||
|
||||
text: i18nc("@action:button Kick the user from the room", "Kick…")
|
||||
icon.name: "im-kick-user"
|
||||
onTriggered: {
|
||||
let dialog = (root.QQC2.ApplicationWindow.window as Kirigami.ApplicationWindow).pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat', 'ReasonDialog'), {
|
||||
title: i18nc("@title:dialog", "Kick User"),
|
||||
placeholder: i18nc("@info:placeholder", "Optionally give a reason for kicking this user"),
|
||||
placeholder: i18nc("@info:placeholder", "Reason for kicking this user"),
|
||||
actionText: i18nc("@action:button 'Kick' as in 'Kick this user from the room'", "Kick"),
|
||||
icon: "im-kick-user",
|
||||
reporting: false,
|
||||
connection: root.connection,
|
||||
icon: "im-kick-user"
|
||||
}, {
|
||||
title: i18nc("@title:dialog", "Kick User"),
|
||||
width: Kirigami.Units.gridUnit * 25
|
||||
@@ -262,12 +253,7 @@ Kirigami.Dialog {
|
||||
}
|
||||
},
|
||||
Kirigami.Action {
|
||||
visible: {
|
||||
if (root.room) {
|
||||
return !root.isSelf && root.room.canSendState("ban") && !root.room.isUserBanned(root.user.id) && root.room.memberEffectivePowerLevel(root.user.id) < root.room.memberEffectivePowerLevel(root.connection.localUserId);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
visible: !root.isSelf && root.room.canSendState("ban") && !root.room.isUserBanned(root.user.id) && root.room.memberEffectivePowerLevel(root.user.id) < root.room.memberEffectivePowerLevel(root.connection.localUserId)
|
||||
|
||||
text: i18nc("@action:button Ban this user from the room", "Ban…")
|
||||
icon.name: "im-ban-user"
|
||||
@@ -275,11 +261,9 @@ Kirigami.Dialog {
|
||||
onTriggered: {
|
||||
let dialog = (root.QQC2.ApplicationWindow.window as Kirigami.ApplicationWindow).pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat', 'ReasonDialog'), {
|
||||
title: i18nc("@title:dialog", "Ban User"),
|
||||
placeholder: i18nc("@info:placeholder", "Optionally give a reason for banning this user"),
|
||||
placeholder: i18nc("@info:placeholder", "Reason for banning this user"),
|
||||
actionText: i18nc("@action:button 'Ban' as in 'Ban this user'", "Ban"),
|
||||
icon: "im-ban-user",
|
||||
reporting: false,
|
||||
connection: root.connection,
|
||||
icon: "im-ban-user"
|
||||
}, {
|
||||
title: i18nc("@title:dialog", "Ban User"),
|
||||
width: Kirigami.Units.gridUnit * 25
|
||||
@@ -291,12 +275,7 @@ Kirigami.Dialog {
|
||||
}
|
||||
},
|
||||
Kirigami.Action {
|
||||
visible: {
|
||||
if (root.room) {
|
||||
return !root.isSelf && root.room.canSendState("ban") && root.room.isUserBanned(root.user.id);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
visible: !root.isSelf && root.room.canSendState("ban") && root.room.isUserBanned(root.user.id)
|
||||
|
||||
text: i18nc("@action:button Unban the user from this room", "Unban")
|
||||
icon.name: "im-irc"
|
||||
@@ -307,7 +286,7 @@ Kirigami.Dialog {
|
||||
}
|
||||
},
|
||||
Kirigami.Action {
|
||||
visible: (root.user.id === root.connection.localUserId || (root.room?.canSendState("redact") ?? false))
|
||||
visible: (root.user.id === root.connection.localUserId || root.room.canSendState("redact"))
|
||||
|
||||
text: i18nc("@action:button Remove messages from the user in this room", "Remove Messages…")
|
||||
icon.name: "delete"
|
||||
@@ -315,11 +294,9 @@ Kirigami.Dialog {
|
||||
onTriggered: {
|
||||
let dialog = ((root.QQC2.ApplicationWindow.window as Kirigami.ApplicationWindow).pageStack as Kirigami.PageRow).pushDialogLayer(Qt.createComponent('org.kde.neochat', 'ReasonDialog'), {
|
||||
title: i18nc("@title:dialog", "Remove Messages"),
|
||||
placeholder: i18nc("@info:placeholder", "Optionally give a reason for removing this user's recent messages"),
|
||||
placeholder: i18nc("@info:placeholder", "Reason for removing this user's recent messages"),
|
||||
actionText: i18nc("@action:button 'Remove' as in 'Remove these messages'", "Remove"),
|
||||
icon: "delete",
|
||||
reporting: false,
|
||||
connection: root.connection,
|
||||
icon: "delete"
|
||||
}, {
|
||||
title: i18nc("@title", "Remove Messages"),
|
||||
width: Kirigami.Units.gridUnit * 25
|
||||
@@ -334,7 +311,7 @@ Kirigami.Dialog {
|
||||
}
|
||||
|
||||
Kirigami.Heading {
|
||||
text: i18nc("@title Role such as 'Admin' or 'Moderator' for this user", "Power Level")
|
||||
text: i18nc("@title Role such as 'Admin' or 'Moderator' for this user", "Role")
|
||||
level: 2
|
||||
visible: root.isRoomProfile
|
||||
|
||||
@@ -352,12 +329,7 @@ Kirigami.Dialog {
|
||||
}
|
||||
|
||||
QQC2.Button {
|
||||
visible: {
|
||||
if (root.room) {
|
||||
return root.room.canSendState("m.room.power_levels") && !(root.room.roomCreatorHasUltimatePowerLevel() && root.room.isCreator(root.user.id));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
visible: root.room.canSendState("m.room.power_levels") && !(root.room.roomCreatorHasUltimatePowerLevel() && root.room.isCreator(root.user.id))
|
||||
text: i18nc("@action:button Set the power level (such as 'Admin') for this user", "Set Power Level")
|
||||
icon.name: "document-edit-symbolic"
|
||||
display: QQC2.AbstractButton.IconOnly
|
||||
@@ -429,34 +401,5 @@ Kirigami.Dialog {
|
||||
color: Kirigami.Theme.disabledTextColor
|
||||
}
|
||||
}
|
||||
|
||||
Kirigami.Heading {
|
||||
text: i18nc("@title Private note for this user", "Private Note")
|
||||
level: 4
|
||||
|
||||
Layout.topMargin: Kirigami.Units.largeSpacing
|
||||
}
|
||||
|
||||
QQC2.TextArea {
|
||||
id: noteText
|
||||
|
||||
text: root.connection.noteForUser(root.user.id)
|
||||
textFormat: TextEdit.PlainText
|
||||
wrapMode: TextEdit.Wrap
|
||||
placeholderText: i18nc("@info:placeholder", "Only visible to you")
|
||||
|
||||
onTextEdited: editTimer.restart()
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: Kirigami.Units.smallSpacing
|
||||
|
||||
// Prevent unnecessary edits by waiting 1 second
|
||||
Timer {
|
||||
id: editTimer
|
||||
|
||||
interval: 1000
|
||||
onTriggered: root.connection.setNoteForUser(root.user.id, noteText.text)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,28 +29,6 @@
|
||||
#include <KIO/OpenUrlJob>
|
||||
#endif
|
||||
|
||||
/**
|
||||
* @brief Stops RoomManager from updating the last room and space config.
|
||||
*/
|
||||
class LastRoomBlocker
|
||||
{
|
||||
public:
|
||||
explicit LastRoomBlocker(RoomManager *manager)
|
||||
: m_manager(manager)
|
||||
{
|
||||
Q_ASSERT(manager);
|
||||
|
||||
m_manager->m_dontUpdateLastRoom = true;
|
||||
}
|
||||
~LastRoomBlocker()
|
||||
{
|
||||
m_manager->m_dontUpdateLastRoom = false;
|
||||
}
|
||||
|
||||
private:
|
||||
RoomManager *m_manager;
|
||||
};
|
||||
|
||||
RoomManager::RoomManager(QObject *parent)
|
||||
: QObject(parent)
|
||||
, m_config(KSharedConfig::openStateConfig())
|
||||
@@ -304,22 +282,26 @@ void RoomManager::viewEventSource(const QString &eventId)
|
||||
Q_EMIT showEventSource(eventId);
|
||||
}
|
||||
|
||||
void RoomManager::viewEventMenu(QObject *parent, const RoomEvent *event, NeoChatRoom *room, const QString &selectedText, const QString &hoveredLink)
|
||||
void RoomManager::viewEventMenu(const QString &eventId, NeoChatRoom *room, const QString &selectedText, const QString &hoveredLink)
|
||||
{
|
||||
if (!event) {
|
||||
qWarning() << "Tried to open event menu with empty event";
|
||||
if (eventId.isEmpty()) {
|
||||
qWarning() << "Tried to open event menu with empty event id";
|
||||
return;
|
||||
}
|
||||
|
||||
Q_EMIT showDelegateMenu(parent,
|
||||
room,
|
||||
event->id(),
|
||||
room->qmlSafeMember(event->senderId()),
|
||||
MessageComponentType::typeForEvent(*event),
|
||||
EventHandler::plainBody(room, event),
|
||||
EventHandler::richBody(room, event),
|
||||
EventHandler::mediaInfo(room, event)["mimeType"_L1].toString(),
|
||||
room->fileTransferInfo(event->id()),
|
||||
const auto it = room->findInTimeline(eventId);
|
||||
if (it == room->historyEdge()) {
|
||||
// This is probably a pending event
|
||||
return;
|
||||
}
|
||||
const auto &event = **it;
|
||||
Q_EMIT showDelegateMenu(eventId,
|
||||
room->qmlSafeMember(event.senderId()),
|
||||
MessageComponentType::typeForEvent(event),
|
||||
EventHandler::plainBody(room, &event),
|
||||
EventHandler::richBody(room, &event),
|
||||
EventHandler::mediaInfo(room, &event)["mimeType"_L1].toString(),
|
||||
room->fileTransferInfo(eventId),
|
||||
selectedText,
|
||||
hoveredLink);
|
||||
}
|
||||
@@ -342,6 +324,17 @@ void RoomManager::loadInitialRoom()
|
||||
resolveResource(m_arg);
|
||||
}
|
||||
|
||||
if (m_isMobile) {
|
||||
QString lastSpace = m_lastRoomConfig.readEntry(u"lastSpace"_s, QString());
|
||||
// We can't have empty keys in KConfig, so we stored it as "Home"
|
||||
if (lastSpace == u"Home"_s) {
|
||||
lastSpace.clear();
|
||||
}
|
||||
setCurrentSpace(lastSpace, false);
|
||||
// We don't want to open a room on startup on mobile
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_currentRoom) {
|
||||
// we opened a room with the arg parsing already
|
||||
return;
|
||||
@@ -354,14 +347,16 @@ void RoomManager::loadInitialRoom()
|
||||
|
||||
void RoomManager::openRoomForActiveConnection()
|
||||
{
|
||||
Q_ASSERT(m_connection);
|
||||
|
||||
if (!m_connection) {
|
||||
setCurrentRoom({});
|
||||
setCurrentSpace({}, false);
|
||||
return;
|
||||
}
|
||||
auto lastSpace = m_lastRoomConfig.readEntry(u"lastSpace"_s, QString());
|
||||
if (lastSpace == u"Home"_s) {
|
||||
lastSpace.clear();
|
||||
}
|
||||
// We don't want to open a room on startup on mobile
|
||||
setCurrentSpace(lastSpace, !m_isMobile);
|
||||
setCurrentSpace(lastSpace, true);
|
||||
}
|
||||
|
||||
UriResolveResult RoomManager::visitUser(User *user, const QString &action)
|
||||
@@ -518,7 +513,7 @@ void RoomManager::setConnection(NeoChatConnection *connection)
|
||||
Q_EMIT connectionChanged();
|
||||
}
|
||||
|
||||
void RoomManager::setCurrentSpace(const QString &spaceId, bool goToLastUsedRoom)
|
||||
void RoomManager::setCurrentSpace(const QString &spaceId, bool setRoom)
|
||||
{
|
||||
m_currentSpaceId = spaceId;
|
||||
|
||||
@@ -538,26 +533,25 @@ void RoomManager::setCurrentSpace(const QString &spaceId, bool goToLastUsedRoom)
|
||||
m_lastRoomConfig.writeEntry(u"lastSpace"_s, spaceId.isEmpty() ? u"Home"_s : spaceId);
|
||||
}
|
||||
|
||||
// If we requested to change to the last opened room, do so:
|
||||
if (goToLastUsedRoom) {
|
||||
// We don't want to needlessly update the last room config here, that should only be done during explicit user action.
|
||||
LastRoomBlocker blocker(this);
|
||||
|
||||
// We can't have empty keys in KConfig, so it's stored as "Home":
|
||||
if (const auto &lastRoom = m_lastRoomConfig.readEntry(spaceId.isEmpty() ? u"Home"_s : spaceId, QString()); !lastRoom.isEmpty()) {
|
||||
resolveResource(lastRoom, "no_join"_L1);
|
||||
return;
|
||||
}
|
||||
|
||||
// If no last room was opened, go to the space home:
|
||||
if (!spaceId.isEmpty() && spaceId != u"DM"_s) {
|
||||
resolveResource(spaceId, "no_join"_L1);
|
||||
return;
|
||||
}
|
||||
|
||||
// Fallback to no room opened:
|
||||
setCurrentRoom({});
|
||||
if (!setRoom) {
|
||||
return;
|
||||
}
|
||||
|
||||
// We intentionally don't want to open the last room on mobile
|
||||
if (m_isMobile) {
|
||||
return;
|
||||
}
|
||||
|
||||
// We can't have empty keys in KConfig, so it's stored as "Home"
|
||||
if (const auto &lastRoom = m_lastRoomConfig.readEntry(spaceId.isEmpty() ? u"Home"_s : spaceId, QString()); !lastRoom.isEmpty()) {
|
||||
resolveResource(lastRoom, "no_join"_L1);
|
||||
return;
|
||||
}
|
||||
if (!spaceId.isEmpty() && spaceId != u"DM"_s) {
|
||||
resolveResource(spaceId, "no_join"_L1);
|
||||
return;
|
||||
}
|
||||
setCurrentRoom({});
|
||||
}
|
||||
|
||||
QString RoomManager::findSpaceIdForCurrentRoom() const
|
||||
@@ -617,23 +611,21 @@ void RoomManager::setCurrentRoom(const QString &roomId)
|
||||
|
||||
Q_EMIT currentRoomChanged();
|
||||
|
||||
if (!m_dontUpdateLastRoom) {
|
||||
if (roomId.isEmpty()) {
|
||||
m_lastRoomConfig.deleteEntry(m_currentSpaceId);
|
||||
return;
|
||||
}
|
||||
if (roomId.isEmpty()) {
|
||||
m_lastRoomConfig.deleteEntry(m_currentSpaceId);
|
||||
return;
|
||||
}
|
||||
|
||||
const auto spaceIdForRoom = findSpaceIdForCurrentRoom();
|
||||
// We can't have empty keys in KConfig, so name it "Home"
|
||||
if (spaceIdForRoom.isEmpty()) {
|
||||
m_lastRoomConfig.writeEntry(u"Home"_s, roomId);
|
||||
} else {
|
||||
m_lastRoomConfig.writeEntry(spaceIdForRoom, roomId);
|
||||
}
|
||||
const auto spaceIdForRoom = findSpaceIdForCurrentRoom();
|
||||
// We can't have empty keys in KConfig, so name it "Home"
|
||||
if (spaceIdForRoom.isEmpty()) {
|
||||
m_lastRoomConfig.writeEntry(u"Home"_s, roomId);
|
||||
} else {
|
||||
m_lastRoomConfig.writeEntry(spaceIdForRoom, roomId);
|
||||
}
|
||||
|
||||
if (m_currentSpaceId != spaceIdForRoom) {
|
||||
setCurrentSpace(spaceIdForRoom, false);
|
||||
}
|
||||
if (m_currentSpaceId != spaceIdForRoom) {
|
||||
setCurrentSpace(spaceIdForRoom, false);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -233,8 +233,7 @@ public:
|
||||
/**
|
||||
* @brief Show a context menu for the given event.
|
||||
*/
|
||||
Q_INVOKABLE void
|
||||
viewEventMenu(QObject *parent, const RoomEvent *event, NeoChatRoom *room, const QString &selectedText = {}, const QString &hoveredLink = {});
|
||||
Q_INVOKABLE void viewEventMenu(const QString &eventId, NeoChatRoom *room, const QString &selectedText = {}, const QString &hoveredLink = {});
|
||||
|
||||
/**
|
||||
* @brief Set a URL to be loaded as the initial room.
|
||||
@@ -307,9 +306,7 @@ Q_SIGNALS:
|
||||
/**
|
||||
* @brief Request to show a menu for the given event.
|
||||
*/
|
||||
void showDelegateMenu(QObject *parent,
|
||||
NeoChatRoom *room,
|
||||
const QString &eventId,
|
||||
void showDelegateMenu(const QString &eventId,
|
||||
const NeochatRoomMember *author,
|
||||
MessageComponentType::Type messageComponentType,
|
||||
const QString &plainText,
|
||||
@@ -340,11 +337,6 @@ Q_SIGNALS:
|
||||
|
||||
void currentSpaceChanged();
|
||||
|
||||
protected:
|
||||
bool m_dontUpdateLastRoom = false; // Don't set directly, use LastRoomBlocker.
|
||||
|
||||
friend class LastRoomBlocker;
|
||||
|
||||
private:
|
||||
bool m_isMobile = false;
|
||||
|
||||
@@ -390,13 +382,8 @@ private:
|
||||
*/
|
||||
QString findSpaceIdForCurrentRoom() const;
|
||||
|
||||
/**
|
||||
* @brief Sets the current space.
|
||||
*
|
||||
* @param spaceId The ID of the space, "DM" for direct messages or an empty string for Home.
|
||||
* @param goToLastUsedRoom If true, we will navigate to the last opened room in this space.
|
||||
*/
|
||||
void setCurrentSpace(const QString &spaceId, bool goToLastUsedRoom = true);
|
||||
// Space ID, "DM", or empty string
|
||||
void setCurrentSpace(const QString &spaceId, bool setRoom = true);
|
||||
|
||||
/**
|
||||
* @brief Resolve a user URI.
|
||||
|
||||
@@ -1,66 +0,0 @@
|
||||
// SPDX-FileCopyrightText: 2026 Joshua Goins <josh@redstrate.com>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
#include "supportcontroller.h"
|
||||
|
||||
#include <Quotient/csapi/support.h>
|
||||
|
||||
#include <QDebug>
|
||||
|
||||
using namespace Quotient;
|
||||
|
||||
void SupportController::setConnection(NeoChatConnection *connection)
|
||||
{
|
||||
if (m_connection != connection) {
|
||||
m_connection = connection;
|
||||
Q_EMIT connectionChanged();
|
||||
|
||||
load();
|
||||
}
|
||||
}
|
||||
|
||||
NeoChatConnection *SupportController::connection() const
|
||||
{
|
||||
return m_connection;
|
||||
}
|
||||
|
||||
QString SupportController::supportPage() const
|
||||
{
|
||||
return m_supportPage;
|
||||
}
|
||||
|
||||
QList<SupportContact> SupportController::contacts() const
|
||||
{
|
||||
return m_contacts;
|
||||
}
|
||||
|
||||
void SupportController::load()
|
||||
{
|
||||
if (!m_connection) {
|
||||
qWarning() << "Tried to load support information without a valid connection?";
|
||||
return;
|
||||
}
|
||||
|
||||
m_connection->callApi<GetWellknownSupportJob>()
|
||||
.onResult([this](const auto &job) {
|
||||
m_supportPage = job->supportPage();
|
||||
m_contacts.reserve(job->contacts().size());
|
||||
for (const auto &contact : job->contacts()) {
|
||||
m_contacts.push_back(SupportContact{
|
||||
.role = contact.role,
|
||||
.matrixId = contact.matrixId,
|
||||
.emailAddress = contact.emailAddress,
|
||||
});
|
||||
}
|
||||
|
||||
Q_EMIT loaded();
|
||||
})
|
||||
.onFailure([this](const auto &job) {
|
||||
Q_UNUSED(job)
|
||||
|
||||
// Just do nothing, our properties will be empty.
|
||||
Q_EMIT loaded();
|
||||
});
|
||||
}
|
||||
|
||||
#include "moc_supportcontroller.cpp"
|
||||
@@ -1,48 +0,0 @@
|
||||
// SPDX-FileCopyrightText: 2026 Joshua Goins <josh@redstrate.com>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "neochatconnection.h"
|
||||
|
||||
class SupportContact
|
||||
{
|
||||
Q_GADGET
|
||||
|
||||
Q_PROPERTY(QString role MEMBER role)
|
||||
Q_PROPERTY(QString matrixId MEMBER matrixId)
|
||||
Q_PROPERTY(QString emailAddress MEMBER emailAddress)
|
||||
|
||||
public:
|
||||
QString role;
|
||||
QString matrixId;
|
||||
QString emailAddress;
|
||||
};
|
||||
|
||||
class SupportController : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
QML_ELEMENT
|
||||
|
||||
Q_PROPERTY(NeoChatConnection *connection READ connection WRITE setConnection NOTIFY connectionChanged REQUIRED)
|
||||
Q_PROPERTY(QString supportPage READ supportPage NOTIFY loaded)
|
||||
Q_PROPERTY(QList<SupportContact> contacts READ contacts NOTIFY loaded)
|
||||
|
||||
public:
|
||||
void setConnection(NeoChatConnection *connection);
|
||||
NeoChatConnection *connection() const;
|
||||
|
||||
QString supportPage() const;
|
||||
QList<SupportContact> contacts() const;
|
||||
|
||||
Q_SIGNALS:
|
||||
void connectionChanged();
|
||||
void loaded();
|
||||
|
||||
private:
|
||||
void load();
|
||||
|
||||
QPointer<NeoChatConnection> m_connection = nullptr;
|
||||
QList<SupportContact> m_contacts;
|
||||
QString m_supportPage;
|
||||
};
|
||||
@@ -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,12 +150,7 @@ Quotient::RoomMember ChatBarCache::relationAuthor() const
|
||||
if (m_relationId.isEmpty()) {
|
||||
return room->member(QString());
|
||||
}
|
||||
const auto [event, _] = room->getEvent(m_relationId);
|
||||
if (event != nullptr) {
|
||||
return room->member(event->senderId());
|
||||
}
|
||||
qWarning() << "Failed to find relation" << m_relationId << "in timeline?";
|
||||
return room->member(QString());
|
||||
return room->member((*room->findInTimeline(m_relationId))->senderId());
|
||||
}
|
||||
|
||||
bool ChatBarCache::relationAuthorIsPresent() const
|
||||
@@ -178,8 +173,8 @@ QString ChatBarCache::relationMessage() const
|
||||
return {};
|
||||
}
|
||||
|
||||
if (auto [event, _] = room->getEvent(m_relationId); event != nullptr) {
|
||||
return EventHandler::markdownBody(event);
|
||||
if (auto event = room->findInTimeline(m_relationId); event != room->historyEdge()) {
|
||||
return EventHandler::markdownBody(&**event);
|
||||
}
|
||||
return {};
|
||||
}
|
||||
@@ -285,6 +280,11 @@ void ChatBarCache::postMessage()
|
||||
return;
|
||||
}
|
||||
|
||||
const auto replyIt = room->findInTimeline(replyId());
|
||||
if (replyIt == room->historyEdge()) {
|
||||
isReply = false;
|
||||
}
|
||||
|
||||
auto content = std::make_unique<Quotient::EventContent::TextContent>(sendText, u"text/html"_s);
|
||||
|
||||
room->post<Quotient::RoomMessageEvent>(text(), *std::get<std::optional<Quotient::RoomMessageEvent::MsgType>>(result), std::move(content), relatesTo);
|
||||
|
||||
@@ -619,21 +619,4 @@ bool NeoChatConnection::supportsMatrixSpecVersion(const QString &version)
|
||||
return supportedMatrixSpecVersions().contains(version);
|
||||
}
|
||||
|
||||
QString NeoChatConnection::noteForUser(const QString &userId)
|
||||
{
|
||||
const auto object = accountDataJson(QStringLiteral("org.kde.neochat.user_note"));
|
||||
return object[userId].toString();
|
||||
}
|
||||
|
||||
void NeoChatConnection::setNoteForUser(const QString &userId, const QString ¬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,16 +234,6 @@ public:
|
||||
*/
|
||||
Q_INVOKABLE bool supportsMatrixSpecVersion(const QString &version);
|
||||
|
||||
/**
|
||||
* @return The private note for this user, if set.
|
||||
*/
|
||||
Q_INVOKABLE QString noteForUser(const QString &userId);
|
||||
|
||||
/**
|
||||
* @brief Sets the private note for this user.
|
||||
*/
|
||||
Q_INVOKABLE void setNoteForUser(const QString &userId, const QString ¬e);
|
||||
|
||||
Q_SIGNALS:
|
||||
void globalUrlPreviewEnabledChanged();
|
||||
void labelChanged();
|
||||
|
||||
@@ -1186,10 +1186,7 @@ void NeoChatRoom::loadPinnedMessage()
|
||||
connection()->callApi<GetOneRoomEventJob>(id(), mostRecentEventId).then([this](const auto &job) {
|
||||
auto event = fromJson<event_ptr_tt<RoomEvent>>(job->jsonData());
|
||||
if (auto encEv = eventCast<EncryptedEvent>(event.get())) {
|
||||
auto decryptedMessage = decryptMessage(*encEv);
|
||||
if (decryptedMessage) {
|
||||
event = std::move(decryptedMessage);
|
||||
}
|
||||
event = decryptMessage(*encEv);
|
||||
}
|
||||
m_pinnedMessage = EventHandler::richBody(this, event.get());
|
||||
Q_EMIT pinnedMessageChanged();
|
||||
@@ -1657,12 +1654,6 @@ void NeoChatRoom::downloadEventFromServer(const QString &eventId)
|
||||
}
|
||||
|
||||
event_ptr_tt<RoomEvent> event = fromJson<event_ptr_tt<RoomEvent>>(job->jsonData());
|
||||
if (auto encEv = eventCast<EncryptedEvent>(event.get())) {
|
||||
auto decryptedEvent = decryptMessage(*encEv);
|
||||
if (decryptedEvent) {
|
||||
event = std::move(decryptedEvent);
|
||||
}
|
||||
}
|
||||
m_extraEvents.push_back(std::move(event));
|
||||
Q_EMIT extraEventLoaded(eventId);
|
||||
},
|
||||
@@ -1700,11 +1691,6 @@ std::pair<const Quotient::RoomEvent *, bool> NeoChatRoom::getEvent(const QString
|
||||
return std::make_pair(extraIt != m_extraEvents.end() ? extraIt->get() : nullptr, false);
|
||||
}
|
||||
|
||||
const RoomEvent *NeoChatRoom::findEvent(const QString &eventId) const
|
||||
{
|
||||
return getEvent(eventId).first;
|
||||
}
|
||||
|
||||
const RoomEvent *NeoChatRoom::getReplyForEvent(const RoomEvent &event) const
|
||||
{
|
||||
#if Quotient_VERSION_MINOR > 9
|
||||
|
||||
@@ -538,13 +538,6 @@ public:
|
||||
*/
|
||||
std::pair<const Quotient::RoomEvent *, bool> getEvent(const QString &eventId) const;
|
||||
|
||||
/**
|
||||
* @brief Returns the event object with the given ID if available.
|
||||
*
|
||||
* This function works identically to getEvent, except this is usable from QML.
|
||||
*/
|
||||
Q_INVOKABLE const Quotient::RoomEvent *findEvent(const QString &eventId) const;
|
||||
|
||||
/**
|
||||
* @brief Returns the event that is being replied to. This includes events that were manually loaded using NeoChatRoom::loadReply.
|
||||
*/
|
||||
|
||||
@@ -130,10 +130,7 @@ QQC2.Control {
|
||||
|
||||
TapHandler {
|
||||
acceptedDevices: PointerDevice.TouchScreen
|
||||
onLongPressed: {
|
||||
const event = root.Message.room.findEvent(root.eventId);
|
||||
RoomManager.viewEventMenu(root.QQC2.Overlay.overlay, event, root.Message.room, root.Message.selectedText, root.Message.hoveredLink);
|
||||
}
|
||||
onLongPressed: RoomManager.viewEventMenu(root.eventId, root.Message.room, root.Message.selectedText, root.Message.hoveredLink);
|
||||
}
|
||||
|
||||
background: null
|
||||
|
||||
@@ -66,10 +66,7 @@ QQC2.Control {
|
||||
enabled: !quoteText.hoveredLink
|
||||
acceptedDevices: PointerDevice.TouchScreen
|
||||
acceptedButtons: Qt.LeftButton
|
||||
onLongPressed: {
|
||||
const event = root.Message.room.findEvent(root.eventId);
|
||||
RoomManager.viewEventMenu(root.QQC2.Overlay.overlay, event, root.Message.room, root.Message.selectedText, root.Message.hoveredLink);
|
||||
}
|
||||
onLongPressed: RoomManager.viewEventMenu(root.eventId, root.Message.room, root.Message.selectedText, root.Message.hoveredLink);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ RowLayout {
|
||||
Layout.fillHeight: true
|
||||
|
||||
implicitWidth: Kirigami.Units.smallSpacing
|
||||
color: root.replyContentModel.author?.color ?? Kirigami.Theme.highlightColor
|
||||
color: root.replyContentModel.author.color
|
||||
radius: Kirigami.Units.cornerRadius
|
||||
}
|
||||
ColumnLayout {
|
||||
|
||||
@@ -86,12 +86,11 @@ RowLayout {
|
||||
QtObject {
|
||||
id: _private
|
||||
|
||||
function showMessageMenu(): void {
|
||||
function showMessageMenu() {
|
||||
if (!NeoChatConfig.developerTools) {
|
||||
return;
|
||||
}
|
||||
const event = root.Message.room.findEvent(root.modelData.eventId);
|
||||
RoomManager.viewEventMenu(root.QQC2.Overlay.overlay, event, root.Message.room, root.author);
|
||||
RoomManager.viewEventMenu(root.modelData.eventId, root.Message.room, root.author, "", "");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -97,18 +97,12 @@ TextEdit {
|
||||
enabled: !root.hoveredLink
|
||||
acceptedButtons: Qt.LeftButton
|
||||
acceptedDevices: PointerDevice.TouchScreen
|
||||
onLongPressed: {
|
||||
const event = root.Message.room.findEvent(root.eventId);
|
||||
RoomManager.viewEventMenu(root.QQC2.Overlay.overlay, event, root.Message.room, root.Message.selectedText, root.Message.hoveredLink);
|
||||
}
|
||||
onLongPressed: RoomManager.viewEventMenu(root.eventId, root.Message.room, root.Message.selectedText, root.Message.hoveredLink);
|
||||
}
|
||||
TapHandler {
|
||||
acceptedButtons: Qt.RightButton
|
||||
acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad | PointerDevice.Stylus
|
||||
gesturePolicy: TapHandler.WithinBounds
|
||||
onTapped: {
|
||||
const event = root.Message.room.findEvent(root.eventId);
|
||||
RoomManager.viewEventMenu(root.QQC2.Overlay.overlay, event, root.Message.room, root.Message.selectedText, root.Message.hoveredLink);
|
||||
}
|
||||
onTapped: RoomManager.viewEventMenu(root.eventId, root.Message.room, root.Message.selectedText, root.Message.hoveredLink);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -269,8 +269,6 @@ void EventMessageContentModel::resetModel()
|
||||
updateItineraryModel();
|
||||
|
||||
Q_EMIT componentsUpdated();
|
||||
// We need QML to re-evaluate author (for example, reply colors) if it was previously null.
|
||||
Q_EMIT authorChanged();
|
||||
}
|
||||
|
||||
void EventMessageContentModel::resetContent(bool isEditing, bool isThreading)
|
||||
|
||||
@@ -87,6 +87,7 @@ QDateTime MessageContentModel::time() const
|
||||
QString MessageContentModel::timeString() const
|
||||
{
|
||||
return time().toLocalTime().toString(u"hh:mm"_s);
|
||||
;
|
||||
}
|
||||
|
||||
QString MessageContentModel::authorId() const
|
||||
|
||||
@@ -8,7 +8,6 @@ import org.kde.kirigami as Kirigami
|
||||
|
||||
import org.kde.neochat.libneochat
|
||||
import org.kde.neochat.timeline as Timeline
|
||||
import org.kde.neochat.settings as Settings
|
||||
|
||||
/**
|
||||
* @brief Page for holding a room drawer component.
|
||||
@@ -50,7 +49,7 @@ Kirigami.Page {
|
||||
text: i18nc("@action:button", "Room settings")
|
||||
icon.name: 'settings-configure-symbolic'
|
||||
onTriggered: {
|
||||
Settings.RoomSettingsView.openRoomSettings(root.room, Settings.RoomSettingsView.Room);
|
||||
RoomSettingsView.openRoomSettings(root.room, RoomSettingsView.Room);
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -52,6 +52,7 @@ QQC2.ScrollView {
|
||||
delegate: Timeline.MessageDelegate {
|
||||
alwaysFillWidth: true
|
||||
cardBackground: false
|
||||
room: root.room
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,6 +61,7 @@ QQC2.ScrollView {
|
||||
delegate: Timeline.MessageDelegate {
|
||||
alwaysFillWidth: true
|
||||
cardBackground: false
|
||||
room: root.room
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,8 +3,6 @@
|
||||
|
||||
import QtQuick
|
||||
|
||||
import org.kde.kirigami as Kirigami
|
||||
|
||||
import org.kde.neochat.libneochat
|
||||
import org.kde.neochat.timeline
|
||||
|
||||
@@ -47,15 +45,4 @@ SearchPage {
|
||||
noResultPlaceholderMessage: i18n("No messages found")
|
||||
|
||||
listVerticalLayoutDirection: ListView.BottomToTop
|
||||
|
||||
Connections {
|
||||
target: root.room.mainCache
|
||||
|
||||
function onRelationIdChanged(oldEventId: string, newEventId: string): void {
|
||||
// If we start replying/editing an event, we need to close the search dialog so the user can type.
|
||||
if (newEventId.length > 0) {
|
||||
root.Kirigami.PageStack.closeDialog();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,9 @@ import org.kde.neochat
|
||||
RowLayout {
|
||||
id: root
|
||||
|
||||
property var desiredWidth
|
||||
property bool collapsed: false
|
||||
required property NeoChatConnection connection
|
||||
|
||||
signal search
|
||||
|
||||
@@ -24,6 +26,10 @@ RowLayout {
|
||||
*/
|
||||
signal textChanged(string newText)
|
||||
|
||||
MediaDevices {
|
||||
id: devices
|
||||
}
|
||||
|
||||
Kirigami.Heading {
|
||||
Layout.fillWidth: true
|
||||
visible: !root.collapsed
|
||||
@@ -45,4 +51,78 @@ RowLayout {
|
||||
QQC2.ToolTip.text: text
|
||||
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||
}
|
||||
|
||||
QQC2.ToolButton {
|
||||
id: menuButton
|
||||
|
||||
property QQC2.Menu menuItem: undefined
|
||||
|
||||
function openMenu(): void {
|
||||
if (!menuItem || !menuItem.visible) {
|
||||
menuItem = menu.createObject(menuButton) as QQC2.Menu;
|
||||
menuItem.closed.connect(menuButton.toggle);
|
||||
menuItem.open();
|
||||
} else {
|
||||
menuItem.dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
Accessible.role: Accessible.ButtonMenu
|
||||
display: QQC2.AbstractButton.IconOnly
|
||||
down: pressed || menuItem.visible
|
||||
text: i18nc("@action:button", "Show Menu")
|
||||
icon.name: "application-menu-symbolic"
|
||||
|
||||
onPressed: openMenu()
|
||||
Keys.onReturnPressed: openMenu()
|
||||
Keys.onEnterPressed: openMenu()
|
||||
Accessible.onPressAction: openMenu()
|
||||
|
||||
QQC2.ToolTip.visible: hovered && !menuItem.visible
|
||||
QQC2.ToolTip.text: text
|
||||
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||
}
|
||||
|
||||
Component {
|
||||
id: menu
|
||||
QQC2.Menu {
|
||||
y: menuButton.height
|
||||
|
||||
QQC2.MenuItem {
|
||||
text: i18n("Find your friends")
|
||||
icon.name: "list-add-user"
|
||||
onTriggered: (root.Kirigami.PageStack.pageStack as Kirigami.PageRow).pushDialogLayer(Qt.createComponent('org.kde.neochat', 'UserSearchPage'), {
|
||||
connection: root.connection
|
||||
}, {
|
||||
title: i18nc("@title", "Find your friends")
|
||||
})
|
||||
}
|
||||
|
||||
QQC2.MenuItem {
|
||||
text: i18n("Create a Room")
|
||||
icon.name: "system-users-symbolic"
|
||||
onTriggered: {
|
||||
(Qt.createComponent('org.kde.neochat', 'CreateRoomDialog').createObject(root, {
|
||||
connection: root.connection
|
||||
}) as CreateRoomDialog).open();
|
||||
}
|
||||
|
||||
Kirigami.Action {
|
||||
shortcut: StandardKey.New
|
||||
onTriggered: parent.trigger()
|
||||
}
|
||||
}
|
||||
|
||||
QQC2.MenuItem {
|
||||
text: i18n("Scan a QR Code")
|
||||
icon.name: "view-barcode-qr"
|
||||
visible: devices.videoInputs.length > 0
|
||||
onTriggered: (root.Kirigami.PageStack.pageStack as Kirigami.PageRow).pushDialogLayer(Qt.createComponent("org.kde.neochat", "QrScannerPage"), {
|
||||
connection: root.connection
|
||||
}, {
|
||||
title: i18nc("@title", "Scan a QR Code")
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -157,11 +157,9 @@ KirigamiComponents.ConvergentContextMenu {
|
||||
onTriggered: {
|
||||
let dialog = (root.Kirigami.PageStack.pageStack as Kirigami.PageRow).pushDialogLayer(Qt.createComponent('org.kde.neochat', 'ReasonDialog'), {
|
||||
title: i18nc("@title:dialog", "Report Room"),
|
||||
placeholder: i18nc("@info:placeholder", "Optionally give a reason for reporting this room"),
|
||||
placeholder: i18nc("@info:placeholder", "Reason for reporting this room"),
|
||||
icon: "dialog-warning-symbolic",
|
||||
actionText: i18nc("@action:button 'Report' as in 'Report this room to the administrators'", "Report"),
|
||||
reporting: true,
|
||||
connection: root.connection,
|
||||
actionText: i18nc("@action:button 'Report' as in 'Report this room to the administrators'", "Report")
|
||||
}, {
|
||||
title: i18nc("@title", "Report Room"),
|
||||
width: Kirigami.Units.gridUnit * 25
|
||||
|
||||
@@ -270,7 +270,9 @@ Kirigami.Page {
|
||||
Component {
|
||||
id: exploreComponent
|
||||
ExploreComponent {
|
||||
desiredWidth: root.width - Kirigami.Units.largeSpacing
|
||||
collapsed: root.collapsed
|
||||
connection: root.connection
|
||||
|
||||
onSearch: root.search()
|
||||
|
||||
|
||||
@@ -46,8 +46,6 @@ QQC2.ItemDelegate {
|
||||
topPadding: 0
|
||||
bottomPadding: 0
|
||||
text: root.collapsed ? "" : root.displayName
|
||||
|
||||
onClicked: root.treeView.toggleExpanded(row)
|
||||
}
|
||||
QQC2.ToolButton {
|
||||
id: collapseButton
|
||||
|
||||
@@ -9,7 +9,6 @@ import QtQuick.Controls as QQC2
|
||||
import QtQuick.Layouts
|
||||
|
||||
import org.kde.kirigami as Kirigami
|
||||
import org.kde.kirigamiaddons.components as KirigamiComponents
|
||||
|
||||
import org.kde.neochat
|
||||
|
||||
@@ -159,7 +158,7 @@ QQC2.Control {
|
||||
width: Math.max(directChatNotificationCountTextMetrics.advanceWidth + Kirigami.Units.smallSpacing * 2, height)
|
||||
height: Kirigami.Units.iconSizes.smallMedium
|
||||
|
||||
text: visible ? directChatButton.countedNotifications : ""
|
||||
text: visible ? root.connection.directChatNotifications + root.connection.directChatInvites : ""
|
||||
visible: directChatButton.hasCountableNotifications && RoomManager.currentSpace !== "DM"
|
||||
color: Kirigami.Theme.textColor
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
@@ -264,46 +263,21 @@ QQC2.Control {
|
||||
}
|
||||
|
||||
AvatarTabButton {
|
||||
id: createNewButton
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: width - Kirigami.Units.smallSpacing
|
||||
Layout.maximumHeight: width - Kirigami.Units.smallSpacing
|
||||
|
||||
text: i18nc("@action:button Create a new room or space", "Create")
|
||||
text: i18n("Create a space")
|
||||
contentItem: Kirigami.Icon {
|
||||
source: "list-add"
|
||||
}
|
||||
|
||||
activeFocusOnTab: true
|
||||
down: menu.opened
|
||||
|
||||
onSelected: menu.popup(root.QQC2.Overlay.overlay, createNewButton.mapToGlobal(Qt.point(createNewButton.width, 0)))
|
||||
|
||||
KirigamiComponents.ConvergentContextMenu {
|
||||
id: menu
|
||||
|
||||
Kirigami.Action {
|
||||
text: i18nc("@action:button Create a new room", "New Room…")
|
||||
icon.name: "list-add-symbolic"
|
||||
|
||||
onTriggered: {
|
||||
(Qt.createComponent('org.kde.neochat', 'CreateRoomDialog').createObject(root, {
|
||||
connection: root.connection
|
||||
}) as CreateRoomDialog).open();
|
||||
}
|
||||
}
|
||||
|
||||
Kirigami.Action {
|
||||
text: i18nc("@action:button Create a new room", "New Space…")
|
||||
icon.name: "list-add-symbolic"
|
||||
|
||||
onTriggered: {
|
||||
(Qt.createComponent('org.kde.neochat', 'CreateSpaceDialog').createObject(root, {
|
||||
connection: root.connection
|
||||
}) as CreateSpaceDialog).open();
|
||||
}
|
||||
}
|
||||
onSelected: {
|
||||
(Qt.createComponent('org.kde.neochat', 'CreateSpaceDialog').createObject(root, {
|
||||
connection: root.connection
|
||||
}) as CreateSpaceDialog).open();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -51,7 +51,6 @@ ecm_add_qml_module(Settings GENERATE_PLUGIN_SOURCE
|
||||
RoomProfile.qml
|
||||
RoomAdvancedPage.qml
|
||||
KeyboardShortcutsPage.qml
|
||||
Members.qml
|
||||
SOURCES
|
||||
colorschemer.cpp
|
||||
threepidaddhelper.cpp
|
||||
|
||||
@@ -36,10 +36,9 @@ FormCard.FormCardPage {
|
||||
devicesModel: root.devicesModel
|
||||
|
||||
FormCard.FormButtonDelegate {
|
||||
icon.name: !root.connection.isVerifiedSession ? "security-low" : "security-high"
|
||||
icon.name: "security-low"
|
||||
text: i18nc("@action:button", "Verify This Device")
|
||||
description: !root.connection.isVerifiedSession ? i18nc("@info:description", "This device is marked as insecure until it's verified by another device. It's recommended to verify as soon as possible.")
|
||||
: i18nc("@info:description", "This device is marked as secure.")
|
||||
description: i18nc("@info:description", "This device is marked as insecure until it's verified by another device. It's recommended to verify as soon as possible.")
|
||||
visible: !root.connection.isVerifiedSession || NeoChatConfig.alwaysVerifyDevice
|
||||
onClicked: {
|
||||
root.connection.startSelfVerification();
|
||||
|
||||
@@ -1,247 +0,0 @@
|
||||
// SPDX-FileCopyrightText: 2022 James Graham <james.h.graham@protonmail.com>
|
||||
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls as QQC2
|
||||
import QtQuick.Layouts
|
||||
|
||||
import org.kde.kirigami as Kirigami
|
||||
import org.kde.kirigamiaddons.formcard as FormCard
|
||||
import org.kde.kirigamiaddons.delegates as Delegates
|
||||
import org.kde.kirigamiaddons.labs.components as KirigamiComponents
|
||||
import org.kde.kitemmodels
|
||||
|
||||
import org.kde.neochat
|
||||
|
||||
FormCard.FormCardPage {
|
||||
id: root
|
||||
|
||||
property NeoChatRoom room
|
||||
|
||||
title: i18nc("@title:window", "Members")
|
||||
|
||||
readonly property bool loading: permissions.count === 0 && !root.room.roomCreatorHasUltimatePowerLevel()
|
||||
|
||||
readonly property PowerLevelModel powerLevelModel: PowerLevelModel {
|
||||
showMute: false
|
||||
}
|
||||
|
||||
FormCard.FormHeader {
|
||||
title: i18nc("@title", "Privileged Members")
|
||||
visible: !root.loading
|
||||
}
|
||||
FormCard.FormCard {
|
||||
visible: !root.loading
|
||||
|
||||
FormCard.AbstractFormDelegate {
|
||||
id: userListSearchCard
|
||||
visible: root.room.canSendState("m.room.power_levels")
|
||||
|
||||
contentItem: Kirigami.SearchField {
|
||||
id: userListSearchField
|
||||
|
||||
autoAccept: false
|
||||
|
||||
Layout.fillWidth: true
|
||||
|
||||
Keys.onUpPressed: userListView.decrementCurrentIndex()
|
||||
Keys.onDownPressed: userListView.incrementCurrentIndex()
|
||||
|
||||
onAccepted: (userListView.itemAtIndex(userListView.currentIndex) as Delegates.RoundedItemDelegate).action.trigger()
|
||||
}
|
||||
QQC2.Popup {
|
||||
id: userListSearchPopup
|
||||
|
||||
x: userListSearchField.x
|
||||
y: userListSearchField.y - height
|
||||
width: userListSearchField.width
|
||||
height: {
|
||||
let maxHeight = userListSearchField.mapToGlobal(userListSearchField.x, userListSearchField.y).y - Kirigami.Units.largeSpacing * 3;
|
||||
let minHeight = Kirigami.Units.gridUnit * 2 + userListSearchPopup.padding * 2;
|
||||
let filterContentHeight = userListView.contentHeight + userListSearchPopup.padding * 2;
|
||||
return Math.max(Math.min(filterContentHeight, maxHeight), minHeight);
|
||||
}
|
||||
padding: Kirigami.Units.smallSpacing
|
||||
leftPadding: Kirigami.Units.smallSpacing / 2
|
||||
rightPadding: Kirigami.Units.smallSpacing / 2
|
||||
modal: false
|
||||
onClosed: userListSearchField.text = ""
|
||||
|
||||
background: Kirigami.ShadowedRectangle {
|
||||
property color borderColor: Kirigami.Theme.textColor
|
||||
|
||||
Kirigami.Theme.colorSet: Kirigami.Theme.View
|
||||
Kirigami.Theme.inherit: false
|
||||
|
||||
radius: Kirigami.Units.cornerRadius
|
||||
color: Kirigami.Theme.backgroundColor
|
||||
|
||||
border {
|
||||
color: Qt.rgba(borderColor.r, borderColor.g, borderColor.b, 0.3)
|
||||
width: 1
|
||||
}
|
||||
|
||||
shadow {
|
||||
xOffset: 0
|
||||
yOffset: 4
|
||||
color: Qt.rgba(0, 0, 0, 0.3)
|
||||
size: 8
|
||||
}
|
||||
}
|
||||
|
||||
contentItem: QQC2.ScrollView {
|
||||
// HACK: Hide unnecessary horizontal scrollbar (https://bugreports.qt.io/browse/QTBUG-83890)
|
||||
QQC2.ScrollBar.horizontal.policy: QQC2.ScrollBar.AlwaysOff
|
||||
|
||||
ListView {
|
||||
id: userListView
|
||||
clip: true
|
||||
|
||||
model: UserFilterModel {
|
||||
id: userListFilterModel
|
||||
sourceModel: RoomManager.userListModel
|
||||
filterText: userListSearchField.text
|
||||
|
||||
onFilterTextChanged: {
|
||||
if (filterText.length > 0 && !userListSearchPopup.visible) {
|
||||
userListSearchPopup.open();
|
||||
} else if (filterText.length <= 0 && userListSearchPopup.visible) {
|
||||
userListSearchPopup.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
delegate: Delegates.RoundedItemDelegate {
|
||||
id: userListItem
|
||||
|
||||
required property string userId
|
||||
required property url avatar
|
||||
required property string name
|
||||
required property int powerLevel
|
||||
required property string powerLevelString
|
||||
|
||||
text: name
|
||||
|
||||
contentItem: RowLayout {
|
||||
KirigamiComponents.Avatar {
|
||||
Layout.preferredWidth: Kirigami.Units.iconSizes.medium
|
||||
Layout.preferredHeight: Kirigami.Units.iconSizes.medium
|
||||
source: userListItem.avatar
|
||||
name: userListItem.name
|
||||
}
|
||||
|
||||
Delegates.SubtitleContentItem {
|
||||
itemDelegate: userListItem
|
||||
subtitle: userListItem.userId
|
||||
labelItem.textFormat: Text.PlainText
|
||||
subtitleItem.textFormat: Text.PlainText
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
QQC2.Label {
|
||||
visible: userListItem.powerLevel > 0
|
||||
|
||||
text: userListItem.powerLevelString
|
||||
color: Kirigami.Theme.disabledTextColor
|
||||
textFormat: Text.PlainText
|
||||
wrapMode: Text.NoWrap
|
||||
}
|
||||
}
|
||||
|
||||
onClicked: {
|
||||
userListSearchPopup.close();
|
||||
(powerLevelDialog.createObject(root.QQC2.Overlay.overlay, {
|
||||
room: root.room,
|
||||
userId: userListItem.userId,
|
||||
powerLevel: userListItem.powerLevel
|
||||
}) as PowerLevelDialog).open();
|
||||
}
|
||||
|
||||
Component {
|
||||
id: powerLevelDialog
|
||||
PowerLevelDialog {}
|
||||
}
|
||||
}
|
||||
|
||||
QQC2.Label {
|
||||
text: i18nc("@info", "No users found.")
|
||||
visible: userListView.count === 0
|
||||
|
||||
anchors {
|
||||
left: parent.left
|
||||
leftMargin: Kirigami.Units.mediumSpacing
|
||||
verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
FormCard.FormDelegateSeparator {
|
||||
above: userListSearchCard
|
||||
}
|
||||
Repeater {
|
||||
id: permissions
|
||||
model: KSortFilterProxyModel {
|
||||
sourceModel: RoomManager.userListModel
|
||||
sortRoleName: "powerLevel"
|
||||
sortOrder: Qt.DescendingOrder
|
||||
filterRowCallback: function (source_row, source_parent) {
|
||||
let powerLevelRole = sourceModel.data(sourceModel.index(source_row, 0, source_parent), UserListModel.PowerLevelRole);
|
||||
return powerLevelRole != 0;
|
||||
}
|
||||
}
|
||||
delegate: FormCard.FormTextDelegate {
|
||||
id: privilegedUserDelegate
|
||||
required property string userId
|
||||
required property string name
|
||||
required property int powerLevel
|
||||
required property string powerLevelString
|
||||
required property bool isCreator
|
||||
|
||||
text: name
|
||||
textItem.textFormat: Text.PlainText
|
||||
description: userId
|
||||
contentItem.children: RowLayout {
|
||||
spacing: Kirigami.Units.largeSpacing
|
||||
QQC2.Label {
|
||||
id: powerLevelLabel
|
||||
text: privilegedUserDelegate.powerLevelString
|
||||
visible: (!root.room.canSendState("m.room.power_levels") || (root.room.memberEffectivePowerLevel(root.room.localMember.id) <= privilegedUserDelegate.powerLevel && privilegedUserDelegate.userId != root.room.localMember.id)) || privilegedUserDelegate.isCreator
|
||||
color: Kirigami.Theme.disabledTextColor
|
||||
}
|
||||
QQC2.ComboBox {
|
||||
focusPolicy: Qt.NoFocus // provided by parent
|
||||
model: PowerLevelModel {}
|
||||
textRole: "name"
|
||||
valueRole: "value"
|
||||
visible: !powerLevelLabel.visible
|
||||
Component.onCompleted: {
|
||||
let index = indexOfValue(privilegedUserDelegate.powerLevel)
|
||||
if (index === -1) {
|
||||
displayText = privilegedUserDelegate.powerLevelString;
|
||||
} else {
|
||||
currentIndex = index;
|
||||
}
|
||||
}
|
||||
onActivated: {
|
||||
root.room.setUserPowerLevel(privilegedUserDelegate.userId, currentValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
visible: root.loading
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: root.height * 0.9
|
||||
Kirigami.LoadingPlaceholder {
|
||||
anchors.centerIn: parent
|
||||
text: i18nc("@placeholder", "Loading…")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -85,19 +85,6 @@ FormCard.FormCardPage {
|
||||
title: i18nc("@title:group", "Encryption")
|
||||
}
|
||||
FormCard.FormCard {
|
||||
FormCard.FormButtonDelegate {
|
||||
id: secretBackupDelegate
|
||||
text: i18nc("@action:inmenu", "Manage Secret Backup")
|
||||
description: i18nc("@info", "Import or unlock encryption keys from other devices.")
|
||||
icon.name: "unlock"
|
||||
onClicked: root.QQC2.ApplicationWindow.window.pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat', 'UnlockSSSSDialog'), {}, {
|
||||
title: i18nc("@title:window", "Manage Secret Backup")
|
||||
})
|
||||
}
|
||||
FormCard.FormDelegateSeparator {
|
||||
above: secretBackupDelegate
|
||||
below: importKeysDelegate
|
||||
}
|
||||
FormCard.FormButtonDelegate {
|
||||
id: importKeysDelegate
|
||||
text: i18nc("@action:button", "Import Keys")
|
||||
|
||||
@@ -33,10 +33,207 @@ FormCard.FormCardPage {
|
||||
}
|
||||
|
||||
FormCard.FormHeader {
|
||||
title: i18nc("@title", "Power Levels")
|
||||
title: i18nc("@title", "Privileged Users")
|
||||
visible: !root.loading
|
||||
}
|
||||
FormCard.FormCard {
|
||||
enabled: root.room.canSendState("m.room.power_levels")
|
||||
visible: !root.loading
|
||||
|
||||
Repeater {
|
||||
id: permissions
|
||||
model: KSortFilterProxyModel {
|
||||
sourceModel: RoomManager.userListModel
|
||||
sortRoleName: "powerLevel"
|
||||
sortOrder: Qt.DescendingOrder
|
||||
filterRowCallback: function (source_row, source_parent) {
|
||||
let powerLevelRole = sourceModel.data(sourceModel.index(source_row, 0, source_parent), UserListModel.PowerLevelRole);
|
||||
return powerLevelRole != 0;
|
||||
}
|
||||
}
|
||||
delegate: FormCard.FormTextDelegate {
|
||||
id: privilegedUserDelegate
|
||||
required property string userId
|
||||
required property string name
|
||||
required property int powerLevel
|
||||
required property string powerLevelString
|
||||
required property bool isCreator
|
||||
|
||||
text: name
|
||||
textItem.textFormat: Text.PlainText
|
||||
description: userId
|
||||
contentItem.children: RowLayout {
|
||||
spacing: Kirigami.Units.largeSpacing
|
||||
QQC2.Label {
|
||||
id: powerLevelLabel
|
||||
text: privilegedUserDelegate.powerLevelString
|
||||
visible: (!root.room.canSendState("m.room.power_levels") || (root.room.memberEffectivePowerLevel(root.room.localMember.id) <= privilegedUserDelegate.powerLevel && privilegedUserDelegate.userId != root.room.localMember.id)) || privilegedUserDelegate.isCreator
|
||||
color: Kirigami.Theme.disabledTextColor
|
||||
}
|
||||
QQC2.ComboBox {
|
||||
focusPolicy: Qt.NoFocus // provided by parent
|
||||
model: PowerLevelModel {}
|
||||
textRole: "name"
|
||||
valueRole: "value"
|
||||
visible: !powerLevelLabel.visible
|
||||
Component.onCompleted: {
|
||||
let index = indexOfValue(privilegedUserDelegate.powerLevel)
|
||||
if (index === -1) {
|
||||
displayText = privilegedUserDelegate.powerLevelString;
|
||||
} else {
|
||||
currentIndex = index;
|
||||
}
|
||||
}
|
||||
onActivated: {
|
||||
root.room.setUserPowerLevel(privilegedUserDelegate.userId, currentValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
FormCard.FormDelegateSeparator {
|
||||
below: userListSearchCard
|
||||
}
|
||||
FormCard.AbstractFormDelegate {
|
||||
id: userListSearchCard
|
||||
visible: root.room.canSendState("m.room.power_levels")
|
||||
|
||||
contentItem: Kirigami.SearchField {
|
||||
id: userListSearchField
|
||||
|
||||
autoAccept: false
|
||||
|
||||
Layout.fillWidth: true
|
||||
|
||||
Keys.onUpPressed: userListView.decrementCurrentIndex()
|
||||
Keys.onDownPressed: userListView.incrementCurrentIndex()
|
||||
|
||||
onAccepted: (userListView.itemAtIndex(userListView.currentIndex) as Delegates.RoundedItemDelegate).action.trigger()
|
||||
}
|
||||
QQC2.Popup {
|
||||
id: userListSearchPopup
|
||||
|
||||
x: userListSearchField.x
|
||||
y: userListSearchField.y - height
|
||||
width: userListSearchField.width
|
||||
height: {
|
||||
let maxHeight = userListSearchField.mapToGlobal(userListSearchField.x, userListSearchField.y).y - Kirigami.Units.largeSpacing * 3;
|
||||
let minHeight = Kirigami.Units.gridUnit * 2 + userListSearchPopup.padding * 2;
|
||||
let filterContentHeight = userListView.contentHeight + userListSearchPopup.padding * 2;
|
||||
return Math.max(Math.min(filterContentHeight, maxHeight), minHeight);
|
||||
}
|
||||
padding: Kirigami.Units.smallSpacing
|
||||
leftPadding: Kirigami.Units.smallSpacing / 2
|
||||
rightPadding: Kirigami.Units.smallSpacing / 2
|
||||
modal: false
|
||||
onClosed: userListSearchField.text = ""
|
||||
|
||||
background: Kirigami.ShadowedRectangle {
|
||||
property color borderColor: Kirigami.Theme.textColor
|
||||
|
||||
Kirigami.Theme.colorSet: Kirigami.Theme.View
|
||||
Kirigami.Theme.inherit: false
|
||||
|
||||
radius: Kirigami.Units.cornerRadius
|
||||
color: Kirigami.Theme.backgroundColor
|
||||
|
||||
border {
|
||||
color: Qt.rgba(borderColor.r, borderColor.g, borderColor.b, 0.3)
|
||||
width: 1
|
||||
}
|
||||
|
||||
shadow {
|
||||
xOffset: 0
|
||||
yOffset: 4
|
||||
color: Qt.rgba(0, 0, 0, 0.3)
|
||||
size: 8
|
||||
}
|
||||
}
|
||||
|
||||
contentItem: QQC2.ScrollView {
|
||||
// HACK: Hide unnecessary horizontal scrollbar (https://bugreports.qt.io/browse/QTBUG-83890)
|
||||
QQC2.ScrollBar.horizontal.policy: QQC2.ScrollBar.AlwaysOff
|
||||
|
||||
ListView {
|
||||
id: userListView
|
||||
clip: true
|
||||
|
||||
model: UserFilterModel {
|
||||
id: userListFilterModel
|
||||
sourceModel: RoomManager.userListModel
|
||||
filterText: userListSearchField.text
|
||||
|
||||
onFilterTextChanged: {
|
||||
if (filterText.length > 0 && !userListSearchPopup.visible) {
|
||||
userListSearchPopup.open();
|
||||
} else if (filterText.length <= 0 && userListSearchPopup.visible) {
|
||||
userListSearchPopup.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
delegate: Delegates.RoundedItemDelegate {
|
||||
id: userListItem
|
||||
|
||||
required property string userId
|
||||
required property url avatar
|
||||
required property string name
|
||||
required property int powerLevel
|
||||
required property string powerLevelString
|
||||
|
||||
text: name
|
||||
|
||||
contentItem: RowLayout {
|
||||
KirigamiComponents.Avatar {
|
||||
Layout.preferredWidth: Kirigami.Units.iconSizes.medium
|
||||
Layout.preferredHeight: Kirigami.Units.iconSizes.medium
|
||||
source: userListItem.avatar
|
||||
name: userListItem.name
|
||||
}
|
||||
|
||||
Delegates.SubtitleContentItem {
|
||||
itemDelegate: userListItem
|
||||
subtitle: userListItem.userId
|
||||
labelItem.textFormat: Text.PlainText
|
||||
subtitleItem.textFormat: Text.PlainText
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
QQC2.Label {
|
||||
visible: userListItem.powerLevel > 0
|
||||
|
||||
text: userListItem.powerLevelString
|
||||
color: Kirigami.Theme.disabledTextColor
|
||||
textFormat: Text.PlainText
|
||||
wrapMode: Text.NoWrap
|
||||
}
|
||||
}
|
||||
|
||||
onClicked: {
|
||||
userListSearchPopup.close();
|
||||
(powerLevelDialog.createObject(root.QQC2.Overlay.overlay, {
|
||||
room: root.room,
|
||||
userId: userListItem.userId,
|
||||
powerLevel: userListItem.powerLevel
|
||||
}) as PowerLevelDialog).open();
|
||||
}
|
||||
|
||||
Component {
|
||||
id: powerLevelDialog
|
||||
PowerLevelDialog {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
FormCard.FormHeader {
|
||||
visible: root.room.canSendState("m.room.power_levels")
|
||||
title: i18nc("@title", "Default permissions")
|
||||
}
|
||||
FormCard.FormCard {
|
||||
visible: root.room.canSendState("m.room.power_levels")
|
||||
Repeater {
|
||||
model: KSortFilterProxyModel {
|
||||
sourceModel: root.permissionsModel
|
||||
@@ -72,49 +269,11 @@ FormCard.FormCardPage {
|
||||
}
|
||||
|
||||
FormCard.FormHeader {
|
||||
title: i18nc("@title", "Messages")
|
||||
visible: root.room.canSendState("m.room.power_levels")
|
||||
title: i18nc("@title", "Basic permissions")
|
||||
}
|
||||
FormCard.FormCard {
|
||||
enabled: root.room.canSendState("m.room.power_levels")
|
||||
Repeater {
|
||||
model: KSortFilterProxyModel {
|
||||
sourceModel: root.permissionsModel
|
||||
filterRowCallback: function (source_row, source_parent) {
|
||||
return sourceModel.data(sourceModel.index(source_row, 0, source_parent), PermissionsModel.IsMessagePermissionRole);
|
||||
}
|
||||
}
|
||||
delegate: FormCard.FormComboBoxDelegate {
|
||||
required property string name
|
||||
required property string subtitle
|
||||
required property string type
|
||||
required property int level
|
||||
required property string levelName
|
||||
|
||||
text: name
|
||||
description: subtitle
|
||||
textRole: "name"
|
||||
valueRole: "value"
|
||||
model: root.powerLevelModel
|
||||
Component.onCompleted: {
|
||||
let index = indexOfValue(level)
|
||||
if (index === -1) {
|
||||
displayText = levelName;
|
||||
} else {
|
||||
currentIndex = index;
|
||||
}
|
||||
}
|
||||
onCurrentValueChanged: if (root.room.canSendState("m.room.power_levels")) {
|
||||
root.permissionsModel.setPowerLevel(type, currentValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
FormCard.FormHeader {
|
||||
title: i18nc("@title", "Moderation")
|
||||
}
|
||||
FormCard.FormCard {
|
||||
enabled: root.room.canSendState("m.room.power_levels")
|
||||
visible: root.room.canSendState("m.room.power_levels")
|
||||
Repeater {
|
||||
model: KSortFilterProxyModel {
|
||||
sourceModel: root.permissionsModel
|
||||
@@ -150,61 +309,18 @@ FormCard.FormCardPage {
|
||||
}
|
||||
|
||||
FormCard.FormHeader {
|
||||
title: i18nc("@title", "General")
|
||||
visible: root.room.canSendState("m.room.power_levels")
|
||||
title: i18nc("@title", "Event permissions")
|
||||
}
|
||||
FormCard.FormCard {
|
||||
enabled: root.room.canSendState("m.room.power_levels")
|
||||
visible: root.room.canSendState("m.room.power_levels")
|
||||
Repeater {
|
||||
model: KSortFilterProxyModel {
|
||||
sourceModel: root.permissionsModel
|
||||
filterRowCallback: function (source_row, source_parent) {
|
||||
return sourceModel.data(sourceModel.index(source_row, 0, source_parent), PermissionsModel.IsGeneralPermissionRole);
|
||||
}
|
||||
}
|
||||
delegate: FormCard.FormComboBoxDelegate {
|
||||
required property string name
|
||||
required property string subtitle
|
||||
required property string type
|
||||
required property int level
|
||||
required property string levelName
|
||||
|
||||
text: name
|
||||
description: subtitle
|
||||
textRole: "name"
|
||||
valueRole: "value"
|
||||
model: root.powerLevelModel
|
||||
Component.onCompleted: {
|
||||
let index = indexOfValue(level)
|
||||
if (index === -1) {
|
||||
displayText = levelName;
|
||||
} else {
|
||||
currentIndex = index;
|
||||
}
|
||||
}
|
||||
onCurrentValueChanged: if (root.room.canSendState("m.room.power_levels")) {
|
||||
root.permissionsModel.setPowerLevel(type, currentValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
FormCard.FormHeader {
|
||||
title: i18nc("@title", "Other Events")
|
||||
}
|
||||
FormCard.FormCard {
|
||||
enabled: root.room.canSendState("m.room.power_levels")
|
||||
|
||||
Repeater {
|
||||
id: otherEventsRepeater
|
||||
|
||||
model: KSortFilterProxyModel {
|
||||
sourceModel: root.permissionsModel
|
||||
filterRowCallback: function (source_row, source_parent) {
|
||||
let isBasicPermissionRole = sourceModel.data(sourceModel.index(source_row, 0, source_parent), PermissionsModel.IsBasicPermissionRole);
|
||||
let isDefaultValueRole = sourceModel.data(sourceModel.index(source_row, 0, source_parent), PermissionsModel.IsDefaultValueRole);
|
||||
let isMessagePermissionRole = sourceModel.data(sourceModel.index(source_row, 0, source_parent), PermissionsModel.IsMessagePermissionRole);
|
||||
let isGeneralPermissionRole = sourceModel.data(sourceModel.index(source_row, 0, source_parent), PermissionsModel.IsGeneralPermissionRole);
|
||||
return !isBasicPermissionRole && !isDefaultValueRole && !isMessagePermissionRole && !isGeneralPermissionRole;
|
||||
return !isBasicPermissionRole && !isDefaultValueRole;
|
||||
}
|
||||
}
|
||||
delegate: FormCard.FormComboBoxDelegate {
|
||||
@@ -232,13 +348,7 @@ FormCard.FormCardPage {
|
||||
}
|
||||
}
|
||||
}
|
||||
FormCard.FormDelegateSeparator {
|
||||
below: addNewEventDelegate
|
||||
visible: otherEventsRepeater.count > 0
|
||||
}
|
||||
FormCard.AbstractFormDelegate {
|
||||
id: addNewEventDelegate
|
||||
|
||||
Layout.fillWidth: true
|
||||
|
||||
contentItem: RowLayout {
|
||||
|
||||
@@ -28,6 +28,7 @@ FormCard.FormCardPage {
|
||||
description: root.room.id
|
||||
|
||||
contentItem.children: QQC2.Button {
|
||||
visible: roomIdDelegate.hovered
|
||||
text: i18nc("@action:button", "Copy room ID to clipboard")
|
||||
icon.name: "edit-copy"
|
||||
display: QQC2.AbstractButton.IconOnly
|
||||
@@ -41,19 +42,14 @@ FormCard.FormCardPage {
|
||||
QQC2.ToolTip.visible: hovered
|
||||
}
|
||||
}
|
||||
FormCard.FormDelegateSeparator {
|
||||
above: roomIdDelegate
|
||||
below: roomVersionDelegate
|
||||
}
|
||||
FormCard.FormTextDelegate {
|
||||
id: roomVersionDelegate
|
||||
text: i18nc("@info:label", "Room Version")
|
||||
description: root.room.version
|
||||
|
||||
contentItem.children: QQC2.Button {
|
||||
visible: root.room.canSwitchVersions()
|
||||
enabled: root.room.version < root.room.maxRoomVersion
|
||||
text: i18nc("@action:button", "Upgrade Room…")
|
||||
text: i18nc("@action:button", "Upgrade Room")
|
||||
icon.name: "arrow-up-double"
|
||||
|
||||
onClicked: {
|
||||
|
||||
@@ -55,17 +55,6 @@ KirigamiSettings.ConfigurationView {
|
||||
};
|
||||
}
|
||||
},
|
||||
KirigamiSettings.ConfigurationModule {
|
||||
moduleId: "members"
|
||||
text: i18nc("@title", "Members")
|
||||
icon.name: "system-users-symbolic"
|
||||
page: () => Qt.createComponent("org.kde.neochat.settings", "Members")
|
||||
initialProperties: () => {
|
||||
return {
|
||||
room: root._room
|
||||
};
|
||||
}
|
||||
},
|
||||
KirigamiSettings.ConfigurationModule {
|
||||
moduleId: "permissions"
|
||||
text: i18nc("@title", "Permissions")
|
||||
|
||||
@@ -50,16 +50,12 @@ static const QStringList knownPermissions = {
|
||||
u"m.room.server_acl"_s,
|
||||
u"m.space.child"_s,
|
||||
u"m.space.parent"_s,
|
||||
u"org.matrix.msc3672.beacon_info"_s,
|
||||
u"org.matrix.msc3381.poll.start"_s,
|
||||
u"org.matrix.msc3381.poll.response"_s,
|
||||
u"org.matrix.msc3381.poll.end"_s,
|
||||
};
|
||||
|
||||
// Alternate name text for default permissions.
|
||||
static const QHash<QString, KLazyLocalizedString> permissionNames = {
|
||||
{UsersDefaultKey, kli18nc("Room permission type", "Default power level")},
|
||||
{StateDefaultKey, kli18nc("Room permission type", "Default power level to change room state")},
|
||||
{UsersDefaultKey, kli18nc("Room permission type", "Default user power level")},
|
||||
{StateDefaultKey, kli18nc("Room permission type", "Default power level to set the room state")},
|
||||
{EventsDefaultKey, kli18nc("Room permission type", "Default power level to send messages")},
|
||||
{InviteKey, kli18nc("Room permission type", "Invite users")},
|
||||
{KickKey, kli18nc("Room permission type", "Kick users")},
|
||||
@@ -74,58 +70,25 @@ static const QHash<QString, KLazyLocalizedString> permissionNames = {
|
||||
{u"m.room.topic"_s, kli18nc("Room permission type", "Change the room topic")},
|
||||
{u"m.room.encryption"_s, kli18nc("Room permission type", "Enable encryption for the room")},
|
||||
{u"m.room.history_visibility"_s, kli18nc("Room permission type", "Change the room history visibility")},
|
||||
{u"m.room.pinned_events"_s, kli18nc("Room permission type", "Pin and unpin messages")},
|
||||
{u"m.room.pinned_events"_s, kli18nc("Room permission type", "Set pinned events")},
|
||||
{u"m.room.tombstone"_s, kli18nc("Room permission type", "Upgrade the room")},
|
||||
{u"m.room.server_acl"_s, kli18nc("Room permission type", "Set the room server access control list (ACL)")},
|
||||
{u"m.space.child"_s, kli18nc("Room permission type", "Set the children of this space")},
|
||||
{u"m.space.parent"_s, kli18nc("Room permission type", "Set the parent space of this room")},
|
||||
{u"org.matrix.msc3672.beacon_info"_s, kli18nc("Room permission type", "Send live location updates")},
|
||||
{u"org.matrix.msc3381.poll.start"_s, kli18nc("Room permission type", "Start polls")},
|
||||
{u"org.matrix.msc3381.poll.response"_s, kli18nc("Room permission type", "Vote in polls")},
|
||||
{u"org.matrix.msc3381.poll.end"_s, kli18nc("Room permission type", "Close polls")},
|
||||
};
|
||||
|
||||
// Subtitles for the default values.
|
||||
static const QHash<QString, KLazyLocalizedString> permissionSubtitles = {
|
||||
{UsersDefaultKey, kli18nc("Room permission type", "This is the power level for all new users when joining the room.")},
|
||||
{StateDefaultKey, kli18nc("Room permission type", "This is used for all state-type events that do not have their own entry.")},
|
||||
{EventsDefaultKey, kli18nc("Room permission type", "This is used for all message-type events that do not have their own entry.")},
|
||||
{UsersDefaultKey, kli18nc("Room permission type", "This is the power level for all new users when joining the room")},
|
||||
{StateDefaultKey, kli18nc("Room permission type", "This is used for all state events that do not have their own entry here")},
|
||||
{EventsDefaultKey, kli18nc("Room permission type", "This is used for all message events that do not have their own entry here")},
|
||||
};
|
||||
|
||||
// Permissions that should use the message event default.
|
||||
// Permissions that should use the event default.
|
||||
static const QStringList eventPermissions = {
|
||||
u"m.room.message"_s,
|
||||
u"m.reaction"_s,
|
||||
u"m.room.redaction"_s,
|
||||
u"org.matrix.msc3381.poll.start"_s,
|
||||
u"org.matrix.msc3381.poll.response"_s,
|
||||
u"org.matrix.msc3381.poll.end"_s,
|
||||
};
|
||||
|
||||
// Permissions related to messaging.
|
||||
static const QStringList messagingPermissions = {
|
||||
u"m.reaction"_s,
|
||||
u"m.room.redaction"_s,
|
||||
u"org.matrix.msc3672.beacon_info"_s,
|
||||
u"org.matrix.msc3381.poll.start"_s,
|
||||
u"org.matrix.msc3381.poll.response"_s,
|
||||
u"org.matrix.msc3381.poll.end"_s,
|
||||
};
|
||||
|
||||
// Permissions related to general room management.
|
||||
static const QStringList generalPermissions = {
|
||||
u"m.room.power_levels"_s,
|
||||
u"m.room.name"_s,
|
||||
u"m.room.avatar"_s,
|
||||
u"m.room.canonical_alias"_s,
|
||||
u"m.room.topic"_s,
|
||||
u"m.room.encryption"_s,
|
||||
u"m.room.history_visibility"_s,
|
||||
u"m.room.pinned_events"_s,
|
||||
u"m.room.tombstone"_s,
|
||||
u"m.room.server_acl"_s,
|
||||
u"m.space.child"_s,
|
||||
u"m.space.parent"_s,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -231,12 +194,6 @@ QVariant PermissionsModel::data(const QModelIndex &index, int role) const
|
||||
if (role == IsBasicPermissionRole) {
|
||||
return basicPermissions.contains(permission);
|
||||
}
|
||||
if (role == IsMessagePermissionRole) {
|
||||
return messagingPermissions.contains(permission);
|
||||
}
|
||||
if (role == IsGeneralPermissionRole) {
|
||||
return generalPermissions.contains(permission);
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
@@ -256,8 +213,6 @@ QHash<int, QByteArray> PermissionsModel::roleNames() const
|
||||
roles[LevelNameRole] = "levelName";
|
||||
roles[IsDefaultValueRole] = "isDefaultValue";
|
||||
roles[IsBasicPermissionRole] = "isBasicPermission";
|
||||
roles[IsMessagePermissionRole] = "isMessagePermission";
|
||||
roles[IsGeneralPermissionRole] = "isGeneralPermission";
|
||||
return roles;
|
||||
}
|
||||
|
||||
|
||||
@@ -36,8 +36,6 @@ public:
|
||||
LevelNameRole, /**< The current power level for the permission as a string. */
|
||||
IsDefaultValueRole, /**< Whether the permission is a default value, e.g. for users. */
|
||||
IsBasicPermissionRole, /**< Whether the permission is one of the basic ones, e.g. kick, ban, etc. */
|
||||
IsMessagePermissionRole, /** Permissions related to messaging. */
|
||||
IsGeneralPermissionRole, /** Permissions related to general room management. */
|
||||
};
|
||||
Q_ENUM(Roles)
|
||||
|
||||
|
||||
@@ -8,24 +8,16 @@ import QtQuick.Layouts
|
||||
import org.kde.kirigami as Kirigami
|
||||
import org.kde.kirigamiaddons.labs.components as KirigamiComponents
|
||||
|
||||
import org.kde.neochat
|
||||
|
||||
RowLayout {
|
||||
id: root
|
||||
|
||||
property var avatarSize: Kirigami.Units.iconSizes.small
|
||||
property alias model: root.limiterModel.sourceModel
|
||||
property alias model: avatarFlowRepeater.model
|
||||
property string toolTipText
|
||||
|
||||
property LimiterModel limiterModel: LimiterModel {
|
||||
maximumCount: 5
|
||||
}
|
||||
|
||||
spacing: -avatarSize / 2
|
||||
Repeater {
|
||||
id: avatarFlowRepeater
|
||||
model: root.limiterModel
|
||||
|
||||
delegate: KirigamiComponents.Avatar {
|
||||
required property string displayName
|
||||
required property url avatarUrl
|
||||
@@ -47,11 +39,11 @@ RowLayout {
|
||||
Layout.preferredHeight: Kirigami.Units.iconSizes.small + Kirigami.Units.smallSpacing
|
||||
Layout.fillHeight: true
|
||||
|
||||
visible: root.limiterModel.extraCount > 0
|
||||
visible: text !== ""
|
||||
color: Kirigami.Theme.textColor
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
|
||||
text: "+ " + root.limiterModel.extraCount
|
||||
text: root.model?.excessReadMarkersString ?? ""
|
||||
|
||||
background: Kirigami.ShadowedRectangle {
|
||||
color: Kirigami.Theme.backgroundColor
|
||||
|
||||
@@ -132,12 +132,14 @@ KirigamiComponents.ConvergentContextMenu {
|
||||
root.room.toggleReaction(root.eventId, emoji);
|
||||
root.close();
|
||||
});
|
||||
dialog.closed.connect(() => {
|
||||
root.close();
|
||||
});
|
||||
dialog.open();
|
||||
return;
|
||||
}
|
||||
|
||||
root.room.toggleReaction(root.eventId, modelData);
|
||||
root.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -159,7 +161,7 @@ KirigamiComponents.ConvergentContextMenu {
|
||||
|
||||
Kirigami.Action {
|
||||
id: replyAction
|
||||
visible: !root.room.readOnly && (root.messageComponentType !== MessageComponentType.Other || NeoChatConfig.relateAnyEvent)
|
||||
visible: root.messageComponentType !== MessageComponentType.Other || NeoChatConfig.relateAnyEvent
|
||||
text: i18nc("@action:inmenu", "Reply")
|
||||
icon.name: "mail-replied-symbolic"
|
||||
onTriggered: {
|
||||
@@ -171,7 +173,7 @@ KirigamiComponents.ConvergentContextMenu {
|
||||
|
||||
Kirigami.Action {
|
||||
id: replyThreadAction
|
||||
visible: (root.messageComponentType !== MessageComponentType.Other || NeoChatConfig.relateAnyEvent) && NeoChatConfig.threads
|
||||
visible: root.messageComponentType !== MessageComponentType.Other || NeoChatConfig.relateAnyEvent
|
||||
text: i18nc("@action:inmenu", "Reply in Thread")
|
||||
icon.name: "dialog-messages"
|
||||
onTriggered: {
|
||||
@@ -202,13 +204,11 @@ KirigamiComponents.ConvergentContextMenu {
|
||||
icon.name: "edit-delete-remove"
|
||||
icon.color: "red"
|
||||
onTriggered: {
|
||||
let dialog = ((root.QQC2.ApplicationWindow.window as Kirigami.ApplicationWindow).pageStack as Kirigami.PageRow).pushDialogLayer(Qt.createComponent('org.kde.neochat', 'ReasonDialog'), {
|
||||
let dialog = (root.Kirigami.PageStack.pageStack as Kirigami.PageRow).pushDialogLayer(Qt.createComponent('org.kde.neochat', 'ReasonDialog'), {
|
||||
title: i18nc("@title:dialog", "Remove Message"),
|
||||
placeholder: i18nc("@info:placeholder", "Optionally give a reason for removing this message"),
|
||||
placeholder: i18nc("@info:placeholder", "Reason for removing this message"),
|
||||
actionText: i18nc("@action:button 'Remove' as in 'Remove this message'", "Remove"),
|
||||
icon: "delete",
|
||||
reporting: false,
|
||||
connection: root.connection,
|
||||
icon: "delete"
|
||||
}, {
|
||||
title: i18nc("@title:dialog", "Remove Message"),
|
||||
width: Kirigami.Units.gridUnit * 25
|
||||
@@ -229,7 +229,7 @@ KirigamiComponents.ConvergentContextMenu {
|
||||
text: i18nc("@action:inmenu As in 'Forward this message'", "Forward…")
|
||||
icon.name: "mail-forward-symbolic"
|
||||
onTriggered: {
|
||||
let page = ((root.QQC2.ApplicationWindow.window as Kirigami.ApplicationWindow).pageStack as Kirigami.PageRow).pushDialogLayer(Qt.createComponent('org.kde.neochat', 'ChooseRoomDialog'), {
|
||||
let page = (root.Kirigami.PageStack.pageStack as Kirigami.PageRow).pushDialogLayer(Qt.createComponent('org.kde.neochat', 'ChooseRoomDialog'), {
|
||||
connection: root.connection
|
||||
}, {
|
||||
title: i18nc("@title", "Forward Message"),
|
||||
@@ -328,13 +328,11 @@ KirigamiComponents.ConvergentContextMenu {
|
||||
icon.name: "dialog-warning-symbolic"
|
||||
visible: !root.author.isLocalMember
|
||||
onTriggered: {
|
||||
let dialog = ((root.QQC2.ApplicationWindow.window as Kirigami.ApplicationWindow).pageStack as Kirigami.PageRow).pushDialogLayer(Qt.createComponent('org.kde.neochat', 'ReasonDialog'), {
|
||||
let dialog = (root.Kirigami.PageStack.pageStack as Kirigami.PageRow).pushDialogLayer(Qt.createComponent('org.kde.neochat', 'ReasonDialog'), {
|
||||
title: i18nc("@title:dialog", "Report Message"),
|
||||
placeholder: i18nc("@info:placeholder", "Optionally give a reason for reporting this message"),
|
||||
placeholder: i18nc("@info:placeholder", "Reason for reporting this message"),
|
||||
icon: "dialog-warning-symbolic",
|
||||
actionText: i18nc("@action:button 'Report' as in 'Report this event to the administrators'", "Report"),
|
||||
reporting: true,
|
||||
connection: root.connection,
|
||||
actionText: i18nc("@action:button 'Report' as in 'Report this event to the administrators'", "Report")
|
||||
}, {
|
||||
title: i18nc("@title", "Report Message"),
|
||||
width: Kirigami.Units.gridUnit * 25
|
||||
@@ -373,7 +371,7 @@ KirigamiComponents.ConvergentContextMenu {
|
||||
Kirigami.Action {
|
||||
text: i18nc("@action:inmenu", "Configure Web Shortcuts…")
|
||||
icon.name: "configure"
|
||||
visible: !Controller.isFlatpak && webShortcutModel.enabled && webShortcutModelAction.visible
|
||||
visible: !Controller.isFlatpak && webShortcutModel.enabled
|
||||
onTriggered: webShortcutModel.configureWebShortcuts()
|
||||
}
|
||||
|
||||
|
||||
@@ -21,12 +21,16 @@ DelegateChooser {
|
||||
|
||||
DelegateChoice {
|
||||
roleValue: DelegateType.State
|
||||
delegate: StateDelegate {}
|
||||
delegate: StateDelegate {
|
||||
room: root.room
|
||||
}
|
||||
}
|
||||
|
||||
DelegateChoice {
|
||||
roleValue: DelegateType.Message
|
||||
delegate: MessageDelegate {}
|
||||
delegate: MessageDelegate {
|
||||
room: root.room
|
||||
}
|
||||
}
|
||||
|
||||
DelegateChoice {
|
||||
@@ -41,17 +45,23 @@ DelegateChooser {
|
||||
|
||||
DelegateChoice {
|
||||
roleValue: DelegateType.Predecessor
|
||||
delegate: PredecessorDelegate {}
|
||||
delegate: PredecessorDelegate {
|
||||
room: root.room
|
||||
}
|
||||
}
|
||||
|
||||
DelegateChoice {
|
||||
roleValue: DelegateType.Successor
|
||||
delegate: SuccessorDelegate {}
|
||||
delegate: SuccessorDelegate {
|
||||
room: root.room
|
||||
}
|
||||
}
|
||||
|
||||
DelegateChoice {
|
||||
roleValue: DelegateType.TimelineEnd
|
||||
delegate: TimelineEndDelegate {}
|
||||
delegate: TimelineEndDelegate {
|
||||
room: root.room
|
||||
}
|
||||
}
|
||||
|
||||
DelegateChoice {
|
||||
@@ -65,7 +75,9 @@ DelegateChooser {
|
||||
|
||||
Component {
|
||||
id: hiddenDelegate
|
||||
HiddenDelegate {}
|
||||
HiddenDelegate {
|
||||
room: root.room
|
||||
}
|
||||
}
|
||||
Component {
|
||||
id: emptyDelegate
|
||||
|
||||
@@ -92,10 +92,8 @@ TimelineDelegate {
|
||||
|
||||
QtObject {
|
||||
id: _private
|
||||
|
||||
function showMessageMenu(): void {
|
||||
let event = root.Message.room.findEvent(root.eventId);
|
||||
RoomManager.viewEventMenu(root.QQC2.Overlay.overlay, event, root.room, "");
|
||||
function showMessageMenu() {
|
||||
RoomManager.viewEventMenu(root.eventId, root.room, "");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -208,15 +208,6 @@ MessageDelegateBase {
|
||||
|
||||
readMarkerComponent: AvatarFlow {
|
||||
model: root.readMarkers
|
||||
|
||||
TapHandler {
|
||||
onTapped: {
|
||||
const dialog = Qt.createComponent("org.kde.neochat", "SeenByDialog").createObject(root, {
|
||||
model: root.readMarkers
|
||||
}) as SeenByDialog;
|
||||
dialog.open();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
compactBackgroundComponent: Rectangle {
|
||||
@@ -227,15 +218,13 @@ MessageDelegateBase {
|
||||
quickActionComponent: QuickActions {
|
||||
room: root.room
|
||||
eventId: root.eventId
|
||||
author: root.author
|
||||
}
|
||||
|
||||
QtObject {
|
||||
id: _private
|
||||
|
||||
function showMessageMenu(): void {
|
||||
let event = root.ListView.view.model.findEvent(root.eventId);
|
||||
RoomManager.viewEventMenu(root.QQC2.Overlay.overlay, event, root.room, root.Message.selectedText, root.Message.hoveredLink);
|
||||
function showMessageMenu() {
|
||||
RoomManager.viewEventMenu(root.eventId, root.room, root.Message.selectedText, root.Message.hoveredLink);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user