Compare commits
1 Commits
work/tobia
...
work/redst
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
db2238805b |
@@ -130,8 +130,7 @@ void EventHandlerTest::timeString()
|
||||
QLocale().toString(QDateTime::fromMSecsSinceEpoch(1432735824654, QTimeZone(QTimeZone::UTC)).toLocalTime().time(), QLocale::ShortFormat));
|
||||
QCOMPARE(EventHandler::timeString(room, event, true),
|
||||
format.formatRelativeDate(QDateTime::fromMSecsSinceEpoch(1432735824654, QTimeZone(QTimeZone::UTC)).toLocalTime().date(), QLocale::ShortFormat));
|
||||
QCOMPARE(EventHandler::timeString(room, event, u"hh:mm"_s),
|
||||
QDateTime::fromMSecsSinceEpoch(1432735824654, QTimeZone(QTimeZone::LocalTime)).toString(u"hh:mm"_s));
|
||||
QCOMPARE(EventHandler::timeString(room, event, u"hh:mm"_s), QDateTime::fromMSecsSinceEpoch(1432735824654, QTimeZone(QTimeZone::UTC)).toString(u"hh:mm"_s));
|
||||
|
||||
const auto txID = room->postJson("m.room.message"_L1, event->fullJson());
|
||||
QCOMPARE(room->pendingEvents().size(), 1);
|
||||
|
||||
561
po/ar/neochat.po
561
po/ar/neochat.po
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
499
po/az/neochat.po
499
po/az/neochat.po
File diff suppressed because it is too large
Load Diff
504
po/ca/neochat.po
504
po/ca/neochat.po
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
521
po/cs/neochat.po
521
po/cs/neochat.po
File diff suppressed because it is too large
Load Diff
493
po/da/neochat.po
493
po/da/neochat.po
File diff suppressed because it is too large
Load Diff
528
po/de/neochat.po
528
po/de/neochat.po
File diff suppressed because it is too large
Load Diff
520
po/el/neochat.po
520
po/el/neochat.po
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
528
po/eo/neochat.po
528
po/eo/neochat.po
File diff suppressed because it is too large
Load Diff
504
po/es/neochat.po
504
po/es/neochat.po
File diff suppressed because it is too large
Load Diff
633
po/eu/neochat.po
633
po/eu/neochat.po
File diff suppressed because it is too large
Load Diff
553
po/fi/neochat.po
553
po/fi/neochat.po
File diff suppressed because it is too large
Load Diff
589
po/fr/neochat.po
589
po/fr/neochat.po
File diff suppressed because it is too large
Load Diff
536
po/gl/neochat.po
536
po/gl/neochat.po
File diff suppressed because it is too large
Load Diff
511
po/he/neochat.po
511
po/he/neochat.po
File diff suppressed because it is too large
Load Diff
528
po/hi/neochat.po
528
po/hi/neochat.po
File diff suppressed because it is too large
Load Diff
954
po/hu/neochat.po
954
po/hu/neochat.po
File diff suppressed because it is too large
Load Diff
532
po/ia/neochat.po
532
po/ia/neochat.po
File diff suppressed because it is too large
Load Diff
515
po/id/neochat.po
515
po/id/neochat.po
File diff suppressed because it is too large
Load Diff
505
po/ie/neochat.po
505
po/ie/neochat.po
File diff suppressed because it is too large
Load Diff
510
po/it/neochat.po
510
po/it/neochat.po
File diff suppressed because it is too large
Load Diff
467
po/ja/neochat.po
467
po/ja/neochat.po
File diff suppressed because it is too large
Load Diff
509
po/ka/neochat.po
509
po/ka/neochat.po
File diff suppressed because it is too large
Load Diff
585
po/ko/neochat.po
585
po/ko/neochat.po
File diff suppressed because it is too large
Load Diff
467
po/lt/neochat.po
467
po/lt/neochat.po
File diff suppressed because it is too large
Load Diff
536
po/lv/neochat.po
536
po/lv/neochat.po
File diff suppressed because it is too large
Load Diff
513
po/nl/neochat.po
513
po/nl/neochat.po
File diff suppressed because it is too large
Load Diff
510
po/nn/neochat.po
510
po/nn/neochat.po
File diff suppressed because it is too large
Load Diff
498
po/pa/neochat.po
498
po/pa/neochat.po
File diff suppressed because it is too large
Load Diff
528
po/pl/neochat.po
528
po/pl/neochat.po
File diff suppressed because it is too large
Load Diff
515
po/pt/neochat.po
515
po/pt/neochat.po
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
529
po/ru/neochat.po
529
po/ru/neochat.po
File diff suppressed because it is too large
Load Diff
528
po/sa/neochat.po
528
po/sa/neochat.po
File diff suppressed because it is too large
Load Diff
499
po/sk/neochat.po
499
po/sk/neochat.po
File diff suppressed because it is too large
Load Diff
517
po/sl/neochat.po
517
po/sl/neochat.po
File diff suppressed because it is too large
Load Diff
536
po/sv/neochat.po
536
po/sv/neochat.po
File diff suppressed because it is too large
Load Diff
528
po/ta/neochat.po
528
po/ta/neochat.po
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
509
po/tr/neochat.po
509
po/tr/neochat.po
File diff suppressed because it is too large
Load Diff
508
po/uk/neochat.po
508
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
@@ -106,7 +106,7 @@ KirigamiComponents.ConvergentContextMenu {
|
||||
}
|
||||
|
||||
QQC2.Action {
|
||||
text: i18n("Logout…")
|
||||
text: i18n("Logout")
|
||||
icon.name: "im-kick-user"
|
||||
onTriggered: confirmLogoutDialogComponent.createObject(root).open()
|
||||
}
|
||||
|
||||
@@ -52,15 +52,6 @@ ColumnLayout {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
}
|
||||
|
||||
Kirigami.SelectableLabel {
|
||||
Layout.fillWidth: true
|
||||
font: Kirigami.Theme.smallFont
|
||||
textFormat: TextEdit.PlainText
|
||||
visible: root.currentRoom && root.currentRoom.canonicalAlias
|
||||
text: root.currentRoom && root.currentRoom.canonicalAlias ? root.currentRoom.canonicalAlias : ""
|
||||
color: Kirigami.Theme.disabledTextColor
|
||||
}
|
||||
|
||||
Kirigami.Heading {
|
||||
text: root.currentRoom.displayName
|
||||
|
||||
@@ -79,14 +70,7 @@ ColumnLayout {
|
||||
spacing: Kirigami.Units.smallSpacing
|
||||
|
||||
Kirigami.Heading {
|
||||
text: root.invitingMember.displayName
|
||||
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
}
|
||||
|
||||
QQC2.Label {
|
||||
text: root.invitingMember.id
|
||||
color: Kirigami.Theme.disabledTextColor
|
||||
text: root.currentRoom.displayName
|
||||
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
}
|
||||
@@ -175,7 +159,7 @@ ColumnLayout {
|
||||
|
||||
QQC2.Label {
|
||||
color: Kirigami.Theme.disabledTextColor
|
||||
text: xi18nc("@info:label Ensure you are referring to the same translation used for that settings page", "You can reject invitations from unknown users under the <interface>Security & Safety</interface> settings.")
|
||||
text: i18nc("@info:label", "You can reject invitations from unknown users under Security settings.")
|
||||
wrapMode: Text.WordWrap
|
||||
|
||||
// + 5 to prevent it from wrapping unnecessarily
|
||||
|
||||
@@ -47,7 +47,7 @@ Kirigami.Page {
|
||||
icon.name: "document-edit"
|
||||
visible: root.allowEdit
|
||||
enabled: room.canSendState(root.type) && (!root.stateKey.startsWith("@") || root.stateKey === root.room.connection.localUserId) && root.type !== "m.room.create"
|
||||
onTriggered: pageStack.pushDialogLayer(Qt.createComponent("org.kde.neochat", "EditStateDialog"), {
|
||||
onTriggered: pageStack.pushDialogLayer(Qt.createComponent("org.kde.neochat", "EditStateDialog.qml"), {
|
||||
room: root.room,
|
||||
type: root.type,
|
||||
stateKey: root.stateKey,
|
||||
|
||||
@@ -23,6 +23,11 @@ Kirigami.Dialog {
|
||||
|
||||
property NeoChatConnection connection
|
||||
|
||||
readonly property ProfileFieldsHelper profileFieldsHelper: ProfileFieldsHelper {
|
||||
connection: root.connection
|
||||
userId: root.user.id
|
||||
}
|
||||
|
||||
leftPadding: 0
|
||||
rightPadding: 0
|
||||
topPadding: 0
|
||||
@@ -126,14 +131,38 @@ Kirigami.Dialog {
|
||||
}
|
||||
}
|
||||
|
||||
Kirigami.Chip {
|
||||
visible: root.room
|
||||
text: root.room ? QmlUtils.nameForPowerLevelValue(root.room.memberEffectivePowerLevel(root.user.id)) : ""
|
||||
closable: false
|
||||
checkable: false
|
||||
RowLayout {
|
||||
spacing: Kirigami.Units.smallSpacing
|
||||
|
||||
Layout.leftMargin: Kirigami.Units.largeSpacing
|
||||
Layout.bottomMargin: Kirigami.Units.largeSpacing
|
||||
|
||||
Kirigami.Chip {
|
||||
visible: root.room
|
||||
text: root.room ? QmlUtils.nameForPowerLevelValue(root.room.memberEffectivePowerLevel(root.user.id)) : ""
|
||||
closable: false
|
||||
checkable: false
|
||||
}
|
||||
|
||||
QQC2.BusyIndicator {
|
||||
visible: root.connection.supportsProfileFields && root.profileFieldsHelper.loading
|
||||
}
|
||||
|
||||
Kirigami.Chip {
|
||||
id: timezoneChip
|
||||
visible: root.connection.supportsProfileFields && !root.profileFieldsHelper.loading && root.profileFieldsHelper.timezone.length > 0
|
||||
text: root.profileFieldsHelper.timezone
|
||||
closable: false
|
||||
checkable: false
|
||||
}
|
||||
|
||||
Kirigami.Chip {
|
||||
id: pronounsChip
|
||||
visible: root.connection.supportsProfileFields && !root.profileFieldsHelper.loading && root.profileFieldsHelper.pronouns.length > 0
|
||||
text: root.profileFieldsHelper.pronouns
|
||||
closable: false
|
||||
checkable: false
|
||||
}
|
||||
}
|
||||
|
||||
Kirigami.Separator {
|
||||
|
||||
@@ -236,18 +236,11 @@ void RoomManager::resolveResource(Uri uri, const QString &action)
|
||||
}
|
||||
}
|
||||
|
||||
void RoomManager::maximizeMedia(const QString &eventId)
|
||||
void RoomManager::maximizeMedia(int index)
|
||||
{
|
||||
if (eventId.isEmpty()) {
|
||||
qWarning() << "Tried to open media for empty event id";
|
||||
if (index < -1 || index > m_mediaMessageFilterModel->rowCount()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto index = m_mediaMessageFilterModel->getRowForEventId(eventId);
|
||||
if (index == -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
Q_EMIT showMaximizedMedia(index);
|
||||
}
|
||||
|
||||
@@ -404,9 +397,7 @@ void RoomManager::joinRoom(Quotient::Connection *account, const QString &roomAli
|
||||
|
||||
// If no one gives us a homeserver suggestion, try the server specified in the alias/id.
|
||||
// Otherwise joining a remote room not on our homeserver will fail.
|
||||
// This is a hack and we're not supposed to do it. With room ids not containing the server going forward, it won't work anymore for new room versions.
|
||||
// FIXME: Let's keep it around anyway for now, remove it at some point, though
|
||||
if (vias.empty() && roomAliasOrId.contains(':'_L1)) {
|
||||
if (vias.empty()) {
|
||||
vias.append(roomAliasOrId.mid(roomAliasOrId.lastIndexOf(':'_L1) + 1));
|
||||
}
|
||||
|
||||
|
||||
@@ -212,8 +212,12 @@ public:
|
||||
|
||||
/**
|
||||
* @brief Show a media item maximized.
|
||||
*
|
||||
* @param index the index to open the maximize delegate model at. This is the
|
||||
* index in the MediaMessageFilterModel owned by this RoomManager. A value
|
||||
* of -1 opens a the default item.
|
||||
*/
|
||||
Q_INVOKABLE void maximizeMedia(const QString &eventId);
|
||||
Q_INVOKABLE void maximizeMedia(int index);
|
||||
|
||||
Q_INVOKABLE void maximizeCode(NeochatRoomMember *author, const QDateTime &time, const QString &codeText, const QString &language);
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ target_sources(LibNeoChat PRIVATE
|
||||
texthandler.cpp
|
||||
urlhelper.cpp
|
||||
utils.cpp
|
||||
profilefieldshelper.cpp
|
||||
enums/messagecomponenttype.h
|
||||
enums/messagetype.h
|
||||
enums/powerlevel.cpp
|
||||
@@ -32,6 +33,7 @@ target_sources(LibNeoChat PRIVATE
|
||||
events/imagepackevent.cpp
|
||||
events/pollevent.cpp
|
||||
jobs/neochatgetcommonroomsjob.cpp
|
||||
jobs/neochatprofilefieldjobs.cpp
|
||||
models/actionsmodel.cpp
|
||||
models/completionmodel.cpp
|
||||
models/completionproxymodel.cpp
|
||||
@@ -44,6 +46,8 @@ target_sources(LibNeoChat PRIVATE
|
||||
models/stickermodel.cpp
|
||||
models/userfiltermodel.cpp
|
||||
models/userlistmodel.cpp
|
||||
models/timezonemodel.cpp
|
||||
models/timezonemodel.h
|
||||
)
|
||||
|
||||
ecm_add_qml_module(LibNeoChat GENERATE_PLUGIN_SOURCE
|
||||
|
||||
@@ -70,23 +70,13 @@ public:
|
||||
*
|
||||
* @param event the event to return a type for.
|
||||
*
|
||||
* @param isInReply whether this event is to be treated like a replied-to event (i.e., a basic text fallback should be shown if no other type is used)
|
||||
*
|
||||
* @sa Type
|
||||
*/
|
||||
static Type typeForEvent(const Quotient::RoomEvent &event, bool isInReply = false)
|
||||
static Type typeForEvent(const Quotient::RoomEvent &event)
|
||||
{
|
||||
using namespace Quotient;
|
||||
|
||||
if (event.isRedacted()) {
|
||||
return MessageComponentType::Text;
|
||||
}
|
||||
|
||||
if (const auto e = eventCast<const RoomMessageEvent>(&event)) {
|
||||
if (e->rawMsgtype() == u"m.key.verification.request"_s) {
|
||||
return MessageComponentType::Verification;
|
||||
}
|
||||
|
||||
switch (e->msgtype()) {
|
||||
case MessageEventType::Emote:
|
||||
return MessageComponentType::Text;
|
||||
@@ -113,8 +103,7 @@ public:
|
||||
if (event.matrixType() == u"org.matrix.msc3672.beacon_info"_s) {
|
||||
return MessageComponentType::LiveLocation;
|
||||
}
|
||||
// In the (unlikely) case that this is a reply to a state event, we do want to show something
|
||||
return isInReply ? MessageComponentType::Text : MessageComponentType::Other;
|
||||
return MessageComponentType::Other;
|
||||
}
|
||||
if (is<const EncryptedEvent>(event)) {
|
||||
return MessageComponentType::Encrypted;
|
||||
@@ -127,8 +116,7 @@ public:
|
||||
return MessageComponentType::Poll;
|
||||
}
|
||||
|
||||
// In the (unlikely) case that this is a reply to an unusual event, we do want to show something
|
||||
return isInReply ? MessageComponentType::Text : MessageComponentType::Other;
|
||||
return MessageComponentType::Other;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -448,12 +448,6 @@ QString EventHandler::getBody(const NeoChatRoom *room, const Quotient::RoomEvent
|
||||
[](const PollStartEvent &e) {
|
||||
return e.question();
|
||||
},
|
||||
[](const EncryptedEvent &) {
|
||||
return i18nc("@info In room list", "Encrypted event");
|
||||
},
|
||||
[](const ReactionEvent &e) {
|
||||
return i18nc("[user] reacted with <emoji>", "reacted with %1", e.key());
|
||||
},
|
||||
i18n("Unknown event"));
|
||||
}
|
||||
|
||||
|
||||
20
src/libneochat/jobs/neochatprofilefieldjobs.cpp
Normal file
20
src/libneochat/jobs/neochatprofilefieldjobs.cpp
Normal file
@@ -0,0 +1,20 @@
|
||||
// SPDX-FileCopyrightText: 2025 Joshua Goins <josh@redstrate.com>
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#include "neochatprofilefieldjobs.h"
|
||||
|
||||
using namespace Quotient;
|
||||
|
||||
NeoChatGetProfileFieldJob::NeoChatGetProfileFieldJob(const QString &userId, const QString &key)
|
||||
: BaseJob(HttpVerb::Get, u"GetProfileFieldJob"_s, makePath("/_matrix/client/unstable/uk.tcpip.msc4133", "/profile/", userId, "/", key))
|
||||
, m_key(key)
|
||||
{
|
||||
}
|
||||
|
||||
NeoChatSetProfileFieldJob::NeoChatSetProfileFieldJob(const QString &userId, const QString &key, const QString &value)
|
||||
: BaseJob(HttpVerb::Put, u"SetProfileFieldJob"_s, makePath("/_matrix/client/unstable/uk.tcpip.msc4133", "/profile/", userId, "/", key))
|
||||
{
|
||||
QJsonObject _dataJson;
|
||||
addParam(_dataJson, key, value);
|
||||
setRequestData({_dataJson});
|
||||
}
|
||||
49
src/libneochat/jobs/neochatprofilefieldjobs.h
Normal file
49
src/libneochat/jobs/neochatprofilefieldjobs.h
Normal file
@@ -0,0 +1,49 @@
|
||||
// SPDX-FileCopyrightText: 2025 Joshua Goins <josh@redstrate.com>
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <Quotient/jobs/basejob.h>
|
||||
|
||||
// NOTE: This is currently being upstreamed to libQuotient, awaiting MSC4133: https://github.com/quotient-im/libQuotient/pull/869
|
||||
|
||||
//! \brief Get a user's profile field.
|
||||
//!
|
||||
//! Get one of the user's profile fields. This API may be used to fetch the user's
|
||||
//! own profile field or to query the profile field of other users; either locally or
|
||||
//! on remote homeservers.
|
||||
class QUOTIENT_API NeoChatGetProfileFieldJob : public Quotient::BaseJob
|
||||
{
|
||||
public:
|
||||
//! \param userId
|
||||
//! The user whose profile field to query.
|
||||
//! \param key
|
||||
//! The key of the profile field.
|
||||
explicit NeoChatGetProfileFieldJob(const QString &userId, const QString &key);
|
||||
|
||||
// Result properties
|
||||
|
||||
//! The value of the profile field.
|
||||
QString value() const
|
||||
{
|
||||
return loadFromJson<QString>(m_key);
|
||||
}
|
||||
|
||||
private:
|
||||
QString m_key;
|
||||
};
|
||||
|
||||
//! \brief Sets a user's profile field.
|
||||
//!
|
||||
//! Set one of the user's own profile fields. This may fail depending on if the server allows the
|
||||
//! user to change their own profile field, or if the field isn't allowed.
|
||||
class QUOTIENT_API NeoChatSetProfileFieldJob : public Quotient::BaseJob
|
||||
{
|
||||
public:
|
||||
//! \param userId
|
||||
//! The user whose avatar URL to set.
|
||||
//!
|
||||
//! \param avatarUrl
|
||||
//! The new avatar URL for this user.
|
||||
explicit NeoChatSetProfileFieldJob(const QString &userId, const QString &key, const QString &value);
|
||||
};
|
||||
@@ -31,7 +31,13 @@ auto leaveRoomLambda = [](const QString &text, NeoChatRoom *room, ChatBarCache *
|
||||
Q_EMIT room->showMessage(MessageType::Information, i18n("Leaving this room."));
|
||||
room->forget();
|
||||
} else {
|
||||
// FIXME: re-add sanity check for roomId/alias
|
||||
QRegularExpression roomRegex(uR"(^[#!][^:]+:\w(?:\w|\.|-)*\.\w+(?::\d{1,5})?)"_s);
|
||||
auto regexMatch = roomRegex.match(text);
|
||||
if (!regexMatch.hasMatch()) {
|
||||
Q_EMIT room->showMessage(MessageType::Error,
|
||||
i18nc("'<text>' does not look like a room id or alias.", "'%1' does not look like a room id or alias.", text));
|
||||
return QString();
|
||||
}
|
||||
auto leaving = dynamic_cast<NeoChatRoom *>(room->connection()->room(text));
|
||||
if (!leaving) {
|
||||
leaving = dynamic_cast<NeoChatRoom *>(room->connection()->roomByAlias(text));
|
||||
@@ -211,7 +217,13 @@ QList<ActionsModel::Action> actions{
|
||||
Action{
|
||||
u"join"_s,
|
||||
[](const QString &text, NeoChatRoom *room, ChatBarCache *) {
|
||||
// FIXME: re-add sanity check for roomId/alias
|
||||
QRegularExpression roomRegex(uR"(^[#!][^:]+:\w(?:\w|\.|-)*\.\w+(?::\d{1,5})?)"_s);
|
||||
auto regexMatch = roomRegex.match(text);
|
||||
if (!regexMatch.hasMatch()) {
|
||||
Q_EMIT room->showMessage(MessageType::Error,
|
||||
i18nc("'<text>' does not look like a room id or alias.", "'%1' does not look like a room id or alias.", text));
|
||||
return QString();
|
||||
}
|
||||
auto targetRoom = text.startsWith(QLatin1Char('!')) ? room->connection()->room(text) : room->connection()->roomByAlias(text);
|
||||
if (targetRoom) {
|
||||
ActionsModel::instance().resolveResource(targetRoom->id());
|
||||
@@ -230,18 +242,25 @@ QList<ActionsModel::Action> actions{
|
||||
[](const QString &text, NeoChatRoom *room, ChatBarCache *) {
|
||||
auto parts = text.split(u" "_s);
|
||||
QString roomName = parts[0];
|
||||
// FIXME: re-add sanity check for roomId/alias
|
||||
if (const auto targetRoom = text.startsWith(QLatin1Char('!')) ? room->connection()->room(text) : room->connection()->roomByAlias(text)) {
|
||||
QRegularExpression roomRegex(uR"(^[#!][^:]+:\w(?:\w|\.|-)*\.\w+(?::\d{1,5})?)"_s);
|
||||
auto regexMatch = roomRegex.match(roomName);
|
||||
if (!regexMatch.hasMatch()) {
|
||||
Q_EMIT room->showMessage(MessageType::Error,
|
||||
i18nc("'<text>' does not look like a room id or alias.", "'%1' does not look like a room id or alias.", text));
|
||||
return QString();
|
||||
}
|
||||
auto targetRoom = text.startsWith(QLatin1Char('!')) ? room->connection()->room(text) : room->connection()->roomByAlias(text);
|
||||
if (targetRoom) {
|
||||
ActionsModel::instance().resolveResource(targetRoom->id());
|
||||
return QString();
|
||||
}
|
||||
Q_EMIT room->showMessage(MessageType::Information, i18nc("Knocking room <roomname>.", "Knocking room %1.", text));
|
||||
auto connection = dynamic_cast<NeoChatConnection *>(room->connection());
|
||||
const auto knownServer = roomName.contains(":"_L1) ? QStringList{roomName.mid(roomName.indexOf(":"_L1) + 1)} : QStringList();
|
||||
const auto knownServer = roomName.mid(roomName.indexOf(":"_L1) + 1);
|
||||
if (parts.length() >= 2) {
|
||||
ActionsModel::instance().knockRoom(connection, roomName, parts[1], knownServer);
|
||||
ActionsModel::instance().knockRoom(connection, roomName, parts[1], QStringList{knownServer});
|
||||
} else {
|
||||
ActionsModel::instance().knockRoom(connection, roomName, QString(), knownServer);
|
||||
ActionsModel::instance().knockRoom(connection, roomName, QString(), QStringList{knownServer});
|
||||
}
|
||||
return QString();
|
||||
},
|
||||
@@ -252,7 +271,13 @@ QList<ActionsModel::Action> actions{
|
||||
Action{
|
||||
u"j"_s,
|
||||
[](const QString &text, NeoChatRoom *room, ChatBarCache *) {
|
||||
// FIXME: re-add sanity check for roomId/alias
|
||||
QRegularExpression roomRegex(uR"(^[#!][^:]+:\w(?:\w|\.|-)*\.\w+(?::\d{1,5})?)"_s);
|
||||
auto regexMatch = roomRegex.match(text);
|
||||
if (!regexMatch.hasMatch()) {
|
||||
Q_EMIT room->showMessage(MessageType::Error,
|
||||
i18nc("'<text>' does not look like a room id or alias.", "'%1' does not look like a room id or alias.", text));
|
||||
return QString();
|
||||
}
|
||||
if (room->connection()->room(text) || room->connection()->roomByAlias(text)) {
|
||||
Q_EMIT room->showMessage(MessageType::Information, i18nc("You are already in room <roomname>.", "You are already in room %1.", text));
|
||||
return QString();
|
||||
|
||||
48
src/libneochat/models/timezonemodel.cpp
Normal file
48
src/libneochat/models/timezonemodel.cpp
Normal file
@@ -0,0 +1,48 @@
|
||||
// SPDX-FileCopyrightText: 2025 Joshua Goins <josh@redstrate.com>
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#include "timezonemodel.h"
|
||||
|
||||
#include <KLocalizedString>
|
||||
|
||||
using namespace Qt::Literals::StringLiterals;
|
||||
|
||||
TimeZoneModel::TimeZoneModel(QObject *parent)
|
||||
: QAbstractListModel(parent)
|
||||
{
|
||||
m_timezoneIds = QTimeZone::availableTimeZoneIds();
|
||||
}
|
||||
|
||||
QVariant TimeZoneModel::data(const QModelIndex &index, int role) const
|
||||
{
|
||||
switch (role) {
|
||||
case Qt::DisplayRole: {
|
||||
if (index.row() == 0) {
|
||||
return i18nc("@item:inlistbox Prefer not to say which timezone im in", "Prefer not to say");
|
||||
} else {
|
||||
return m_timezoneIds[index.row() - 1];
|
||||
}
|
||||
}
|
||||
default:
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
int TimeZoneModel::rowCount(const QModelIndex &parent) const
|
||||
{
|
||||
Q_UNUSED(parent);
|
||||
|
||||
return m_timezoneIds.count() + 1;
|
||||
}
|
||||
|
||||
int TimeZoneModel::indexOfValue(const QString &code)
|
||||
{
|
||||
const auto it = std::ranges::find(std::as_const(m_timezoneIds), code.toUtf8());
|
||||
if (it != m_timezoneIds.cend()) {
|
||||
return std::distance(m_timezoneIds.cbegin(), it) + 1;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
#include "moc_timezonemodel.cpp"
|
||||
24
src/libneochat/models/timezonemodel.h
Normal file
24
src/libneochat/models/timezonemodel.h
Normal file
@@ -0,0 +1,24 @@
|
||||
// SPDX-FileCopyrightText: 2025 Joshua Goins <josh@redstrate.com>
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QAbstractListModel>
|
||||
#include <QQmlEngine>
|
||||
|
||||
class TimeZoneModel : public QAbstractListModel
|
||||
{
|
||||
Q_OBJECT
|
||||
QML_ELEMENT
|
||||
|
||||
public:
|
||||
explicit TimeZoneModel(QObject *parent = nullptr);
|
||||
|
||||
[[nodiscard]] QVariant data(const QModelIndex &index, int role) const override;
|
||||
[[nodiscard]] int rowCount(const QModelIndex &parent) const override;
|
||||
|
||||
Q_INVOKABLE int indexOfValue(const QString &code);
|
||||
|
||||
private:
|
||||
QList<QByteArray> m_timezoneIds;
|
||||
};
|
||||
@@ -100,10 +100,6 @@ QVariant UserListModel::data(const QModelIndex &index, int role) const
|
||||
return plEvent->powerLevelForUser(memberId);
|
||||
}
|
||||
if (role == PowerLevelStringRole) {
|
||||
if (m_currentRoom->roomCreatorHasUltimatePowerLevel() && m_currentRoom->isCreator(memberId)) {
|
||||
return i18nc("@info the person that created this room", "Creator");
|
||||
}
|
||||
|
||||
auto pl = m_currentRoom->currentState().get<RoomPowerLevelsEvent>();
|
||||
// User might not in the room yet, in this case pl can be nullptr.
|
||||
// e.g. When invited but user not accepted or denied the invitation.
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
#include <QImageReader>
|
||||
#include <QJsonDocument>
|
||||
|
||||
#include "jobs/neochatprofilefieldjobs.h"
|
||||
#include "neochatroom.h"
|
||||
#include "spacehierarchycache.h"
|
||||
|
||||
@@ -139,6 +140,19 @@ void NeoChatConnection::connectSignals()
|
||||
Q_EMIT canCheckMutualRoomsChanged();
|
||||
m_canEraseData = job->unstableFeatures().contains("org.matrix.msc4025"_L1) || job->versions().count("v1.10"_L1);
|
||||
Q_EMIT canEraseDataChanged();
|
||||
m_supportsProfileFields = job->unstableFeatures().contains("uk.tcpip.msc4133"_L1);
|
||||
Q_EMIT supportsProfileFieldsChanged();
|
||||
|
||||
if (m_supportsProfileFields) {
|
||||
callApi<NeoChatGetProfileFieldJob>(BackgroundRequest, userId(), QStringLiteral("us.cloke.msc4175.tz")).then([this](const auto &job) {
|
||||
m_timezone = job->value();
|
||||
Q_EMIT timezoneChanged();
|
||||
});
|
||||
callApi<NeoChatGetProfileFieldJob>(BackgroundRequest, userId(), QStringLiteral("io.fsky.nyx.pronouns")).then([this](const auto &job) {
|
||||
m_pronouns = job->value();
|
||||
Q_EMIT pronounsChanged();
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
Qt::SingleShotConnection);
|
||||
@@ -558,4 +572,33 @@ bool NeoChatConnection::enablePushNotifications() const
|
||||
return m_pushNotificationsEnabled;
|
||||
}
|
||||
|
||||
bool NeoChatConnection::supportsProfileFields() const
|
||||
{
|
||||
return m_supportsProfileFields;
|
||||
}
|
||||
|
||||
QString NeoChatConnection::timezone() const
|
||||
{
|
||||
return m_timezone;
|
||||
}
|
||||
|
||||
void NeoChatConnection::setTimezone(const QString &value)
|
||||
{
|
||||
callApi<NeoChatSetProfileFieldJob>(BackgroundRequest, userId(), QStringLiteral("us.cloke.msc4175.tz"), value);
|
||||
}
|
||||
|
||||
QString NeoChatConnection::pronouns() const
|
||||
{
|
||||
return m_pronouns;
|
||||
}
|
||||
|
||||
void NeoChatConnection::setPronouns(const QString &value)
|
||||
{
|
||||
const QJsonObject pronounsObj{{"summary"_L1, value}};
|
||||
callApi<NeoChatSetProfileFieldJob>(BackgroundRequest,
|
||||
userId(),
|
||||
QStringLiteral("io.fsky.nyx.pronouns"),
|
||||
QString::fromUtf8(QJsonDocument(pronounsObj).toJson(QJsonDocument::Compact)));
|
||||
}
|
||||
|
||||
#include "moc_neochatconnection.cpp"
|
||||
|
||||
@@ -90,6 +90,21 @@ class NeoChatConnection : public Quotient::Connection
|
||||
*/
|
||||
Q_PROPERTY(bool enablePushNotifications READ enablePushNotifications NOTIFY enablePushNotificationsChanged)
|
||||
|
||||
/**
|
||||
* @brief If the server supports profile fields (MSC4133)
|
||||
*/
|
||||
Q_PROPERTY(bool supportsProfileFields READ supportsProfileFields NOTIFY supportsProfileFieldsChanged)
|
||||
|
||||
/**
|
||||
* @brief The timezone profile field for this account.
|
||||
*/
|
||||
Q_PROPERTY(QString timezone READ timezone WRITE setTimezone NOTIFY timezoneChanged)
|
||||
|
||||
/**
|
||||
* @brief The pronouns profile field for this account.
|
||||
*/
|
||||
Q_PROPERTY(QString pronouns READ pronouns WRITE setPronouns NOTIFY pronounsChanged)
|
||||
|
||||
public:
|
||||
/**
|
||||
* @brief Defines the status after an attempt to change the password on an account.
|
||||
@@ -204,6 +219,14 @@ public:
|
||||
bool pushNotificationsAvailable() const;
|
||||
bool enablePushNotifications() const;
|
||||
|
||||
bool supportsProfileFields() const;
|
||||
|
||||
QString timezone() const;
|
||||
void setTimezone(const QString &value);
|
||||
|
||||
QString pronouns() const;
|
||||
void setPronouns(const QString &value);
|
||||
|
||||
LinkPreviewer *previewerForLink(const QUrl &link);
|
||||
|
||||
Q_SIGNALS:
|
||||
@@ -221,6 +244,9 @@ Q_SIGNALS:
|
||||
void canCheckMutualRoomsChanged();
|
||||
void canEraseDataChanged();
|
||||
void enablePushNotificationsChanged();
|
||||
void supportsProfileFieldsChanged();
|
||||
void timezoneChanged();
|
||||
void pronounsChanged();
|
||||
|
||||
/**
|
||||
* @brief Request a message be shown to the user of the given type.
|
||||
@@ -250,4 +276,7 @@ private:
|
||||
bool m_canCheckMutualRooms = false;
|
||||
bool m_canEraseData = false;
|
||||
bool m_pushNotificationsEnabled = false;
|
||||
bool m_supportsProfileFields = false;
|
||||
QString m_timezone;
|
||||
QString m_pronouns;
|
||||
};
|
||||
|
||||
@@ -359,14 +359,9 @@ const RoomEvent *NeoChatRoom::lastEvent(std::function<bool(const RoomEvent *)> f
|
||||
if (auto lastEvent = eventCast<const RoomMessageEvent>(event)) {
|
||||
return lastEvent;
|
||||
}
|
||||
|
||||
if (auto lastEvent = eventCast<const PollStartEvent>(event)) {
|
||||
return lastEvent;
|
||||
}
|
||||
|
||||
if (auto lastEvent = eventCast<const EncryptedEvent>(event)) {
|
||||
return lastEvent;
|
||||
}
|
||||
}
|
||||
|
||||
if (m_cachedEvent != nullptr) {
|
||||
@@ -446,19 +441,20 @@ void NeoChatRoom::onRedaction(const RoomEvent &prevEvent, const RoomEvent & /*af
|
||||
}
|
||||
}
|
||||
|
||||
QDateTime NeoChatRoom::lastActiveTime() const
|
||||
QDateTime NeoChatRoom::lastActiveTime()
|
||||
{
|
||||
// Find the last relevant event:
|
||||
if (const auto event = lastEvent(m_hiddenFilter)) {
|
||||
if (timelineSize() == 0) {
|
||||
if (m_cachedEvent != nullptr) {
|
||||
return m_cachedEvent->originTimestamp();
|
||||
}
|
||||
return QDateTime();
|
||||
}
|
||||
|
||||
if (auto event = lastEvent()) {
|
||||
return event->originTimestamp();
|
||||
}
|
||||
|
||||
// If nothing is loaded yet, and there is no cached event:
|
||||
if (timelineSize() == 0) {
|
||||
return {};
|
||||
}
|
||||
|
||||
// No message found, take last event:
|
||||
// no message found, take last event
|
||||
return messageEvents().rbegin()->get()->originTimestamp();
|
||||
}
|
||||
|
||||
@@ -536,9 +532,6 @@ bool NeoChatRoom::containsUser(const QString &userID) const
|
||||
|
||||
bool NeoChatRoom::canSendEvent(const QString &eventType) const
|
||||
{
|
||||
if (roomCreatorHasUltimatePowerLevel() && isCreator(localMember().id())) {
|
||||
return true;
|
||||
}
|
||||
auto plEvent = currentState().get<RoomPowerLevelsEvent>();
|
||||
if (!plEvent) {
|
||||
return false;
|
||||
@@ -551,9 +544,6 @@ bool NeoChatRoom::canSendEvent(const QString &eventType) const
|
||||
|
||||
bool NeoChatRoom::canSendState(const QString &eventType) const
|
||||
{
|
||||
if (roomCreatorHasUltimatePowerLevel() && isCreator(localMember().id())) {
|
||||
return true;
|
||||
}
|
||||
auto plEvent = currentState().get<RoomPowerLevelsEvent>();
|
||||
if (!plEvent) {
|
||||
return false;
|
||||
@@ -1680,14 +1670,8 @@ void NeoChatRoom::setRoomState(const QString &type, const QString &stateKey, con
|
||||
|
||||
NeochatRoomMember *NeoChatRoom::qmlSafeMember(const QString &memberId)
|
||||
{
|
||||
if (memberId.isEmpty()) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
if (!m_memberObjects.contains(memberId)) {
|
||||
auto member = m_memberObjects.emplace(memberId, std::make_unique<NeochatRoomMember>(this, memberId)).first->second.get();
|
||||
QQmlEngine::setObjectOwnership(member, QQmlEngine::CppOwnership);
|
||||
return member;
|
||||
return m_memberObjects.emplace(memberId, std::make_unique<NeochatRoomMember>(this, memberId)).first->second.get();
|
||||
}
|
||||
|
||||
return m_memberObjects[memberId].get();
|
||||
@@ -1747,20 +1731,4 @@ void NeoChatRoom::setHiddenFilter(std::function<bool(const Quotient::RoomEvent *
|
||||
NeoChatRoom::m_hiddenFilter = hiddenFilter;
|
||||
}
|
||||
|
||||
bool NeoChatRoom::roomCreatorHasUltimatePowerLevel() const
|
||||
{
|
||||
bool ok = false;
|
||||
auto version = this->version().toInt(&ok);
|
||||
// This is terrible. For non-numeric room versions, I don't think there's a way of knowing whether they're pre- or post hydra.
|
||||
// We just assume they are. Shouldn't matter for normal users anyway.
|
||||
return !ok || version > 11;
|
||||
}
|
||||
|
||||
bool NeoChatRoom::isCreator(const QString &userId) const
|
||||
{
|
||||
auto createEvent = currentState().get<RoomCreateEvent>();
|
||||
return roomCreatorHasUltimatePowerLevel() && createEvent
|
||||
&& (createEvent->senderId() == userId || createEvent->contentPart<QStringList>(u"additional_creators"_s).contains(userId));
|
||||
}
|
||||
|
||||
#include "moc_neochatroom.cpp"
|
||||
|
||||
@@ -208,7 +208,7 @@ public:
|
||||
bool visible() const;
|
||||
void setVisible(bool visible);
|
||||
|
||||
[[nodiscard]] QDateTime lastActiveTime() const;
|
||||
[[nodiscard]] QDateTime lastActiveTime();
|
||||
|
||||
/**
|
||||
* @brief Get the last interesting event.
|
||||
@@ -589,18 +589,6 @@ public:
|
||||
|
||||
static void setHiddenFilter(std::function<bool(const Quotient::RoomEvent *)> hiddenFilter);
|
||||
|
||||
/**
|
||||
* @brief Whether this room has a room version where the creator is treated as having an ultimate power level
|
||||
*
|
||||
* For unusual room versions, this information might be wrong.
|
||||
*/
|
||||
bool roomCreatorHasUltimatePowerLevel() const;
|
||||
|
||||
/**
|
||||
* @brief Whether this user is considered a creator of this room. Only applies to post-v12 rooms.
|
||||
*/
|
||||
bool isCreator(const QString &userId) const;
|
||||
|
||||
private:
|
||||
bool m_visible = false;
|
||||
|
||||
|
||||
110
src/libneochat/profilefieldshelper.cpp
Normal file
110
src/libneochat/profilefieldshelper.cpp
Normal file
@@ -0,0 +1,110 @@
|
||||
// SPDX-FileCopyrightText: 2025 Joshua Goins <josh@redstrate.com>
|
||||
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||
|
||||
#include "profilefieldshelper.h"
|
||||
#include "jobs/neochatprofilefieldjobs.h"
|
||||
#include "neochatconnection.h"
|
||||
|
||||
#include <Quotient/csapi/profile.h>
|
||||
|
||||
using namespace Quotient;
|
||||
|
||||
NeoChatConnection *ProfileFieldsHelper::connection() const
|
||||
{
|
||||
return m_connection.get();
|
||||
}
|
||||
|
||||
void ProfileFieldsHelper::setConnection(NeoChatConnection *connection)
|
||||
{
|
||||
if (m_connection != connection) {
|
||||
m_connection = connection;
|
||||
Q_EMIT connectionChanged();
|
||||
load();
|
||||
}
|
||||
}
|
||||
|
||||
QString ProfileFieldsHelper::userId() const
|
||||
{
|
||||
return m_userId;
|
||||
}
|
||||
|
||||
void ProfileFieldsHelper::setUserId(const QString &id)
|
||||
{
|
||||
if (m_userId != id) {
|
||||
m_userId = id;
|
||||
Q_EMIT userIdChanged();
|
||||
load();
|
||||
}
|
||||
}
|
||||
|
||||
QString ProfileFieldsHelper::timezone() const
|
||||
{
|
||||
if (m_timezone.isEmpty()) {
|
||||
return {};
|
||||
}
|
||||
return QTimeZone(m_timezone.toUtf8()).displayName(QTimeZone::GenericTime);
|
||||
}
|
||||
|
||||
QString ProfileFieldsHelper::pronouns() const
|
||||
{
|
||||
return m_pronouns;
|
||||
}
|
||||
|
||||
bool ProfileFieldsHelper::loading() const
|
||||
{
|
||||
return m_loading;
|
||||
}
|
||||
|
||||
void ProfileFieldsHelper::load()
|
||||
{
|
||||
if (!m_connection || m_userId.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!m_connection->supportsProfileFields()) {
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
|
||||
m_connection->callApi<NeoChatGetProfileFieldJob>(BackgroundRequest, m_userId, QStringLiteral("us.cloke.msc4175.tz"))
|
||||
.then(
|
||||
[this](const auto &job) {
|
||||
m_timezone = job->value();
|
||||
Q_EMIT timezoneChanged();
|
||||
m_fetchedTimezone = true;
|
||||
checkIfFinished();
|
||||
},
|
||||
[this] {
|
||||
m_fetchedTimezone = true;
|
||||
checkIfFinished();
|
||||
});
|
||||
|
||||
m_connection->callApi<NeoChatGetProfileFieldJob>(BackgroundRequest, m_userId, QStringLiteral("io.fsky.nyx.pronouns"))
|
||||
.then(
|
||||
[this](const auto &job) {
|
||||
const QJsonDocument document = QJsonDocument::fromJson(job->value().toUtf8());
|
||||
m_pronouns = document["summary"_L1].toString();
|
||||
Q_EMIT pronounsChanged();
|
||||
m_fetchedPronouns = true;
|
||||
checkIfFinished();
|
||||
},
|
||||
[this] {
|
||||
m_fetchedPronouns = true;
|
||||
checkIfFinished();
|
||||
});
|
||||
}
|
||||
|
||||
void ProfileFieldsHelper::checkIfFinished()
|
||||
{
|
||||
if (m_fetchedTimezone && m_fetchedPronouns) {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
void ProfileFieldsHelper::setLoading(const bool loading)
|
||||
{
|
||||
m_loading = loading;
|
||||
Q_EMIT loadingChanged();
|
||||
}
|
||||
79
src/libneochat/profilefieldshelper.h
Normal file
79
src/libneochat/profilefieldshelper.h
Normal file
@@ -0,0 +1,79 @@
|
||||
// SPDX-FileCopyrightText: 2025 Joshua Goins <josh@redstrate.com>
|
||||
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QObject>
|
||||
#include <QQmlEngine>
|
||||
|
||||
#include <Quotient/jobs/basejob.h>
|
||||
|
||||
class NeoChatConnection;
|
||||
|
||||
/**
|
||||
* @class ProfileFieldsHelper
|
||||
*
|
||||
* This class is designed to help grabbing the profile fields of a user.
|
||||
*/
|
||||
class ProfileFieldsHelper : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
QML_ELEMENT
|
||||
|
||||
/**
|
||||
* @brief The connection to use.
|
||||
*/
|
||||
Q_PROPERTY(NeoChatConnection *connection READ connection WRITE setConnection NOTIFY connectionChanged REQUIRED)
|
||||
|
||||
/**
|
||||
* @brief The id of the user to grab profile fields from.
|
||||
*/
|
||||
Q_PROPERTY(QString userId READ userId WRITE setUserId NOTIFY userIdChanged REQUIRED)
|
||||
|
||||
/**
|
||||
* @brief The timezone field of the user.
|
||||
*/
|
||||
Q_PROPERTY(QString timezone READ timezone NOTIFY timezoneChanged)
|
||||
|
||||
/**
|
||||
* @brief The pronouns field of the user.
|
||||
*/
|
||||
Q_PROPERTY(QString pronouns READ pronouns NOTIFY pronounsChanged)
|
||||
|
||||
/**
|
||||
* @brief If the fields are loading.
|
||||
*/
|
||||
Q_PROPERTY(bool loading READ loading NOTIFY loadingChanged)
|
||||
|
||||
public:
|
||||
[[nodiscard]] NeoChatConnection *connection() const;
|
||||
void setConnection(NeoChatConnection *connection);
|
||||
|
||||
[[nodiscard]] QString userId() const;
|
||||
void setUserId(const QString &id);
|
||||
|
||||
[[nodiscard]] QString timezone() const;
|
||||
[[nodiscard]] QString pronouns() const;
|
||||
|
||||
[[nodiscard]] bool loading() const;
|
||||
|
||||
Q_SIGNALS:
|
||||
void connectionChanged();
|
||||
void userIdChanged();
|
||||
void timezoneChanged();
|
||||
void pronounsChanged();
|
||||
void loadingChanged();
|
||||
|
||||
private:
|
||||
void load();
|
||||
void checkIfFinished();
|
||||
void setLoading(bool loading);
|
||||
|
||||
QPointer<NeoChatConnection> m_connection;
|
||||
QString m_userId;
|
||||
bool m_loading = true;
|
||||
QString m_timezone;
|
||||
bool m_fetchedTimezone = false;
|
||||
QString m_pronouns;
|
||||
bool m_fetchedPronouns = false;
|
||||
};
|
||||
@@ -570,9 +570,8 @@ QVariantMap TextHandler::getAttributes(const QString &tag, const QString &tagStr
|
||||
QList<MessageComponent>
|
||||
TextHandler::textComponents(QString string, Qt::TextFormat inputFormat, const NeoChatRoom *room, const Quotient::RoomEvent *event, bool isEdited)
|
||||
{
|
||||
if (string.trimmed().isEmpty() && event->is<Quotient::RoomMessageEvent>()
|
||||
&& !eventCast<const Quotient::RoomMessageEvent>(event)->has<Quotient::EventContent::FileContentBase>()) {
|
||||
return {MessageComponent{MessageComponentType::Text, i18n("<i>This event does not have any content.</i>"), {}}};
|
||||
if (string.isEmpty()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
// Strip mx-reply if present.
|
||||
@@ -591,7 +590,7 @@ TextHandler::textComponents(QString string, Qt::TextFormat inputFormat, const Ne
|
||||
string = string.trimmed();
|
||||
|
||||
if (event != nullptr && room != nullptr) {
|
||||
if (auto e = eventCast<const Quotient::RoomMessageEvent>(event); e && e->msgtype() == Quotient::MessageEventType::Emote && components.size() == 1) {
|
||||
if (auto e = eventCast<const Quotient::RoomMessageEvent>(event); e->msgtype() == Quotient::MessageEventType::Emote && components.size() == 1) {
|
||||
if (components[0].type == MessageComponentType::Text) {
|
||||
components[0].content = emoteString(room, event) + components[0].content;
|
||||
} else {
|
||||
|
||||
@@ -158,7 +158,12 @@ Item {
|
||||
}
|
||||
root.Message.timeline.interactive = false;
|
||||
if (!root.mediaInfo.isSticker) {
|
||||
RoomManager.maximizeMedia(root.eventId);
|
||||
// We need to make sure the index is that of the MediaMessageFilterModel.
|
||||
if (root.Message.timeline.model instanceof MessageFilterModel) {
|
||||
RoomManager.maximizeMedia(RoomManager.mediaMessageFilterModel.getRowForSourceItem(root.Message.index));
|
||||
} else {
|
||||
RoomManager.maximizeMedia(root.Message.index);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -385,7 +385,12 @@ Video {
|
||||
onTriggered: {
|
||||
root.Message.timeline.interactive = false;
|
||||
root.pause();
|
||||
RoomManager.maximizeMedia(root.eventId);
|
||||
// We need to make sure the index is that of the MediaMessageFilterModel.
|
||||
if (root.Message.timeline.model instanceof MessageFilterModel) {
|
||||
RoomManager.maximizeMedia(RoomManager.mediaMessageFilterModel.getRowForSourceItem(root.Message.index));
|
||||
} else {
|
||||
RoomManager.maximizeMedia(root.Message.index);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
#include "chatbarcache.h"
|
||||
#include "contentprovider.h"
|
||||
#include "filetype.h"
|
||||
#include "linkpreviewer.h"
|
||||
#include "models/reactionmodel.h"
|
||||
#include "neochatconnection.h"
|
||||
#include "neochatroom.h"
|
||||
@@ -421,8 +422,7 @@ bool MessageContentModel::hasComponentType(MessageComponentType::Type type)
|
||||
!= m_components.cend();
|
||||
}
|
||||
|
||||
void MessageContentModel::forEachComponentOfType(MessageComponentType::Type type,
|
||||
std::function<MessageContentModel::ComponentIt(MessageContentModel::ComponentIt)> function)
|
||||
void MessageContentModel::forEachComponentOfType(MessageComponentType::Type type, std::function<void(const QModelIndex &)> function)
|
||||
{
|
||||
auto it = m_components.begin();
|
||||
while ((it = std::find_if(it,
|
||||
@@ -431,12 +431,12 @@ void MessageContentModel::forEachComponentOfType(MessageComponentType::Type type
|
||||
return component.type == type;
|
||||
}))
|
||||
!= m_components.end()) {
|
||||
it = function(it);
|
||||
function(index(it - m_components.begin()));
|
||||
++it;
|
||||
}
|
||||
}
|
||||
|
||||
void MessageContentModel::forEachComponentOfType(QList<MessageComponentType::Type> types,
|
||||
std::function<MessageContentModel::ComponentIt(MessageContentModel::ComponentIt)> function)
|
||||
void MessageContentModel::forEachComponentOfType(QList<MessageComponentType::Type> types, std::function<void(const QModelIndex &)> function)
|
||||
{
|
||||
for (const auto &type : types) {
|
||||
forEachComponentOfType(type, function);
|
||||
@@ -466,10 +466,6 @@ void MessageContentModel::resetModel()
|
||||
m_components += messageContentComponents();
|
||||
endResetModel();
|
||||
|
||||
if (m_room->urlPreviewEnabled()) {
|
||||
forEachComponentOfType({MessageComponentType::Text, MessageComponentType::Quote}, m_linkPreviewFunction);
|
||||
}
|
||||
|
||||
updateReplyModel();
|
||||
updateReactionModel();
|
||||
}
|
||||
@@ -489,10 +485,6 @@ void MessageContentModel::resetContent(bool isEditing, bool isThreading)
|
||||
m_components += newComponents;
|
||||
endInsertRows();
|
||||
|
||||
if (m_room->urlPreviewEnabled()) {
|
||||
forEachComponentOfType({MessageComponentType::Text, MessageComponentType::Quote}, m_linkPreviewFunction);
|
||||
}
|
||||
|
||||
updateReplyModel();
|
||||
updateReactionModel();
|
||||
}
|
||||
@@ -506,13 +498,27 @@ QList<MessageComponent> MessageContentModel::messageContentComponents(bool isEdi
|
||||
|
||||
QList<MessageComponent> newComponents;
|
||||
|
||||
const auto roomMessageEvent = eventCast<const Quotient::RoomMessageEvent>(event.first);
|
||||
if (roomMessageEvent && roomMessageEvent->rawMsgtype() == u"m.key.verification.request"_s) {
|
||||
newComponents += MessageComponent{MessageComponentType::Verification, QString(), {}};
|
||||
return newComponents;
|
||||
}
|
||||
|
||||
if (event.first->isRedacted()) {
|
||||
newComponents += MessageComponent{MessageComponentType::Text, QString(), {}};
|
||||
return newComponents;
|
||||
}
|
||||
|
||||
if (isEditing) {
|
||||
newComponents += MessageComponent{MessageComponentType::ChatBar, QString(), {}};
|
||||
} else {
|
||||
newComponents.append(componentsForType(MessageComponentType::typeForEvent(*event.first, m_isReply)));
|
||||
newComponents.append(componentsForType(MessageComponentType::typeForEvent(*event.first)));
|
||||
}
|
||||
|
||||
if (m_room->urlPreviewEnabled()) {
|
||||
newComponents = addLinkPreviews(newComponents);
|
||||
}
|
||||
|
||||
const auto roomMessageEvent = eventCast<const Quotient::RoomMessageEvent>(event.first);
|
||||
#if Quotient_VERSION_MINOR > 9 || (Quotient_VERSION_MINOR == 9 && Quotient_VERSION_PATCH > 1)
|
||||
if (m_threadsEnabled && roomMessageEvent && (roomMessageEvent->isThreaded() || m_room->threads().contains(roomMessageEvent->id()))
|
||||
&& roomMessageEvent->id() == roomMessageEvent->threadRootEventId()) {
|
||||
@@ -591,26 +597,22 @@ QList<MessageComponent> MessageContentModel::componentsForType(MessageComponentT
|
||||
}
|
||||
|
||||
switch (type) {
|
||||
case MessageComponentType::Verification: {
|
||||
return {MessageComponent{MessageComponentType::Verification, QString(), {}}};
|
||||
}
|
||||
case MessageComponentType::Text: {
|
||||
if (const auto roomMessageEvent = eventCast<const Quotient::RoomMessageEvent>(event.first)) {
|
||||
return TextHandler().textComponents(EventHandler::rawMessageBody(*roomMessageEvent),
|
||||
EventHandler::messageBodyInputFormat(*roomMessageEvent),
|
||||
m_room,
|
||||
roomMessageEvent,
|
||||
roomMessageEvent->isReplaced());
|
||||
} else {
|
||||
return TextHandler().textComponents(EventHandler::plainBody(m_room, event.first), Qt::TextFormat::PlainText, m_room, event.first, false);
|
||||
}
|
||||
|
||||
const auto roomMessageEvent = eventCast<const Quotient::RoomMessageEvent>(event.first);
|
||||
return TextHandler().textComponents(EventHandler::rawMessageBody(*roomMessageEvent),
|
||||
EventHandler::messageBodyInputFormat(*roomMessageEvent),
|
||||
m_room,
|
||||
roomMessageEvent,
|
||||
roomMessageEvent->isReplaced());
|
||||
auto body = EventHandler::rawMessageBody(*roomMessageEvent);
|
||||
if (body.trimmed().isEmpty()) {
|
||||
return TextHandler().textComponents(i18n("<i>This event does not have any content.</i>"),
|
||||
Qt::TextFormat::RichText,
|
||||
m_room,
|
||||
roomMessageEvent,
|
||||
roomMessageEvent->isReplaced());
|
||||
} else {
|
||||
return TextHandler().textComponents(body,
|
||||
EventHandler::messageBodyInputFormat(*roomMessageEvent),
|
||||
m_room,
|
||||
roomMessageEvent,
|
||||
roomMessageEvent->isReplaced());
|
||||
}
|
||||
}
|
||||
case MessageComponentType::File: {
|
||||
QList<MessageComponent> components;
|
||||
@@ -701,20 +703,42 @@ MessageComponent MessageContentModel::linkPreviewComponent(const QUrl &link)
|
||||
}
|
||||
if (linkPreviewer->loaded()) {
|
||||
return MessageComponent{MessageComponentType::LinkPreview, QString(), {{"link"_L1, link}}};
|
||||
}
|
||||
connect(linkPreviewer, &LinkPreviewer::loadedChanged, this, [this, link]() {
|
||||
const auto linkPreviewer = dynamic_cast<NeoChatConnection *>(m_room->connection())->previewerForLink(link);
|
||||
if (linkPreviewer != nullptr && linkPreviewer->loaded()) {
|
||||
forEachComponentOfType(MessageComponentType::LinkPreviewLoad, [this, link](ComponentIt it) {
|
||||
if (it->attributes["link"_L1].toUrl() == link) {
|
||||
it->type = MessageComponentType::LinkPreview;
|
||||
Q_EMIT dataChanged(index(it - m_components.begin()), index(it - m_components.begin()), {ComponentTypeRole});
|
||||
} else {
|
||||
connect(linkPreviewer, &LinkPreviewer::loadedChanged, this, [this, link]() {
|
||||
const auto linkPreviewer = dynamic_cast<NeoChatConnection *>(m_room->connection())->previewerForLink(link);
|
||||
if (linkPreviewer != nullptr && linkPreviewer->loaded()) {
|
||||
for (auto it = m_components.begin(); it != m_components.end(); it++) {
|
||||
if (it->attributes["link"_L1].toUrl() == link) {
|
||||
it->type = MessageComponentType::LinkPreview;
|
||||
Q_EMIT dataChanged(index(it - m_components.begin()), index(it - m_components.begin()), {ComponentTypeRole});
|
||||
}
|
||||
}
|
||||
return it;
|
||||
});
|
||||
}
|
||||
});
|
||||
return MessageComponent{MessageComponentType::LinkPreviewLoad, QString(), {{"link"_L1, link}}};
|
||||
}
|
||||
}
|
||||
|
||||
QList<MessageComponent> MessageContentModel::addLinkPreviews(QList<MessageComponent> inputComponents)
|
||||
{
|
||||
int i = 0;
|
||||
while (i < inputComponents.size()) {
|
||||
const auto component = inputComponents.at(i);
|
||||
if (component.type == MessageComponentType::Text || component.type == MessageComponentType::Quote) {
|
||||
if (LinkPreviewer::hasPreviewableLinks(component.content)) {
|
||||
const auto links = LinkPreviewer::linkPreviews(component.content);
|
||||
for (qsizetype j = 0; j < links.size(); ++j) {
|
||||
const auto linkPreview = linkPreviewComponent(links[j]);
|
||||
if (!m_removedLinkPreviews.contains(links[j]) && !linkPreview.isEmpty()) {
|
||||
inputComponents.insert(i + j + 1, linkPreview);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
});
|
||||
return MessageComponent{MessageComponentType::LinkPreviewLoad, QString(), {{"link"_L1, link}}};
|
||||
i++;
|
||||
}
|
||||
|
||||
return inputComponents;
|
||||
}
|
||||
|
||||
void MessageContentModel::closeLinkPreview(int row)
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
#include <Quotient/events/roomevent.h>
|
||||
|
||||
#include "enums/messagecomponenttype.h"
|
||||
#include "linkpreviewer.h"
|
||||
#include "messagecomponent.h"
|
||||
#include "models/itinerarymodel.h"
|
||||
#include "models/reactionmodel.h"
|
||||
@@ -134,32 +133,13 @@ private:
|
||||
void initializeEvent();
|
||||
void getEvent();
|
||||
|
||||
using ComponentIt = QList<MessageComponent>::iterator;
|
||||
|
||||
QList<MessageComponent> m_components;
|
||||
bool hasComponentType(MessageComponentType::Type type);
|
||||
void forEachComponentOfType(MessageComponentType::Type type, std::function<ComponentIt(ComponentIt)> function);
|
||||
void forEachComponentOfType(QList<MessageComponentType::Type> types, std::function<ComponentIt(ComponentIt)> function);
|
||||
void forEachComponentOfType(MessageComponentType::Type type, std::function<void(const QModelIndex &)> function);
|
||||
void forEachComponentOfType(QList<MessageComponentType::Type> types, std::function<void(const QModelIndex &)> function);
|
||||
|
||||
std::function<ComponentIt(const ComponentIt &)> m_fileInfoFunction = [this](ComponentIt it) {
|
||||
Q_EMIT dataChanged(index(it - m_components.begin()), index(it - m_components.begin()), {MessageContentModel::FileTransferInfoRole});
|
||||
return ++it;
|
||||
};
|
||||
std::function<ComponentIt(const ComponentIt &)> m_linkPreviewFunction = [this](ComponentIt it) {
|
||||
bool previewAdded = false;
|
||||
if (LinkPreviewer::hasPreviewableLinks(it->content)) {
|
||||
const auto links = LinkPreviewer::linkPreviews(it->content);
|
||||
for (qsizetype j = 0; j < links.size(); ++j) {
|
||||
const auto linkPreview = linkPreviewComponent(links[j]);
|
||||
if (!m_removedLinkPreviews.contains(links[j]) && !linkPreview.isEmpty()) {
|
||||
beginInsertRows({}, std::distance(m_components.begin(), it) + j + 1, std::distance(m_components.begin(), it) + j + 1);
|
||||
it = m_components.insert(it + j + 1, linkPreview);
|
||||
previewAdded = true;
|
||||
endInsertRows();
|
||||
}
|
||||
};
|
||||
}
|
||||
return previewAdded ? it : ++it;
|
||||
std::function<void(const QModelIndex &)> m_fileInfoFunction = [this](const QModelIndex &index) {
|
||||
Q_EMIT dataChanged(index, index, {MessageContentModel::FileTransferInfoRole});
|
||||
};
|
||||
|
||||
void resetModel();
|
||||
@@ -174,6 +154,7 @@ private:
|
||||
|
||||
QList<MessageComponent> componentsForType(MessageComponentType::Type type);
|
||||
MessageComponent linkPreviewComponent(const QUrl &link);
|
||||
QList<MessageComponent> addLinkPreviews(QList<MessageComponent> inputComponents);
|
||||
|
||||
QList<QUrl> m_removedLinkPreviews;
|
||||
|
||||
|
||||
@@ -153,7 +153,7 @@ QQC2.ScrollView {
|
||||
Delegates.RoundedItemDelegate {
|
||||
id: leaveButton
|
||||
icon.name: "arrow-left-symbolic"
|
||||
text: root.room.isSpace ? i18nc("@action:button", "Leave this space…") : i18nc("@action:button", "Leave this room…")
|
||||
text: root.room.isSpace ? i18nc("@action:button", "Leave this space") : i18nc("@action:button", "Leave this room")
|
||||
activeFocusOnTab: true
|
||||
|
||||
Layout.fillWidth: true
|
||||
|
||||
@@ -58,7 +58,7 @@ KirigamiComponents.ConvergentContextMenu {
|
||||
icon.name: "notifications"
|
||||
|
||||
Kirigami.Action {
|
||||
text: i18nc("@action:inmenu Notification 'Default Settings'", "Default Settings")
|
||||
text: i18n("Follow Global Setting")
|
||||
icon.name: "globe"
|
||||
checkable: true
|
||||
autoExclusive: true
|
||||
@@ -152,7 +152,7 @@ KirigamiComponents.ConvergentContextMenu {
|
||||
}
|
||||
|
||||
QQC2.Action {
|
||||
text: i18n("Leave Room…")
|
||||
text: i18n("Leave Room")
|
||||
icon.name: "go-previous"
|
||||
onTriggered: {
|
||||
Qt.createComponent('org.kde.neochat', 'ConfirmLeaveDialog').createObject(root.QQC2.ApplicationWindow.window, {
|
||||
|
||||
@@ -70,10 +70,8 @@ KirigamiComponents.ConvergentContextMenu {
|
||||
}
|
||||
|
||||
QQC2.Action {
|
||||
text: i18nc("'Space' is a matrix space", "Leave Space…")
|
||||
text: i18nc("'Space' is a matrix space", "Leave Space")
|
||||
icon.name: "go-previous"
|
||||
onTriggered: Qt.createComponent('org.kde.neochat', 'ConfirmLeaveDialog').createObject(root.QQC2.ApplicationWindow.window, {
|
||||
room: root.room
|
||||
}).open();
|
||||
onTriggered: root.room.forget()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -115,6 +115,39 @@ FormCard.FormCardPage {
|
||||
text: root.connection ? root.connection.label : ""
|
||||
}
|
||||
FormCard.FormDelegateSeparator {}
|
||||
FormCard.FormComboBoxDelegate {
|
||||
id: timezoneLabel
|
||||
|
||||
property string textValue: root.connection ? root.connection.timezone : ""
|
||||
|
||||
visible: root.connection.supportsProfileFields
|
||||
enabled: root.connection.canChangeProfileFields && root.connection.profileFieldAllowed("us.cloke.msc4175.tz")
|
||||
text: i18nc("@label:combobox", "Timezone:")
|
||||
model: TimeZoneModel {}
|
||||
textRole: "display"
|
||||
valueRole: "display"
|
||||
onActivated: index => {
|
||||
// "Prefer not to say" choice clears it.
|
||||
if (index === 0) {
|
||||
textValue = "";
|
||||
return;
|
||||
}
|
||||
|
||||
// Otherwise, set it to the text value which is the IANA identifier
|
||||
textValue = timezoneLabel.currentValue;
|
||||
}
|
||||
Component.onCompleted: currentIndex = model.indexOfValue(textValue)
|
||||
}
|
||||
FormCard.FormDelegateSeparator {}
|
||||
FormCard.FormTextFieldDelegate {
|
||||
id: pronounsLabel
|
||||
visible: root.connection.supportsProfileFields
|
||||
enabled: root.connection.canChangeProfileFields && root.connection.profileFieldAllowed("io.fsky.nyx.pronouns")
|
||||
label: i18nc("@label:textbox", "Pronouns:")
|
||||
placeholderText: i18nc("@placeholder", "she/her")
|
||||
text: root.connection ? root.connection.pronouns : ""
|
||||
}
|
||||
FormCard.FormDelegateSeparator {}
|
||||
FormCard.FormButtonDelegate {
|
||||
text: i18nc("@action:button", "Show QR Code")
|
||||
icon.name: "view-barcode-qr-symbolic"
|
||||
@@ -146,6 +179,12 @@ FormCard.FormCardPage {
|
||||
if (root.connection.label !== accountLabel.text) {
|
||||
root.connection.label = accountLabel.text;
|
||||
}
|
||||
if (root.connection.timezone !== timezoneLabel.textValue) {
|
||||
root.connection.timezone = timezoneLabel.textValue;
|
||||
}
|
||||
if (root.connection.pronouns !== pronounsLabel.text) {
|
||||
root.connection.pronouns = pronounsLabel.text;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -252,7 +291,7 @@ FormCard.FormCardPage {
|
||||
FormCard.FormCard {
|
||||
FormCard.FormButtonDelegate {
|
||||
id: deactivateAccountButton
|
||||
text: i18nc("@action:button", "Deactivate Account…")
|
||||
text: i18n("Deactivate Account")
|
||||
icon.name: "trash-empty-symbolic"
|
||||
onClicked: {
|
||||
const component = Qt.createComponent('org.kde.neochat', 'ConfirmDeactivateAccountDialog');
|
||||
|
||||
@@ -85,7 +85,7 @@ FormCard.FormCardPage {
|
||||
}
|
||||
|
||||
QQC2.ToolButton {
|
||||
text: i18n("Logout…")
|
||||
text: i18n("Logout")
|
||||
icon.name: "im-kick-user"
|
||||
onClicked: confirmLogoutDialogComponent.createObject(root.QQC2.Overlay.overlay).open()
|
||||
}
|
||||
|
||||
@@ -45,26 +45,6 @@ FormCard.FormCardPage {
|
||||
}
|
||||
}
|
||||
|
||||
FormCard.FormCard {
|
||||
Layout.topMargin: Kirigami.Units.largeSpacing
|
||||
FormCard.AbstractFormDelegate {
|
||||
contentItem: RowLayout {
|
||||
spacing: Kirigami.Units.largeSpacing
|
||||
Kirigami.Icon {
|
||||
source: "data-information"
|
||||
width: Kirigami.Units.iconSizes.sizeForLabels
|
||||
height: Kirigami.Units.iconSizes.sizeForLabels
|
||||
}
|
||||
QQC2.Label {
|
||||
text: i18nc("@info", "These are the default notification settings for all rooms. You can customize notifications per-room in the room list or room settings.")
|
||||
wrapMode: Text.WordWrap
|
||||
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
FormCard.FormHeader {
|
||||
title: i18nc("@title:group", "Room Notifications")
|
||||
}
|
||||
|
||||
@@ -345,7 +345,7 @@ FormCard.FormCardPage {
|
||||
FormCard.FormCard {
|
||||
FormCard.FormButtonDelegate {
|
||||
icon.name: "kt-restore-defaults-symbolic"
|
||||
text: i18nc("@action:button", "Reset all configuration values to their default…")
|
||||
text: i18nc("@action:button", "Reset all configuration values to their default")
|
||||
onClicked: resetDialog.open()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ FormCard.FormCardPage {
|
||||
|
||||
FormCard.FormCard {
|
||||
FormCard.FormRadioDelegate {
|
||||
text: i18nc("As in the default notification setting", "Default Settings")
|
||||
text: i18n("Follow global setting")
|
||||
checked: room.pushNotificationState === PushNotificationState.Default
|
||||
enabled: room.pushNotificationState !== PushNotificationState.Unknown
|
||||
onToggled: {
|
||||
|
||||
@@ -25,34 +25,13 @@ FormCard.FormCardPage {
|
||||
title: i18nc("@option:check", "Encryption")
|
||||
}
|
||||
FormCard.FormCard {
|
||||
FormCard.AbstractFormDelegate {
|
||||
visible: room.usesEncryption
|
||||
|
||||
contentItem: RowLayout {
|
||||
spacing: Kirigami.Units.largeSpacing
|
||||
Kirigami.Icon {
|
||||
source: "lock"
|
||||
width: Kirigami.Units.iconSizes.sizeForLabels
|
||||
height: Kirigami.Units.iconSizes.sizeForLabels
|
||||
}
|
||||
QQC2.Label {
|
||||
text: i18nc("@info", "This room uses encryption.")
|
||||
wrapMode: Text.WordWrap
|
||||
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
}
|
||||
}
|
||||
FormCard.FormButtonDelegate {
|
||||
FormCard.FormSwitchDelegate {
|
||||
id: enableEncryptionSwitch
|
||||
|
||||
icon.name: "lock-symbolic"
|
||||
text: i18nc("@action:button Enable encryption in this room", "Enable Encryption…")
|
||||
description: i18nc("@info:description", "Once enabled, encryption cannot be disabled.")
|
||||
text: i18n("Enable encryption")
|
||||
description: i18nc("option:check", "Once enabled, encryption cannot be disabled.")
|
||||
enabled: room.canEncryptRoom
|
||||
visible: !room.usesEncryption
|
||||
|
||||
onClicked: {
|
||||
checked: room.usesEncryption
|
||||
onToggled: if (checked) {
|
||||
let dialog = confirmEncryptionDialog.createObject(QQC2.Overlay.overlay, {
|
||||
room: room
|
||||
});
|
||||
|
||||
@@ -95,11 +95,9 @@ ColumnLayout {
|
||||
}
|
||||
}
|
||||
QQC2.Button {
|
||||
text: i18nc("@action:button", "Leave this space…")
|
||||
text: i18nc("@action:button", "Leave this space")
|
||||
icon.name: "go-previous"
|
||||
onClicked: Qt.createComponent('org.kde.neochat', 'ConfirmLeaveDialog').createObject(root.QQC2.ApplicationWindow.window, {
|
||||
room: root.room
|
||||
}).open();
|
||||
onClicked: root.room.forget()
|
||||
}
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
|
||||
@@ -123,7 +123,7 @@ KirigamiComponents.ConvergentContextMenu {
|
||||
|
||||
|
||||
component ReportMessageAction: Kirigami.Action {
|
||||
text: i18nc("@action:button 'Report' as in 'Report this event to the administrators'", "Report…")
|
||||
text: i18nc("@action:button 'Report' as in 'Report this event to the administrators'", "Report")
|
||||
icon.name: "dialog-warning-symbolic"
|
||||
visible: !author.isLocalMember
|
||||
onTriggered: {
|
||||
|
||||
@@ -159,7 +159,7 @@ QQC2.ScrollView {
|
||||
|
||||
function onReadMarkerAdded() {
|
||||
if (root.markReadCondition == LibNeoChat.TimelineMarkReadCondition.EntryVisible && messageListView.allUnreadVisible()) {
|
||||
_private.room.markAllMessagesAsRead();
|
||||
root.room.markAllMessagesAsRead();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -85,14 +85,9 @@ QHash<int, QByteArray> MediaMessageFilterModel::roleNames() const
|
||||
return roles;
|
||||
}
|
||||
|
||||
int MediaMessageFilterModel::getRowForEventId(const QString &eventId) const
|
||||
int MediaMessageFilterModel::getRowForSourceItem(int sourceRow) const
|
||||
{
|
||||
for (auto i = 0; i < rowCount(); i++) {
|
||||
if (data(index(i, 0), MessageModel::EventIdRole).toString() == eventId) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
return mapFromSource(sourceModel()->index(sourceRow, 0)).row();
|
||||
}
|
||||
|
||||
#include "moc_mediamessagefiltermodel.cpp"
|
||||
|
||||
@@ -63,5 +63,5 @@ public:
|
||||
*/
|
||||
[[nodiscard]] QHash<int, QByteArray> roleNames() const override;
|
||||
|
||||
int getRowForEventId(const QString &eventId) const;
|
||||
Q_INVOKABLE int getRowForSourceItem(int sourceRow) const;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user