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