Compare commits

..

36 Commits

Author SHA1 Message Date
Tobias Fella
371be1511f Don't show erroneous "This event has no content" text for files" 2025-07-27 14:53:54 +02:00
l10n daemon script
04472dae4f GIT_SILENT Sync po/docbooks with svn 2025-07-27 01:40:53 +00:00
Tobias Fella
aa40fc84ea Adapt to power level changes in room version 12 2025-07-26 22:19:07 +00:00
Tobias Fella
24e43d063a Fix test 2025-07-27 00:18:37 +02:00
l10n daemon script
c5caffcdf9 GIT_SILENT Sync po/docbooks with svn 2025-07-25 01:40:12 +00:00
l10n daemon script
95d334ad86 GIT_SILENT Sync po/docbooks with svn 2025-07-23 01:40:50 +00:00
Tobias Fella
602ac5c55f Fix opening EditStateDialog 2025-07-22 21:02:51 +02:00
l10n daemon script
247423bf83 GIT_SILENT Sync po/docbooks with svn 2025-07-22 01:42:47 +00:00
l10n daemon script
24d35b3eae GIT_SILENT Sync po/docbooks with svn 2025-07-21 01:41:16 +00:00
l10n daemon script
8bcd9f7469 GIT_SILENT Sync po/docbooks with svn 2025-07-20 01:41:17 +00:00
Tobias Fella
edf5d55da4 Prepare for new RoomId format
See MSC4291
2025-07-19 13:45:23 +00:00
l10n daemon script
976af783e2 GIT_SILENT Sync po/docbooks with svn 2025-07-19 06:16:11 +00:00
l10n daemon script
d87954838e GIT_SILENT Sync po/docbooks with svn 2025-07-18 01:38:52 +00:00
Joshua Goins
e757331dce Fix reference to Security & Safety settings on the invite screen
It's now called "Security & Safety", not just "Security". I also added a
note for translators to ensure they keep their strings consistent here.
2025-07-17 19:02:37 -04:00
l10n daemon script
bf4f6f5728 GIT_SILENT Sync po/docbooks with svn 2025-07-17 01:38:43 +00:00
Joshua Goins
c73bc8fc29 Redesign the enable encryption room setting
Clients like Element and ours show the room encryption mode as a toggle,
which in my opinion doesn't make sense. It's an irreversible operation,
so it should be a button!

When encryption is already used in the room, the button turns into a
non-interactive card.
2025-07-16 18:23:58 -04:00
Joshua Goins
211a08db68 Add ellipses to various settings actions that have confirm dialogs 2025-07-16 18:23:46 -04:00
Joshua Goins
38987e6d4c Add ellipses to the Report message action
This has a dialog to enter a message associated with the report, so as
suggested by the HIG it should have ellipses.
2025-07-16 18:23:46 -04:00
Joshua Goins
9d76e7e30b Show ellipses for leaving rooms and space actions, and always confirm
The HIG suggests using ellipses for actions that have a confirmation,
and leaving a space or room is one such cases. Otherwise, the user has
no idea if leaving is an immediate, irreversible action.

It turns out there *was* some cases where pressing this button
(especially for spaces) would actually do it without confirmation, which
is now fixed.
2025-07-16 18:23:46 -04:00
Joshua Goins
4c1a8d3657 Show the user's id or a room's canonical alias (if set) in invites
This should prevent the easiest way of masquerading, and provide an \
important identifier for rooms. Note that *only* the room canonical
alias is shown, if it's not set then it's just the display name. This is
intentional as regular users rarely interact with room IDs, but they can
still check it elsewhere in NeoChat.
2025-07-16 18:08:36 -04:00
James Graham
7a5de25885 Further refactor MessageContentModel
Further refactor MessageContentModel. Move away from special casing certian MessageContentTypes. Use forEachComponentOfType more
2025-07-16 18:27:03 +01:00
l10n daemon script
a17aa2c6fa GIT_SILENT Sync po/docbooks with svn 2025-07-16 01:56:47 +00:00
Tobias Fella
207a7876b6 Improve visualization of replies to non-message events 2025-07-15 19:55:35 +00:00
Tobias Fella
4c638a740e Set object ownership for NeoChatRoomMembers 2025-07-15 19:54:24 +00:00
Joshua Goins
0ee89e1b2b Show unable to decrypt events in the room list
These were previously (unintentionally) filtered, but I wanted to add
them because without showing and caching them my DMs look extremely out
of order. And assuming that you get an unexpected "unable to decrypt"
event, you would want that room to "shoot to the top" anyway.
2025-07-15 19:14:54 +00:00
Joshua Goins
4af42a57f4 Fix undefined QML reference in TimelineView
markAllMessagesAsRead() has moved it seems, and this specific case
wasn't changed.
2025-07-15 19:14:41 +00:00
Joshua Goins
34f2c2dabc Stop a room's lastActiveTime from changing because of hidden events
This was just a missed oversight during the refactoring, we need to pass
the hidden filter into NeoChatRoom::lastEvent so it doesn't pick up on
hidden events and push rooms to the top for no discernible reason.

Also, I simplified the function to take out the cached event handling,
because lastEvent is already doing that. The function is const now, too
and has some nicer comments.
2025-07-15 14:44:23 -04:00
l10n daemon script
9ff942915a GIT_SILENT Sync po/docbooks with svn 2025-07-15 01:44:46 +00:00
Tobias Fella
10123abc5b Fix maximizing replied-to media
The previous index-based handling opened the wrong media, as it used the wrong index.
2025-07-14 20:16:34 +00:00
l10n daemon script
ad993d4340 GIT_SILENT Sync po/docbooks with svn 2025-07-14 01:49:19 +00:00
l10n daemon script
ddc0a66d5b GIT_SILENT Sync po/docbooks with svn 2025-07-13 01:40:25 +00:00
l10n daemon script
e8981bdc0f GIT_SILENT Sync po/docbooks with svn 2025-07-12 01:42:05 +00:00
l10n daemon script
c42486a061 GIT_SILENT Sync po/docbooks with svn 2025-07-11 01:40:27 +00:00
l10n daemon script
64d82b8d2a GIT_SILENT Sync po/docbooks with svn 2025-07-10 01:40:11 +00:00
l10n daemon script
677abee890 GIT_SILENT Sync po/docbooks with svn 2025-07-09 01:42:04 +00:00
Joshua Goins
3a25a62350 Refer to global notification settings as "Default Settings" instead
This changes references to "global notification settings" to "default
settings", which should hopefully make it clearer what these actually
are.

Also, a information tip has been added to the settings page to clarify
what these settings do and imply that the per-room settings take
precedence.
2025-07-08 17:17:04 -04:00
86 changed files with 13551 additions and 11427 deletions

View File

@@ -130,7 +130,8 @@ 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::UTC)).toString(u"hh:mm"_s));
QCOMPARE(EventHandler::timeString(room, event, u"hh:mm"_s),
QDateTime::fromMSecsSinceEpoch(1432735824654, QTimeZone(QTimeZone::LocalTime)).toString(u"hh:mm"_s));
const auto txID = room->postJson("m.room.message"_L1, event->fullJson());
QCOMPARE(room->pendingEvents().size(), 1);

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -106,7 +106,7 @@ KirigamiComponents.ConvergentContextMenu {
}
QQC2.Action {
text: i18n("Logout")
text: i18n("Logout")
icon.name: "im-kick-user"
onTriggered: confirmLogoutDialogComponent.createObject(root).open()
}

View File

@@ -52,6 +52,15 @@ 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
@@ -70,7 +79,14 @@ ColumnLayout {
spacing: Kirigami.Units.smallSpacing
Kirigami.Heading {
text: root.currentRoom.displayName
text: root.invitingMember.displayName
Layout.alignment: Qt.AlignHCenter
}
QQC2.Label {
text: root.invitingMember.id
color: Kirigami.Theme.disabledTextColor
Layout.alignment: Qt.AlignHCenter
}
@@ -159,7 +175,7 @@ ColumnLayout {
QQC2.Label {
color: Kirigami.Theme.disabledTextColor
text: i18nc("@info:label", "You can reject invitations from unknown users under Security settings.")
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.")
wrapMode: Text.WordWrap
// + 5 to prevent it from wrapping unnecessarily

View File

@@ -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.qml"), {
onTriggered: pageStack.pushDialogLayer(Qt.createComponent("org.kde.neochat", "EditStateDialog"), {
room: root.room,
type: root.type,
stateKey: root.stateKey,

View File

@@ -23,11 +23,6 @@ Kirigami.Dialog {
property NeoChatConnection connection
readonly property ProfileFieldsHelper profileFieldsHelper: ProfileFieldsHelper {
connection: root.connection
userId: root.user.id
}
leftPadding: 0
rightPadding: 0
topPadding: 0
@@ -131,38 +126,14 @@ Kirigami.Dialog {
}
}
RowLayout {
spacing: Kirigami.Units.smallSpacing
Kirigami.Chip {
visible: root.room
text: root.room ? QmlUtils.nameForPowerLevelValue(root.room.memberEffectivePowerLevel(root.user.id)) : ""
closable: false
checkable: false
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 {

View File

@@ -236,11 +236,18 @@ void RoomManager::resolveResource(Uri uri, const QString &action)
}
}
void RoomManager::maximizeMedia(int index)
void RoomManager::maximizeMedia(const QString &eventId)
{
if (index < -1 || index > m_mediaMessageFilterModel->rowCount()) {
if (eventId.isEmpty()) {
qWarning() << "Tried to open media for empty event id";
return;
}
const auto index = m_mediaMessageFilterModel->getRowForEventId(eventId);
if (index == -1) {
return;
}
Q_EMIT showMaximizedMedia(index);
}
@@ -397,7 +404,9 @@ 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.
if (vias.empty()) {
// 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)) {
vias.append(roomAliasOrId.mid(roomAliasOrId.lastIndexOf(':'_L1) + 1));
}

View File

@@ -212,12 +212,8 @@ 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(int index);
Q_INVOKABLE void maximizeMedia(const QString &eventId);
Q_INVOKABLE void maximizeCode(NeochatRoomMember *author, const QDateTime &time, const QString &codeText, const QString &language);

View File

@@ -22,7 +22,6 @@ target_sources(LibNeoChat PRIVATE
texthandler.cpp
urlhelper.cpp
utils.cpp
profilefieldshelper.cpp
enums/messagecomponenttype.h
enums/messagetype.h
enums/powerlevel.cpp
@@ -33,7 +32,6 @@ 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
@@ -46,8 +44,6 @@ 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

View File

@@ -70,13 +70,23 @@ 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)
static Type typeForEvent(const Quotient::RoomEvent &event, bool isInReply = false)
{
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;
@@ -103,7 +113,8 @@ public:
if (event.matrixType() == u"org.matrix.msc3672.beacon_info"_s) {
return MessageComponentType::LiveLocation;
}
return MessageComponentType::Other;
// 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;
}
if (is<const EncryptedEvent>(event)) {
return MessageComponentType::Encrypted;
@@ -116,7 +127,8 @@ public:
return MessageComponentType::Poll;
}
return MessageComponentType::Other;
// 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;
}
/**

View File

@@ -448,6 +448,12 @@ 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"));
}

View File

@@ -1,20 +0,0 @@
// 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});
}

View File

@@ -1,49 +0,0 @@
// 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);
};

View File

@@ -31,13 +31,7 @@ auto leaveRoomLambda = [](const QString &text, NeoChatRoom *room, ChatBarCache *
Q_EMIT room->showMessage(MessageType::Information, i18n("Leaving this room."));
room->forget();
} else {
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();
}
// FIXME: re-add sanity check for roomId/alias
auto leaving = dynamic_cast<NeoChatRoom *>(room->connection()->room(text));
if (!leaving) {
leaving = dynamic_cast<NeoChatRoom *>(room->connection()->roomByAlias(text));
@@ -217,13 +211,7 @@ QList<ActionsModel::Action> actions{
Action{
u"join"_s,
[](const QString &text, NeoChatRoom *room, ChatBarCache *) {
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();
}
// FIXME: re-add sanity check for roomId/alias
auto targetRoom = text.startsWith(QLatin1Char('!')) ? room->connection()->room(text) : room->connection()->roomByAlias(text);
if (targetRoom) {
ActionsModel::instance().resolveResource(targetRoom->id());
@@ -242,25 +230,18 @@ QList<ActionsModel::Action> actions{
[](const QString &text, NeoChatRoom *room, ChatBarCache *) {
auto parts = text.split(u" "_s);
QString roomName = parts[0];
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) {
// FIXME: re-add sanity check for roomId/alias
if (const auto targetRoom = text.startsWith(QLatin1Char('!')) ? room->connection()->room(text) : room->connection()->roomByAlias(text)) {
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.mid(roomName.indexOf(":"_L1) + 1);
const auto knownServer = roomName.contains(":"_L1) ? QStringList{roomName.mid(roomName.indexOf(":"_L1) + 1)} : QStringList();
if (parts.length() >= 2) {
ActionsModel::instance().knockRoom(connection, roomName, parts[1], QStringList{knownServer});
ActionsModel::instance().knockRoom(connection, roomName, parts[1], knownServer);
} else {
ActionsModel::instance().knockRoom(connection, roomName, QString(), QStringList{knownServer});
ActionsModel::instance().knockRoom(connection, roomName, QString(), knownServer);
}
return QString();
},
@@ -271,13 +252,7 @@ QList<ActionsModel::Action> actions{
Action{
u"j"_s,
[](const QString &text, NeoChatRoom *room, ChatBarCache *) {
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();
}
// FIXME: re-add sanity check for roomId/alias
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();

View File

@@ -1,48 +0,0 @@
// 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"

View File

@@ -1,24 +0,0 @@
// 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;
};

View File

@@ -100,6 +100,10 @@ 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.

View File

@@ -6,7 +6,6 @@
#include <QImageReader>
#include <QJsonDocument>
#include "jobs/neochatprofilefieldjobs.h"
#include "neochatroom.h"
#include "spacehierarchycache.h"
@@ -140,19 +139,6 @@ 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);
@@ -572,33 +558,4 @@ 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"

View File

@@ -90,21 +90,6 @@ 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.
@@ -219,14 +204,6 @@ 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:
@@ -244,9 +221,6 @@ 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.
@@ -276,7 +250,4 @@ private:
bool m_canCheckMutualRooms = false;
bool m_canEraseData = false;
bool m_pushNotificationsEnabled = false;
bool m_supportsProfileFields = false;
QString m_timezone;
QString m_pronouns;
};

View File

@@ -359,9 +359,14 @@ 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) {
@@ -441,20 +446,19 @@ void NeoChatRoom::onRedaction(const RoomEvent &prevEvent, const RoomEvent & /*af
}
}
QDateTime NeoChatRoom::lastActiveTime()
QDateTime NeoChatRoom::lastActiveTime() const
{
if (timelineSize() == 0) {
if (m_cachedEvent != nullptr) {
return m_cachedEvent->originTimestamp();
}
return QDateTime();
}
if (auto event = lastEvent()) {
// Find the last relevant event:
if (const auto event = lastEvent(m_hiddenFilter)) {
return event->originTimestamp();
}
// no message found, take last event
// If nothing is loaded yet, and there is no cached event:
if (timelineSize() == 0) {
return {};
}
// No message found, take last event:
return messageEvents().rbegin()->get()->originTimestamp();
}
@@ -532,6 +536,9 @@ 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;
@@ -544,6 +551,9 @@ 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;
@@ -1670,8 +1680,14 @@ 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)) {
return m_memberObjects.emplace(memberId, std::make_unique<NeochatRoomMember>(this, memberId)).first->second.get();
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[memberId].get();
@@ -1731,4 +1747,20 @@ 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"

View File

@@ -208,7 +208,7 @@ public:
bool visible() const;
void setVisible(bool visible);
[[nodiscard]] QDateTime lastActiveTime();
[[nodiscard]] QDateTime lastActiveTime() const;
/**
* @brief Get the last interesting event.
@@ -589,6 +589,18 @@ 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;

View File

@@ -1,110 +0,0 @@
// 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();
}

View File

@@ -1,79 +0,0 @@
// 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;
};

View File

@@ -570,8 +570,9 @@ 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.isEmpty()) {
return {};
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>"), {}}};
}
// Strip mx-reply if present.
@@ -590,7 +591,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->msgtype() == Quotient::MessageEventType::Emote && components.size() == 1) {
if (auto e = eventCast<const Quotient::RoomMessageEvent>(event); e && e->msgtype() == Quotient::MessageEventType::Emote && components.size() == 1) {
if (components[0].type == MessageComponentType::Text) {
components[0].content = emoteString(room, event) + components[0].content;
} else {

View File

@@ -158,12 +158,7 @@ Item {
}
root.Message.timeline.interactive = false;
if (!root.mediaInfo.isSticker) {
// 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);
}
RoomManager.maximizeMedia(root.eventId);
}
}
}

View File

@@ -385,12 +385,7 @@ Video {
onTriggered: {
root.Message.timeline.interactive = false;
root.pause();
// 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);
}
RoomManager.maximizeMedia(root.eventId);
}
}
}

View File

@@ -29,7 +29,6 @@
#include "chatbarcache.h"
#include "contentprovider.h"
#include "filetype.h"
#include "linkpreviewer.h"
#include "models/reactionmodel.h"
#include "neochatconnection.h"
#include "neochatroom.h"
@@ -422,7 +421,8 @@ bool MessageContentModel::hasComponentType(MessageComponentType::Type type)
!= m_components.cend();
}
void MessageContentModel::forEachComponentOfType(MessageComponentType::Type type, std::function<void(const QModelIndex &)> function)
void MessageContentModel::forEachComponentOfType(MessageComponentType::Type type,
std::function<MessageContentModel::ComponentIt(MessageContentModel::ComponentIt)> 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()) {
function(index(it - m_components.begin()));
++it;
it = function(it);
}
}
void MessageContentModel::forEachComponentOfType(QList<MessageComponentType::Type> types, std::function<void(const QModelIndex &)> function)
void MessageContentModel::forEachComponentOfType(QList<MessageComponentType::Type> types,
std::function<MessageContentModel::ComponentIt(MessageContentModel::ComponentIt)> function)
{
for (const auto &type : types) {
forEachComponentOfType(type, function);
@@ -466,6 +466,10 @@ void MessageContentModel::resetModel()
m_components += messageContentComponents();
endResetModel();
if (m_room->urlPreviewEnabled()) {
forEachComponentOfType({MessageComponentType::Text, MessageComponentType::Quote}, m_linkPreviewFunction);
}
updateReplyModel();
updateReactionModel();
}
@@ -485,6 +489,10 @@ void MessageContentModel::resetContent(bool isEditing, bool isThreading)
m_components += newComponents;
endInsertRows();
if (m_room->urlPreviewEnabled()) {
forEachComponentOfType({MessageComponentType::Text, MessageComponentType::Quote}, m_linkPreviewFunction);
}
updateReplyModel();
updateReactionModel();
}
@@ -498,27 +506,13 @@ 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)));
}
if (m_room->urlPreviewEnabled()) {
newComponents = addLinkPreviews(newComponents);
newComponents.append(componentsForType(MessageComponentType::typeForEvent(*event.first, m_isReply)));
}
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()) {
@@ -597,22 +591,26 @@ QList<MessageComponent> MessageContentModel::componentsForType(MessageComponentT
}
switch (type) {
case MessageComponentType::Verification: {
return {MessageComponent{MessageComponentType::Verification, QString(), {}}};
}
case MessageComponentType::Text: {
const auto roomMessageEvent = eventCast<const Quotient::RoomMessageEvent>(event.first);
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());
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(body,
EventHandler::messageBodyInputFormat(*roomMessageEvent),
m_room,
roomMessageEvent,
roomMessageEvent->isReplaced());
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());
}
case MessageComponentType::File: {
QList<MessageComponent> components;
@@ -703,42 +701,20 @@ MessageComponent MessageContentModel::linkPreviewComponent(const QUrl &link)
}
if (linkPreviewer->loaded()) {
return MessageComponent{MessageComponentType::LinkPreview, QString(), {{"link"_L1, link}}};
} 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});
}
}
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});
}
}
});
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 it;
});
}
i++;
}
return inputComponents;
});
return MessageComponent{MessageComponentType::LinkPreviewLoad, QString(), {{"link"_L1, link}}};
}
void MessageContentModel::closeLinkPreview(int row)

View File

@@ -9,6 +9,7 @@
#include <Quotient/events/roomevent.h>
#include "enums/messagecomponenttype.h"
#include "linkpreviewer.h"
#include "messagecomponent.h"
#include "models/itinerarymodel.h"
#include "models/reactionmodel.h"
@@ -133,13 +134,32 @@ 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<void(const QModelIndex &)> function);
void forEachComponentOfType(QList<MessageComponentType::Type> types, std::function<void(const QModelIndex &)> function);
void forEachComponentOfType(MessageComponentType::Type type, std::function<ComponentIt(ComponentIt)> function);
void forEachComponentOfType(QList<MessageComponentType::Type> types, std::function<ComponentIt(ComponentIt)> function);
std::function<void(const QModelIndex &)> m_fileInfoFunction = [this](const QModelIndex &index) {
Q_EMIT dataChanged(index, index, {MessageContentModel::FileTransferInfoRole});
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;
};
void resetModel();
@@ -154,7 +174,6 @@ private:
QList<MessageComponent> componentsForType(MessageComponentType::Type type);
MessageComponent linkPreviewComponent(const QUrl &link);
QList<MessageComponent> addLinkPreviews(QList<MessageComponent> inputComponents);
QList<QUrl> m_removedLinkPreviews;

View File

@@ -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

View File

@@ -58,7 +58,7 @@ KirigamiComponents.ConvergentContextMenu {
icon.name: "notifications"
Kirigami.Action {
text: i18n("Follow Global Setting")
text: i18nc("@action:inmenu Notification 'Default Settings'", "Default Settings")
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, {

View File

@@ -70,8 +70,10 @@ 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: root.room.forget()
onTriggered: Qt.createComponent('org.kde.neochat', 'ConfirmLeaveDialog').createObject(root.QQC2.ApplicationWindow.window, {
room: root.room
}).open();
}
}

View File

@@ -115,39 +115,6 @@ 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"
@@ -179,12 +146,6 @@ 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;
}
}
}
}
@@ -291,7 +252,7 @@ FormCard.FormCardPage {
FormCard.FormCard {
FormCard.FormButtonDelegate {
id: deactivateAccountButton
text: i18n("Deactivate Account")
text: i18nc("@action:button", "Deactivate Account")
icon.name: "trash-empty-symbolic"
onClicked: {
const component = Qt.createComponent('org.kde.neochat', 'ConfirmDeactivateAccountDialog');

View File

@@ -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()
}

View File

@@ -45,6 +45,26 @@ 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")
}

View File

@@ -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()
}
}

View File

@@ -28,7 +28,7 @@ FormCard.FormCardPage {
FormCard.FormCard {
FormCard.FormRadioDelegate {
text: i18n("Follow global setting")
text: i18nc("As in the default notification setting", "Default Settings")
checked: room.pushNotificationState === PushNotificationState.Default
enabled: room.pushNotificationState !== PushNotificationState.Unknown
onToggled: {

View File

@@ -25,13 +25,34 @@ FormCard.FormCardPage {
title: i18nc("@option:check", "Encryption")
}
FormCard.FormCard {
FormCard.FormSwitchDelegate {
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 {
id: enableEncryptionSwitch
text: i18n("Enable encryption")
description: i18nc("option:check", "Once enabled, encryption cannot be disabled.")
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.")
enabled: room.canEncryptRoom
checked: room.usesEncryption
onToggled: if (checked) {
visible: !room.usesEncryption
onClicked: {
let dialog = confirmEncryptionDialog.createObject(QQC2.Overlay.overlay, {
room: room
});

View File

@@ -95,9 +95,11 @@ ColumnLayout {
}
}
QQC2.Button {
text: i18nc("@action:button", "Leave this space")
text: i18nc("@action:button", "Leave this space")
icon.name: "go-previous"
onClicked: root.room.forget()
onClicked: Qt.createComponent('org.kde.neochat', 'ConfirmLeaveDialog').createObject(root.QQC2.ApplicationWindow.window, {
room: root.room
}).open();
}
Item {
Layout.fillWidth: true

View File

@@ -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: {

View File

@@ -159,7 +159,7 @@ QQC2.ScrollView {
function onReadMarkerAdded() {
if (root.markReadCondition == LibNeoChat.TimelineMarkReadCondition.EntryVisible && messageListView.allUnreadVisible()) {
root.room.markAllMessagesAsRead();
_private.room.markAllMessagesAsRead();
}
}

View File

@@ -85,9 +85,14 @@ QHash<int, QByteArray> MediaMessageFilterModel::roleNames() const
return roles;
}
int MediaMessageFilterModel::getRowForSourceItem(int sourceRow) const
int MediaMessageFilterModel::getRowForEventId(const QString &eventId) const
{
return mapFromSource(sourceModel()->index(sourceRow, 0)).row();
for (auto i = 0; i < rowCount(); i++) {
if (data(index(i, 0), MessageModel::EventIdRole).toString() == eventId) {
return i;
}
}
return -1;
}
#include "moc_mediamessagefiltermodel.cpp"

View File

@@ -63,5 +63,5 @@ public:
*/
[[nodiscard]] QHash<int, QByteArray> roleNames() const override;
Q_INVOKABLE int getRowForSourceItem(int sourceRow) const;
int getRowForEventId(const QString &eventId) const;
};