Compare commits

..

2 Commits

Author SHA1 Message Date
Carlos De Maine
bf754711e3 skip windows as one test is failing 2025-07-06 09:34:01 +10:00
Carlos De Maine
32d0f846ee Testing for VM based CI 2025-07-06 09:15:21 +10:00
81 changed files with 11430 additions and 14031 deletions

View File

@@ -3,19 +3,20 @@
include:
- project: sysadmin/ci-utilities
ref: work/switch-vm-ci
file:
- /gitlab-templates/reuse-lint.yml
- /gitlab-templates/json-validation.yml
- /gitlab-templates/xml-lint.yml
- /gitlab-templates/yaml-lint.yml
- /gitlab-templates/android-qt6.yml
#- /gitlab-templates/reuse-lint.yml
#- /gitlab-templates/json-validation.yml
#- /gitlab-templates/xml-lint.yml
#- /gitlab-templates/yaml-lint.yml
#- /gitlab-templates/android-qt6.yml
- /gitlab-templates/linux-qt6.yml
- /gitlab-templates/linux-qt6-next.yml
- /gitlab-templates/windows-qt6.yml
- /gitlab-templates/freebsd-qt6.yml
#- /gitlab-templates/windows-qt6.yml
#- /gitlab-templates/freebsd-qt6.yml
- /gitlab-templates/flatpak.yml
- /gitlab-templates/snap-snapcraft-lxd.yml
- /gitlab-templates/craft-android-qt6-apks.yml
- /gitlab-templates/craft-appimage-qt6.yml
#- /gitlab-templates/craft-android-qt6-apks.yml
#- /gitlab-templates/craft-appimage-qt6.yml
- /gitlab-templates/craft-windows-x86-64-qt6.yml
- /gitlab-templates/craft-windows-appx-qt6.yml

View File

@@ -130,8 +130,7 @@ void EventHandlerTest::timeString()
QLocale().toString(QDateTime::fromMSecsSinceEpoch(1432735824654, QTimeZone(QTimeZone::UTC)).toLocalTime().time(), QLocale::ShortFormat));
QCOMPARE(EventHandler::timeString(room, event, true),
format.formatRelativeDate(QDateTime::fromMSecsSinceEpoch(1432735824654, QTimeZone(QTimeZone::UTC)).toLocalTime().date(), QLocale::ShortFormat));
QCOMPARE(EventHandler::timeString(room, event, u"hh:mm"_s),
QDateTime::fromMSecsSinceEpoch(1432735824654, QTimeZone(QTimeZone::LocalTime)).toString(u"hh:mm"_s));
QCOMPARE(EventHandler::timeString(room, event, u"hh:mm"_s), QDateTime::fromMSecsSinceEpoch(1432735824654, QTimeZone(QTimeZone::UTC)).toString(u"hh:mm"_s));
const auto txID = room->postJson("m.room.message"_L1, event->fullJson());
QCOMPARE(room->pendingEvents().size(), 1);

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,15 +52,6 @@ ColumnLayout {
Layout.alignment: Qt.AlignHCenter
}
Kirigami.SelectableLabel {
Layout.fillWidth: true
font: Kirigami.Theme.smallFont
textFormat: TextEdit.PlainText
visible: root.currentRoom && root.currentRoom.canonicalAlias
text: root.currentRoom && root.currentRoom.canonicalAlias ? root.currentRoom.canonicalAlias : ""
color: Kirigami.Theme.disabledTextColor
}
Kirigami.Heading {
text: root.currentRoom.displayName
@@ -79,14 +70,7 @@ ColumnLayout {
spacing: Kirigami.Units.smallSpacing
Kirigami.Heading {
text: root.invitingMember.displayName
Layout.alignment: Qt.AlignHCenter
}
QQC2.Label {
text: root.invitingMember.id
color: Kirigami.Theme.disabledTextColor
text: root.currentRoom.displayName
Layout.alignment: Qt.AlignHCenter
}
@@ -175,7 +159,7 @@ ColumnLayout {
QQC2.Label {
color: Kirigami.Theme.disabledTextColor
text: xi18nc("@info:label Ensure you are referring to the same translation used for that settings page", "You can reject invitations from unknown users under the <interface>Security & Safety</interface> settings.")
text: i18nc("@info:label", "You can reject invitations from unknown users under Security settings.")
wrapMode: Text.WordWrap
// + 5 to prevent it from wrapping unnecessarily

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

View File

@@ -236,18 +236,11 @@ void RoomManager::resolveResource(Uri uri, const QString &action)
}
}
void RoomManager::maximizeMedia(const QString &eventId)
void RoomManager::maximizeMedia(int index)
{
if (eventId.isEmpty()) {
qWarning() << "Tried to open media for empty event id";
if (index < -1 || index > m_mediaMessageFilterModel->rowCount()) {
return;
}
const auto index = m_mediaMessageFilterModel->getRowForEventId(eventId);
if (index == -1) {
return;
}
Q_EMIT showMaximizedMedia(index);
}
@@ -271,10 +264,6 @@ 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)) {
@@ -404,9 +393,7 @@ void RoomManager::joinRoom(Quotient::Connection *account, const QString &roomAli
// If no one gives us a homeserver suggestion, try the server specified in the alias/id.
// Otherwise joining a remote room not on our homeserver will fail.
// This is a hack and we're not supposed to do it. With room ids not containing the server going forward, it won't work anymore for new room versions.
// FIXME: Let's keep it around anyway for now, remove it at some point, though
if (vias.empty() && roomAliasOrId.contains(':'_L1)) {
if (vias.empty()) {
vias.append(roomAliasOrId.mid(roomAliasOrId.lastIndexOf(':'_L1) + 1));
}

View File

@@ -212,8 +212,12 @@ public:
/**
* @brief Show a media item maximized.
*
* @param index the index to open the maximize delegate model at. This is the
* index in the MediaMessageFilterModel owned by this RoomManager. A value
* of -1 opens a the default item.
*/
Q_INVOKABLE void maximizeMedia(const QString &eventId);
Q_INVOKABLE void maximizeMedia(int index);
Q_INVOKABLE void maximizeCode(NeochatRoomMember *author, const QDateTime &time, const QString &codeText, const QString &language);

View File

@@ -70,23 +70,13 @@ public:
*
* @param event the event to return a type for.
*
* @param isInReply whether this event is to be treated like a replied-to event (i.e., a basic text fallback should be shown if no other type is used)
*
* @sa Type
*/
static Type typeForEvent(const Quotient::RoomEvent &event, bool isInReply = false)
static Type typeForEvent(const Quotient::RoomEvent &event)
{
using namespace Quotient;
if (event.isRedacted()) {
return MessageComponentType::Text;
}
if (const auto e = eventCast<const RoomMessageEvent>(&event)) {
if (e->rawMsgtype() == u"m.key.verification.request"_s) {
return MessageComponentType::Verification;
}
switch (e->msgtype()) {
case MessageEventType::Emote:
return MessageComponentType::Text;
@@ -113,8 +103,7 @@ public:
if (event.matrixType() == u"org.matrix.msc3672.beacon_info"_s) {
return MessageComponentType::LiveLocation;
}
// In the (unlikely) case that this is a reply to a state event, we do want to show something
return isInReply ? MessageComponentType::Text : MessageComponentType::Other;
return MessageComponentType::Other;
}
if (is<const EncryptedEvent>(event)) {
return MessageComponentType::Encrypted;
@@ -127,8 +116,7 @@ public:
return MessageComponentType::Poll;
}
// In the (unlikely) case that this is a reply to an unusual event, we do want to show something
return isInReply ? MessageComponentType::Text : MessageComponentType::Other;
return MessageComponentType::Other;
}
/**

View File

@@ -448,12 +448,6 @@ QString EventHandler::getBody(const NeoChatRoom *room, const Quotient::RoomEvent
[](const PollStartEvent &e) {
return e.question();
},
[](const EncryptedEvent &) {
return i18nc("@info In room list", "Encrypted event");
},
[](const ReactionEvent &e) {
return i18nc("[user] reacted with <emoji>", "reacted with %1", e.key());
},
i18n("Unknown event"));
}

View File

@@ -31,7 +31,13 @@ auto leaveRoomLambda = [](const QString &text, NeoChatRoom *room, ChatBarCache *
Q_EMIT room->showMessage(MessageType::Information, i18n("Leaving this room."));
room->forget();
} else {
// FIXME: re-add sanity check for roomId/alias
QRegularExpression roomRegex(uR"(^[#!][^:]+:\w(?:\w|\.|-)*\.\w+(?::\d{1,5})?)"_s);
auto regexMatch = roomRegex.match(text);
if (!regexMatch.hasMatch()) {
Q_EMIT room->showMessage(MessageType::Error,
i18nc("'<text>' does not look like a room id or alias.", "'%1' does not look like a room id or alias.", text));
return QString();
}
auto leaving = dynamic_cast<NeoChatRoom *>(room->connection()->room(text));
if (!leaving) {
leaving = dynamic_cast<NeoChatRoom *>(room->connection()->roomByAlias(text));
@@ -211,7 +217,13 @@ QList<ActionsModel::Action> actions{
Action{
u"join"_s,
[](const QString &text, NeoChatRoom *room, ChatBarCache *) {
// FIXME: re-add sanity check for roomId/alias
QRegularExpression roomRegex(uR"(^[#!][^:]+:\w(?:\w|\.|-)*\.\w+(?::\d{1,5})?)"_s);
auto regexMatch = roomRegex.match(text);
if (!regexMatch.hasMatch()) {
Q_EMIT room->showMessage(MessageType::Error,
i18nc("'<text>' does not look like a room id or alias.", "'%1' does not look like a room id or alias.", text));
return QString();
}
auto targetRoom = text.startsWith(QLatin1Char('!')) ? room->connection()->room(text) : room->connection()->roomByAlias(text);
if (targetRoom) {
ActionsModel::instance().resolveResource(targetRoom->id());
@@ -230,18 +242,25 @@ QList<ActionsModel::Action> actions{
[](const QString &text, NeoChatRoom *room, ChatBarCache *) {
auto parts = text.split(u" "_s);
QString roomName = parts[0];
// FIXME: re-add sanity check for roomId/alias
if (const auto targetRoom = text.startsWith(QLatin1Char('!')) ? room->connection()->room(text) : room->connection()->roomByAlias(text)) {
QRegularExpression roomRegex(uR"(^[#!][^:]+:\w(?:\w|\.|-)*\.\w+(?::\d{1,5})?)"_s);
auto regexMatch = roomRegex.match(roomName);
if (!regexMatch.hasMatch()) {
Q_EMIT room->showMessage(MessageType::Error,
i18nc("'<text>' does not look like a room id or alias.", "'%1' does not look like a room id or alias.", text));
return QString();
}
auto targetRoom = text.startsWith(QLatin1Char('!')) ? room->connection()->room(text) : room->connection()->roomByAlias(text);
if (targetRoom) {
ActionsModel::instance().resolveResource(targetRoom->id());
return QString();
}
Q_EMIT room->showMessage(MessageType::Information, i18nc("Knocking room <roomname>.", "Knocking room %1.", text));
auto connection = dynamic_cast<NeoChatConnection *>(room->connection());
const auto knownServer = roomName.contains(":"_L1) ? QStringList{roomName.mid(roomName.indexOf(":"_L1) + 1)} : QStringList();
const auto knownServer = roomName.mid(roomName.indexOf(":"_L1) + 1);
if (parts.length() >= 2) {
ActionsModel::instance().knockRoom(connection, roomName, parts[1], knownServer);
ActionsModel::instance().knockRoom(connection, roomName, parts[1], QStringList{knownServer});
} else {
ActionsModel::instance().knockRoom(connection, roomName, QString(), knownServer);
ActionsModel::instance().knockRoom(connection, roomName, QString(), QStringList{knownServer});
}
return QString();
},
@@ -252,7 +271,13 @@ QList<ActionsModel::Action> actions{
Action{
u"j"_s,
[](const QString &text, NeoChatRoom *room, ChatBarCache *) {
// FIXME: re-add sanity check for roomId/alias
QRegularExpression roomRegex(uR"(^[#!][^:]+:\w(?:\w|\.|-)*\.\w+(?::\d{1,5})?)"_s);
auto regexMatch = roomRegex.match(text);
if (!regexMatch.hasMatch()) {
Q_EMIT room->showMessage(MessageType::Error,
i18nc("'<text>' does not look like a room id or alias.", "'%1' does not look like a room id or alias.", text));
return QString();
}
if (room->connection()->room(text) || room->connection()->roomByAlias(text)) {
Q_EMIT room->showMessage(MessageType::Information, i18nc("You are already in room <roomname>.", "You are already in room %1.", text));
return QString();

View File

@@ -100,10 +100,6 @@ QVariant UserListModel::data(const QModelIndex &index, int role) const
return plEvent->powerLevelForUser(memberId);
}
if (role == PowerLevelStringRole) {
if (m_currentRoom->roomCreatorHasUltimatePowerLevel() && m_currentRoom->isCreator(memberId)) {
return i18nc("@info the person that created this room", "Creator");
}
auto pl = m_currentRoom->currentState().get<RoomPowerLevelsEvent>();
// User might not in the room yet, in this case pl can be nullptr.
// e.g. When invited but user not accepted or denied the invitation.

View File

@@ -359,14 +359,9 @@ const RoomEvent *NeoChatRoom::lastEvent(std::function<bool(const RoomEvent *)> f
if (auto lastEvent = eventCast<const RoomMessageEvent>(event)) {
return lastEvent;
}
if (auto lastEvent = eventCast<const PollStartEvent>(event)) {
return lastEvent;
}
if (auto lastEvent = eventCast<const EncryptedEvent>(event)) {
return lastEvent;
}
}
if (m_cachedEvent != nullptr) {
@@ -446,19 +441,20 @@ void NeoChatRoom::onRedaction(const RoomEvent &prevEvent, const RoomEvent & /*af
}
}
QDateTime NeoChatRoom::lastActiveTime() const
QDateTime NeoChatRoom::lastActiveTime()
{
// Find the last relevant event:
if (const auto event = lastEvent(m_hiddenFilter)) {
if (timelineSize() == 0) {
if (m_cachedEvent != nullptr) {
return m_cachedEvent->originTimestamp();
}
return QDateTime();
}
if (auto event = lastEvent()) {
return event->originTimestamp();
}
// If nothing is loaded yet, and there is no cached event:
if (timelineSize() == 0) {
return {};
}
// No message found, take last event:
// no message found, take last event
return messageEvents().rbegin()->get()->originTimestamp();
}
@@ -536,9 +532,6 @@ bool NeoChatRoom::containsUser(const QString &userID) const
bool NeoChatRoom::canSendEvent(const QString &eventType) const
{
if (roomCreatorHasUltimatePowerLevel() && isCreator(localMember().id())) {
return true;
}
auto plEvent = currentState().get<RoomPowerLevelsEvent>();
if (!plEvent) {
return false;
@@ -551,9 +544,6 @@ bool NeoChatRoom::canSendEvent(const QString &eventType) const
bool NeoChatRoom::canSendState(const QString &eventType) const
{
if (roomCreatorHasUltimatePowerLevel() && isCreator(localMember().id())) {
return true;
}
auto plEvent = currentState().get<RoomPowerLevelsEvent>();
if (!plEvent) {
return false;
@@ -1680,14 +1670,8 @@ void NeoChatRoom::setRoomState(const QString &type, const QString &stateKey, con
NeochatRoomMember *NeoChatRoom::qmlSafeMember(const QString &memberId)
{
if (memberId.isEmpty()) {
return nullptr;
}
if (!m_memberObjects.contains(memberId)) {
auto member = m_memberObjects.emplace(memberId, std::make_unique<NeochatRoomMember>(this, memberId)).first->second.get();
QQmlEngine::setObjectOwnership(member, QQmlEngine::CppOwnership);
return member;
return m_memberObjects.emplace(memberId, std::make_unique<NeochatRoomMember>(this, memberId)).first->second.get();
}
return m_memberObjects[memberId].get();
@@ -1747,20 +1731,4 @@ void NeoChatRoom::setHiddenFilter(std::function<bool(const Quotient::RoomEvent *
NeoChatRoom::m_hiddenFilter = hiddenFilter;
}
bool NeoChatRoom::roomCreatorHasUltimatePowerLevel() const
{
bool ok = false;
auto version = this->version().toInt(&ok);
// This is terrible. For non-numeric room versions, I don't think there's a way of knowing whether they're pre- or post hydra.
// We just assume they are. Shouldn't matter for normal users anyway.
return !ok || version > 11;
}
bool NeoChatRoom::isCreator(const QString &userId) const
{
auto createEvent = currentState().get<RoomCreateEvent>();
return roomCreatorHasUltimatePowerLevel() && createEvent
&& (createEvent->senderId() == userId || createEvent->contentPart<QStringList>(u"additional_creators"_s).contains(userId));
}
#include "moc_neochatroom.cpp"

View File

@@ -208,7 +208,7 @@ public:
bool visible() const;
void setVisible(bool visible);
[[nodiscard]] QDateTime lastActiveTime() const;
[[nodiscard]] QDateTime lastActiveTime();
/**
* @brief Get the last interesting event.
@@ -589,18 +589,6 @@ public:
static void setHiddenFilter(std::function<bool(const Quotient::RoomEvent *)> hiddenFilter);
/**
* @brief Whether this room has a room version where the creator is treated as having an ultimate power level
*
* For unusual room versions, this information might be wrong.
*/
bool roomCreatorHasUltimatePowerLevel() const;
/**
* @brief Whether this user is considered a creator of this room. Only applies to post-v12 rooms.
*/
bool isCreator(const QString &userId) const;
private:
bool m_visible = false;

View File

@@ -570,9 +570,8 @@ QVariantMap TextHandler::getAttributes(const QString &tag, const QString &tagStr
QList<MessageComponent>
TextHandler::textComponents(QString string, Qt::TextFormat inputFormat, const NeoChatRoom *room, const Quotient::RoomEvent *event, bool isEdited)
{
if (string.trimmed().isEmpty() && event->is<Quotient::RoomMessageEvent>()
&& !eventCast<const Quotient::RoomMessageEvent>(event)->has<Quotient::EventContent::FileContentBase>()) {
return {MessageComponent{MessageComponentType::Text, i18n("<i>This event does not have any content.</i>"), {}}};
if (string.isEmpty()) {
return {};
}
// Strip mx-reply if present.
@@ -591,7 +590,7 @@ TextHandler::textComponents(QString string, Qt::TextFormat inputFormat, const Ne
string = string.trimmed();
if (event != nullptr && room != nullptr) {
if (auto e = eventCast<const Quotient::RoomMessageEvent>(event); e && e->msgtype() == Quotient::MessageEventType::Emote && components.size() == 1) {
if (auto e = eventCast<const Quotient::RoomMessageEvent>(event); e->msgtype() == Quotient::MessageEventType::Emote && components.size() == 1) {
if (components[0].type == MessageComponentType::Text) {
components[0].content = emoteString(room, event) + components[0].content;
} else {

View File

@@ -158,7 +158,12 @@ Item {
}
root.Message.timeline.interactive = false;
if (!root.mediaInfo.isSticker) {
RoomManager.maximizeMedia(root.eventId);
// We need to make sure the index is that of the MediaMessageFilterModel.
if (root.Message.timeline.model instanceof MessageFilterModel) {
RoomManager.maximizeMedia(RoomManager.mediaMessageFilterModel.getRowForSourceItem(root.Message.index));
} else {
RoomManager.maximizeMedia(root.Message.index);
}
}
}
}

View File

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

View File

@@ -385,7 +385,12 @@ Video {
onTriggered: {
root.Message.timeline.interactive = false;
root.pause();
RoomManager.maximizeMedia(root.eventId);
// We need to make sure the index is that of the MediaMessageFilterModel.
if (root.Message.timeline.model instanceof MessageFilterModel) {
RoomManager.maximizeMedia(RoomManager.mediaMessageFilterModel.getRowForSourceItem(root.Message.index));
} else {
RoomManager.maximizeMedia(root.Message.index);
}
}
}
}

View File

@@ -29,6 +29,7 @@
#include "chatbarcache.h"
#include "contentprovider.h"
#include "filetype.h"
#include "linkpreviewer.h"
#include "models/reactionmodel.h"
#include "neochatconnection.h"
#include "neochatroom.h"
@@ -56,6 +57,7 @@ void MessageContentModel::initializeModel()
connect(m_room, &NeoChatRoom::pendingEventAdded, this, [this]() {
if (m_room != nullptr && m_currentState == Unknown) {
initializeEvent();
updateReplyModel();
resetModel();
}
});
@@ -69,6 +71,7 @@ void MessageContentModel::initializeModel()
connect(m_room, &NeoChatRoom::pendingEventMerged, this, [this]() {
if (m_room != nullptr && m_currentState == Pending) {
initializeEvent();
updateReplyModel();
resetModel();
}
});
@@ -77,6 +80,7 @@ void MessageContentModel::initializeModel()
for (int i = fromIndex; i <= toIndex; i++) {
if (m_room->findInTimeline(i)->event()->id() == m_eventId) {
initializeEvent();
updateReplyModel();
resetModel();
}
}
@@ -92,26 +96,22 @@ void MessageContentModel::initializeModel()
});
connect(m_room, &NeoChatRoom::newFileTransfer, this, [this](const QString &eventId) {
if (eventId == m_eventId) {
forEachComponentOfType({MessageComponentType::File, MessageComponentType::Audio, MessageComponentType::Image, MessageComponentType::Video},
m_fileInfoFunction);
updateFileInfo();
}
});
connect(m_room, &NeoChatRoom::fileTransferProgress, this, [this](const QString &eventId) {
if (eventId == m_eventId) {
forEachComponentOfType({MessageComponentType::File, MessageComponentType::Audio, MessageComponentType::Image, MessageComponentType::Video},
m_fileInfoFunction);
updateFileInfo();
}
});
connect(m_room, &NeoChatRoom::fileTransferCompleted, this, [this](const QString &eventId) {
if (m_room != nullptr && eventId == m_eventId) {
forEachComponentOfType({MessageComponentType::File, MessageComponentType::Audio, MessageComponentType::Image, MessageComponentType::Video},
m_fileInfoFunction);
updateFileInfo();
}
});
connect(m_room, &NeoChatRoom::fileTransferFailed, this, [this](const QString &eventId, const QString &errorMessage) {
if (eventId == m_eventId) {
forEachComponentOfType({MessageComponentType::File, MessageComponentType::Audio, MessageComponentType::Image, MessageComponentType::Video},
m_fileInfoFunction);
updateFileInfo();
if (errorMessage.isEmpty()) {
Q_EMIT m_room->showMessage(MessageType::Error, i18nc("@info", "Failed to download file."));
} else {
@@ -149,6 +149,7 @@ void MessageContentModel::initializeModel()
});
connect(this, &MessageContentModel::threadsEnabledChanged, this, [this]() {
updateReplyModel();
resetModel();
});
connect(m_room, &Room::updatedEvent, this, [this](const QString &eventId) {
@@ -158,9 +159,23 @@ 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) {
@@ -188,6 +203,7 @@ void MessageContentModel::getEvent()
if (m_room != nullptr) {
if (eventId == m_eventId) {
initializeEvent();
updateReplyModel();
resetModel();
return true;
}
@@ -411,38 +427,6 @@ 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();
@@ -466,11 +450,6 @@ void MessageContentModel::resetModel()
m_components += messageContentComponents();
endResetModel();
if (m_room->urlPreviewEnabled()) {
forEachComponentOfType({MessageComponentType::Text, MessageComponentType::Quote}, m_linkPreviewFunction);
}
updateReplyModel();
updateReactionModel();
}
@@ -489,11 +468,6 @@ void MessageContentModel::resetContent(bool isEditing, bool isThreading)
m_components += newComponents;
endInsertRows();
if (m_room->urlPreviewEnabled()) {
forEachComponentOfType({MessageComponentType::Text, MessageComponentType::Quote}, m_linkPreviewFunction);
}
updateReplyModel();
updateReactionModel();
}
@@ -506,13 +480,31 @@ 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, m_isReply)));
newComponents.append(componentsForType(MessageComponentType::typeForEvent(*event.first)));
}
if (m_room->urlPreviewEnabled()) {
newComponents = addLinkPreviews(newComponents);
}
const auto roomMessageEvent = eventCast<const Quotient::RoomMessageEvent>(event.first);
#if Quotient_VERSION_MINOR > 9 || (Quotient_VERSION_MINOR == 9 && Quotient_VERSION_PATCH > 1)
if (m_threadsEnabled && roomMessageEvent && (roomMessageEvent->isThreaded() || m_room->threads().contains(roomMessageEvent->id()))
&& roomMessageEvent->id() == roomMessageEvent->threadRootEventId()) {
@@ -548,8 +540,7 @@ void MessageContentModel::updateReplyModel()
}
if (!roomMessageEvent->isReply(m_threadsEnabled) || (roomMessageEvent->isThreaded() && m_threadsEnabled)) {
if (m_replyModel) {
m_replyModel->disconnect(this);
m_replyModel->deleteLater();
delete m_replyModel;
}
return;
}
@@ -563,24 +554,6 @@ 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)
@@ -591,26 +564,22 @@ QList<MessageComponent> MessageContentModel::componentsForType(MessageComponentT
}
switch (type) {
case MessageComponentType::Verification: {
return {MessageComponent{MessageComponentType::Verification, QString(), {}}};
}
case MessageComponentType::Text: {
if (const auto roomMessageEvent = eventCast<const Quotient::RoomMessageEvent>(event.first)) {
return TextHandler().textComponents(EventHandler::rawMessageBody(*roomMessageEvent),
EventHandler::messageBodyInputFormat(*roomMessageEvent),
m_room,
roomMessageEvent,
roomMessageEvent->isReplaced());
} else {
return TextHandler().textComponents(EventHandler::plainBody(m_room, event.first), Qt::TextFormat::PlainText, m_room, event.first, false);
}
const auto roomMessageEvent = eventCast<const Quotient::RoomMessageEvent>(event.first);
return TextHandler().textComponents(EventHandler::rawMessageBody(*roomMessageEvent),
EventHandler::messageBodyInputFormat(*roomMessageEvent),
m_room,
roomMessageEvent,
roomMessageEvent->isReplaced());
auto body = EventHandler::rawMessageBody(*roomMessageEvent);
if (body.trimmed().isEmpty()) {
return TextHandler().textComponents(i18n("<i>This event does not have any content.</i>"),
Qt::TextFormat::RichText,
m_room,
roomMessageEvent,
roomMessageEvent->isReplaced());
} else {
return TextHandler().textComponents(body,
EventHandler::messageBodyInputFormat(*roomMessageEvent),
m_room,
roomMessageEvent,
roomMessageEvent->isReplaced());
}
}
case MessageComponentType::File: {
QList<MessageComponent> components;
@@ -701,20 +670,42 @@ MessageComponent MessageContentModel::linkPreviewComponent(const QUrl &link)
}
if (linkPreviewer->loaded()) {
return MessageComponent{MessageComponentType::LinkPreview, QString(), {{"link"_L1, link}}};
}
connect(linkPreviewer, &LinkPreviewer::loadedChanged, this, [this, link]() {
const auto linkPreviewer = dynamic_cast<NeoChatConnection *>(m_room->connection())->previewerForLink(link);
if (linkPreviewer != nullptr && linkPreviewer->loaded()) {
forEachComponentOfType(MessageComponentType::LinkPreviewLoad, [this, link](ComponentIt it) {
if (it->attributes["link"_L1].toUrl() == link) {
it->type = MessageComponentType::LinkPreview;
Q_EMIT dataChanged(index(it - m_components.begin()), index(it - m_components.begin()), {ComponentTypeRole});
} else {
connect(linkPreviewer, &LinkPreviewer::loadedChanged, this, [this, link]() {
const auto linkPreviewer = dynamic_cast<NeoChatConnection *>(m_room->connection())->previewerForLink(link);
if (linkPreviewer != nullptr && linkPreviewer->loaded()) {
for (auto it = m_components.begin(); it != m_components.end(); it++) {
if (it->attributes["link"_L1].toUrl() == link) {
it->type = MessageComponentType::LinkPreview;
Q_EMIT dataChanged(index(it - m_components.begin()), index(it - m_components.begin()), {ComponentTypeRole});
}
}
return it;
});
}
});
return MessageComponent{MessageComponentType::LinkPreviewLoad, QString(), {{"link"_L1, link}}};
}
}
QList<MessageComponent> MessageContentModel::addLinkPreviews(QList<MessageComponent> inputComponents)
{
int i = 0;
while (i < inputComponents.size()) {
const auto component = inputComponents.at(i);
if (component.type == MessageComponentType::Text || component.type == MessageComponentType::Quote) {
if (LinkPreviewer::hasPreviewableLinks(component.content)) {
const auto links = LinkPreviewer::linkPreviews(component.content);
for (qsizetype j = 0; j < links.size(); ++j) {
const auto linkPreview = linkPreviewComponent(links[j]);
if (!m_removedLinkPreviews.contains(links[j]) && !linkPreview.isEmpty()) {
inputComponents.insert(i + j + 1, linkPreview);
}
};
}
}
});
return MessageComponent{MessageComponentType::LinkPreviewLoad, QString(), {{"link"_L1, link}}};
i++;
}
return inputComponents;
}
void MessageContentModel::closeLinkPreview(int row)

View File

@@ -9,7 +9,6 @@
#include <Quotient/events/roomevent.h>
#include "enums/messagecomponenttype.h"
#include "linkpreviewer.h"
#include "messagecomponent.h"
#include "models/itinerarymodel.h"
#include "models/reactionmodel.h"
@@ -133,35 +132,9 @@ private:
void initializeModel();
void initializeEvent();
void getEvent();
using ComponentIt = QList<MessageComponent>::iterator;
void updateFileInfo();
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);
@@ -174,6 +147,7 @@ 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: i18nc("@action:inmenu Notification 'Default Settings'", "Default Settings")
text: i18n("Follow Global Setting")
icon.name: "globe"
checkable: true
autoExclusive: true
@@ -152,7 +152,7 @@ KirigamiComponents.ConvergentContextMenu {
}
QQC2.Action {
text: i18n("Leave Room")
text: i18n("Leave Room")
icon.name: "go-previous"
onTriggered: {
Qt.createComponent('org.kde.neochat', 'ConfirmLeaveDialog').createObject(root.QQC2.ApplicationWindow.window, {

View File

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

View File

@@ -70,10 +70,8 @@ KirigamiComponents.ConvergentContextMenu {
}
QQC2.Action {
text: i18nc("'Space' is a matrix space", "Leave Space")
text: i18nc("'Space' is a matrix space", "Leave Space")
icon.name: "go-previous"
onTriggered: Qt.createComponent('org.kde.neochat', 'ConfirmLeaveDialog').createObject(root.QQC2.ApplicationWindow.window, {
room: root.room
}).open();
onTriggered: root.room.forget()
}
}

View File

@@ -134,7 +134,6 @@ 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,7 +104,6 @@ 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: i18nc("@action:button", "Deactivate Account")
text: i18n("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,26 +45,6 @@ FormCard.FormCardPage {
}
}
FormCard.FormCard {
Layout.topMargin: Kirigami.Units.largeSpacing
FormCard.AbstractFormDelegate {
contentItem: RowLayout {
spacing: Kirigami.Units.largeSpacing
Kirigami.Icon {
source: "data-information"
width: Kirigami.Units.iconSizes.sizeForLabels
height: Kirigami.Units.iconSizes.sizeForLabels
}
QQC2.Label {
text: i18nc("@info", "These are the default notification settings for all rooms. You can customize notifications per-room in the room list or room settings.")
wrapMode: Text.WordWrap
Layout.fillWidth: true
}
}
}
}
FormCard.FormHeader {
title: i18nc("@title:group", "Room Notifications")
}

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: i18nc("As in the default notification setting", "Default Settings")
text: i18n("Follow global setting")
checked: room.pushNotificationState === PushNotificationState.Default
enabled: room.pushNotificationState !== PushNotificationState.Unknown
onToggled: {

View File

@@ -25,34 +25,13 @@ FormCard.FormCardPage {
title: i18nc("@option:check", "Encryption")
}
FormCard.FormCard {
FormCard.AbstractFormDelegate {
visible: room.usesEncryption
contentItem: RowLayout {
spacing: Kirigami.Units.largeSpacing
Kirigami.Icon {
source: "lock"
width: Kirigami.Units.iconSizes.sizeForLabels
height: Kirigami.Units.iconSizes.sizeForLabels
}
QQC2.Label {
text: i18nc("@info", "This room uses encryption.")
wrapMode: Text.WordWrap
Layout.fillWidth: true
}
}
}
FormCard.FormButtonDelegate {
FormCard.FormSwitchDelegate {
id: enableEncryptionSwitch
icon.name: "lock-symbolic"
text: i18nc("@action:button Enable encryption in this room", "Enable Encryption…")
description: i18nc("@info:description", "Once enabled, encryption cannot be disabled.")
text: i18n("Enable encryption")
description: i18nc("option:check", "Once enabled, encryption cannot be disabled.")
enabled: room.canEncryptRoom
visible: !room.usesEncryption
onClicked: {
checked: room.usesEncryption
onToggled: if (checked) {
let dialog = confirmEncryptionDialog.createObject(QQC2.Overlay.overlay, {
room: room
});

View File

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

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()) {
_private.room.markAllMessagesAsRead();
root.room.markAllMessagesAsRead();
}
}

View File

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

View File

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