Compare commits

...

44 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
l10n daemon script
bc7b480c41 GIT_SILENT Sync po/docbooks with svn 2025-07-08 01:40:16 +00:00
Tobias Fella
d9b495356d Fix roomlist positioning when changing rooms
The room list is supposed to center the new room, but, since the current room as stored in SortFilterRoomTreeModel is only updated after we're jumping to the room, mostly just jumped to the old room.
2025-07-07 18:31:40 +00:00
Tobias Fella
ce82606e6e Fix jumping to replied-to event 2025-07-07 14:20:02 +00:00
l10n daemon script
07837c2e64 GIT_SILENT Sync po/docbooks with svn 2025-07-07 01:38:29 +00:00
Tobias Fella
1738253e6f Don't crash when calling RoomManager::viewEventMenu with empty event id 2025-07-06 23:14:28 +02:00
James Graham
17fa2246da Refine MessageContentModel further
Create some standard functions that should hopefully make it easier to manage the model contents. Used for file stuff for now.
2025-07-06 21:16:17 +01:00
l10n daemon script
4f5e096e7e GIT_SILENT Sync po/docbooks with svn 2025-07-06 01:37:58 +00:00
Albert Astals Cid
b125c284bd GIT_SILENT Upgrade release service version to 25.11.70. 2025-07-05 11:59:46 +02:00
81 changed files with 14120 additions and 11518 deletions

View File

@@ -8,7 +8,7 @@ cmake_minimum_required(VERSION 3.16)
# KDE Applications version, managed by release script.
set(RELEASE_SERVICE_VERSION_MAJOR "25")
set(RELEASE_SERVICE_VERSION_MINOR "07")
set(RELEASE_SERVICE_VERSION_MINOR "11")
set(RELEASE_SERVICE_VERSION_MICRO "70")
set(RELEASE_SERVICE_VERSION "${RELEASE_SERVICE_VERSION_MAJOR}.${RELEASE_SERVICE_VERSION_MINOR}.${RELEASE_SERVICE_VERSION_MICRO}")

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

@@ -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);
}
@@ -264,6 +271,10 @@ void RoomManager::viewEventSource(const QString &eventId)
void RoomManager::viewEventMenu(const QString &eventId, NeoChatRoom *room, NeochatRoomMember *sender, const QString &selectedText, const QString &hoveredLink)
{
if (eventId.isEmpty()) {
qWarning() << "Tried to open event menu with empty event id";
return;
}
const auto &event = **room->findInTimeline(eventId);
if (EventHandler::mediaInfo(room, &event).contains("mimeType"_L1)) {
@@ -393,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

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

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

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

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

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

@@ -65,7 +65,7 @@ RowLayout {
id: contentRepeater
model: root.replyContentModel
delegate: ReplyMessageComponentChooser {
onReplyClicked: root.Message.timeline.goToEvent(root.replyEventId)
onReplyClicked: RoomManager.goToEvent(root.replyEventId)
}
}
}
@@ -74,7 +74,7 @@ RowLayout {
}
TapHandler {
acceptedButtons: Qt.LeftButton
onTapped: root.Message.timeline.goToEvent(root.replyEventId)
onTapped: RoomManager.goToEvent(root.replyEventId)
}
QtObject {
id: _private

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"
@@ -57,7 +56,6 @@ void MessageContentModel::initializeModel()
connect(m_room, &NeoChatRoom::pendingEventAdded, this, [this]() {
if (m_room != nullptr && m_currentState == Unknown) {
initializeEvent();
updateReplyModel();
resetModel();
}
});
@@ -71,7 +69,6 @@ void MessageContentModel::initializeModel()
connect(m_room, &NeoChatRoom::pendingEventMerged, this, [this]() {
if (m_room != nullptr && m_currentState == Pending) {
initializeEvent();
updateReplyModel();
resetModel();
}
});
@@ -80,7 +77,6 @@ void MessageContentModel::initializeModel()
for (int i = fromIndex; i <= toIndex; i++) {
if (m_room->findInTimeline(i)->event()->id() == m_eventId) {
initializeEvent();
updateReplyModel();
resetModel();
}
}
@@ -96,22 +92,26 @@ void MessageContentModel::initializeModel()
});
connect(m_room, &NeoChatRoom::newFileTransfer, this, [this](const QString &eventId) {
if (eventId == m_eventId) {
updateFileInfo();
forEachComponentOfType({MessageComponentType::File, MessageComponentType::Audio, MessageComponentType::Image, MessageComponentType::Video},
m_fileInfoFunction);
}
});
connect(m_room, &NeoChatRoom::fileTransferProgress, this, [this](const QString &eventId) {
if (eventId == m_eventId) {
updateFileInfo();
forEachComponentOfType({MessageComponentType::File, MessageComponentType::Audio, MessageComponentType::Image, MessageComponentType::Video},
m_fileInfoFunction);
}
});
connect(m_room, &NeoChatRoom::fileTransferCompleted, this, [this](const QString &eventId) {
if (m_room != nullptr && eventId == m_eventId) {
updateFileInfo();
forEachComponentOfType({MessageComponentType::File, MessageComponentType::Audio, MessageComponentType::Image, MessageComponentType::Video},
m_fileInfoFunction);
}
});
connect(m_room, &NeoChatRoom::fileTransferFailed, this, [this](const QString &eventId, const QString &errorMessage) {
if (eventId == m_eventId) {
updateFileInfo();
forEachComponentOfType({MessageComponentType::File, MessageComponentType::Audio, MessageComponentType::Image, MessageComponentType::Video},
m_fileInfoFunction);
if (errorMessage.isEmpty()) {
Q_EMIT m_room->showMessage(MessageType::Error, i18nc("@info", "Failed to download file."));
} else {
@@ -149,7 +149,6 @@ void MessageContentModel::initializeModel()
});
connect(this, &MessageContentModel::threadsEnabledChanged, this, [this]() {
updateReplyModel();
resetModel();
});
connect(m_room, &Room::updatedEvent, this, [this](const QString &eventId) {
@@ -159,23 +158,9 @@ void MessageContentModel::initializeModel()
});
initializeEvent();
if (m_currentState == Available || m_currentState == Pending) {
updateReplyModel();
}
resetModel();
}
void MessageContentModel::updateFileInfo()
{
for (auto it = m_components.cbegin(); it != m_components.cend(); it++) {
const auto currentIndex = it - m_components.cbegin();
if (m_components.at(currentIndex).type == MessageComponentType::File || m_components.at(currentIndex).type == MessageComponentType::Audio
|| m_components.at(currentIndex).type == MessageComponentType::Image || m_components.at(currentIndex).type == MessageComponentType::Video) {
Q_EMIT dataChanged(index(currentIndex), index(currentIndex), {FileTransferInfoRole});
}
}
}
void MessageContentModel::initializeEvent()
{
if (m_currentState == UnAvailable) {
@@ -203,7 +188,6 @@ void MessageContentModel::getEvent()
if (m_room != nullptr) {
if (eventId == m_eventId) {
initializeEvent();
updateReplyModel();
resetModel();
return true;
}
@@ -427,6 +411,38 @@ QHash<int, QByteArray> MessageContentModel::roleNamesStatic()
return roles;
}
bool MessageContentModel::hasComponentType(MessageComponentType::Type type)
{
return std::find_if(m_components.cbegin(),
m_components.cend(),
[type](const MessageComponent &component) {
return component.type == type;
})
!= m_components.cend();
}
void MessageContentModel::forEachComponentOfType(MessageComponentType::Type type,
std::function<MessageContentModel::ComponentIt(MessageContentModel::ComponentIt)> function)
{
auto it = m_components.begin();
while ((it = std::find_if(it,
m_components.end(),
[type](const MessageComponent &component) {
return component.type == type;
}))
!= m_components.end()) {
it = function(it);
}
}
void MessageContentModel::forEachComponentOfType(QList<MessageComponentType::Type> types,
std::function<MessageContentModel::ComponentIt(MessageContentModel::ComponentIt)> function)
{
for (const auto &type : types) {
forEachComponentOfType(type, function);
}
}
void MessageContentModel::resetModel()
{
beginResetModel();
@@ -450,6 +466,11 @@ void MessageContentModel::resetModel()
m_components += messageContentComponents();
endResetModel();
if (m_room->urlPreviewEnabled()) {
forEachComponentOfType({MessageComponentType::Text, MessageComponentType::Quote}, m_linkPreviewFunction);
}
updateReplyModel();
updateReactionModel();
}
@@ -468,6 +489,11 @@ void MessageContentModel::resetContent(bool isEditing, bool isThreading)
m_components += newComponents;
endInsertRows();
if (m_room->urlPreviewEnabled()) {
forEachComponentOfType({MessageComponentType::Text, MessageComponentType::Quote}, m_linkPreviewFunction);
}
updateReplyModel();
updateReactionModel();
}
@@ -480,31 +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 (m_replyModel != nullptr) {
newComponents += MessageComponent{MessageComponentType::Reply, QString(), {}};
}
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()) {
@@ -540,7 +548,8 @@ void MessageContentModel::updateReplyModel()
}
if (!roomMessageEvent->isReply(m_threadsEnabled) || (roomMessageEvent->isThreaded() && m_threadsEnabled)) {
if (m_replyModel) {
delete m_replyModel;
m_replyModel->disconnect(this);
m_replyModel->deleteLater();
}
return;
}
@@ -554,6 +563,24 @@ void MessageContentModel::updateReplyModel()
connect(m_replyModel, &MessageContentModel::eventUpdated, this, [this]() {
Q_EMIT dataChanged(index(0), index(0), {ReplyAuthorRole});
});
bool hasModel = hasComponentType(MessageComponentType::Reply);
if (m_replyModel && !hasModel) {
int insertRow = 0;
if (m_components.first().type == MessageComponentType::Author) {
insertRow = 1;
}
beginInsertRows({}, insertRow, insertRow);
m_components.insert(insertRow, MessageComponent{MessageComponentType::Reply, QString(), {}});
} else if (!m_replyModel && hasModel) {
int removeRow = 0;
if (m_components.first().type == MessageComponentType::Author) {
removeRow = 1;
}
beginRemoveRows({}, removeRow, removeRow);
m_components.removeAt(removeRow);
endRemoveRows();
}
}
QList<MessageComponent> MessageContentModel::componentsForType(MessageComponentType::Type type)
@@ -564,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;
@@ -670,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"
@@ -132,9 +133,35 @@ private:
void initializeModel();
void initializeEvent();
void getEvent();
void updateFileInfo();
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);
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();
void resetContent(bool isEditing = false, bool isThreading = false);
QList<MessageComponent> messageContentComponents(bool isEditing = false, bool isThreading = false);
@@ -147,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

@@ -96,7 +96,10 @@ Kirigami.Page {
function onCurrentSpaceChanged() {
treeView.expandRecursively();
}
}
Connections {
target: RoomManager.sortFilterRoomTreeModel
function onCurrentRoomChanged() {
treeView.positionViewAtIndex(RoomManager.sortFilterRoomTreeModel.currentRoomIndex(), TableView.AlignVCenter)
}

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

@@ -134,6 +134,7 @@ void SortFilterRoomTreeModel::setActiveSpaceId(const QString &spaceId)
void SortFilterRoomTreeModel::setCurrentRoom(NeoChatRoom *room)
{
m_currentRoom = room;
Q_EMIT currentRoomChanged();
}
SortFilterRoomTreeModel::Mode SortFilterRoomTreeModel::mode() const

View File

@@ -104,6 +104,7 @@ Q_SIGNALS:
void filterTextChanged();
void activeSpaceIdChanged();
void modeChanged();
void currentRoomChanged();
private:
Mode m_mode = All;

View File

@@ -252,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;
};