Compare commits

..

1 Commits

Author SHA1 Message Date
Joshua Goins
7fee8e6c71 Use Quotient's BlurHash implementation, send BlurHashes with images
This adds support for sending blurhashes with images, which need to be
generated client-side. Quotient now supports this and the blurhash
implementation was also moved upstream.
2025-07-08 17:09:10 -04:00
82 changed files with 10960 additions and 13720 deletions

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

@@ -12,8 +12,6 @@ add_library(neochat STATIC
models/userdirectorylistmodel.h
notificationsmanager.cpp
notificationsmanager.h
blurhash.cpp
blurhash.h
blurhashimageprovider.cpp
blurhashimageprovider.h
windowcontroller.cpp

View File

@@ -1,198 +0,0 @@
// SPDX-FileCopyrightText: 2018 Wolt Enterprises
// SPDX-License-Identifier: MIT
#include "blurhash.h"
#include <math.h>
#include <stdbool.h>
#include <stdlib.h>
#include <string.h>
#include <vector>
namespace
{
const char chars[] = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz#$%*+,-.:;=?@[]^_{|}~";
struct Color {
float r = 0;
float g = 0;
float b = 0;
};
inline int linearTosRGB(float value)
{
float v = fmaxf(0, fminf(1, value));
if (v <= 0.0031308)
return v * 12.92 * 255 + 0.5;
else
return (1.055 * powf(v, 1 / 2.4) - 0.055) * 255 + 0.5;
}
inline float sRGBToLinear(int value)
{
float v = (float)value / 255;
if (v <= 0.04045)
return v / 12.92;
else
return powf((v + 0.055) / 1.055, 2.4);
}
inline float signPow(float value, float exp)
{
return copysignf(powf(fabsf(value), exp), value);
}
inline uint8_t clampToUByte(int *src)
{
if (*src >= 0 && *src <= 255) {
return *src;
}
return (*src < 0) ? 0 : 255;
}
inline uint8_t *createByteArray(int size)
{
return (uint8_t *)malloc(size * sizeof(uint8_t));
}
int decodeToInt(const char *string, int start, int end)
{
int value = 0;
for (int iter1 = start; iter1 < end; iter1++) {
int index = -1;
for (int iter2 = 0; iter2 < 83; iter2++) {
if (chars[iter2] == string[iter1]) {
index = iter2;
break;
}
}
if (index == -1) {
return -1;
}
value = value * 83 + index;
}
return value;
}
void decodeDC(int value, Color *color)
{
color->r = sRGBToLinear(value >> 16);
color->g = sRGBToLinear((value >> 8) & 255);
color->b = sRGBToLinear(value & 255);
}
void decodeAC(int value, float maximumValue, Color *color)
{
int quantR = (int)floorf(value / (19 * 19));
int quantG = (int)floorf(value / 19) % 19;
int quantB = (int)value % 19;
color->r = signPow(((float)quantR - 9) / 9, 2.0) * maximumValue;
color->g = signPow(((float)quantG - 9) / 9, 2.0) * maximumValue;
color->b = signPow(((float)quantB - 9) / 9, 2.0) * maximumValue;
}
int decodeToArray(const char *blurhash, int width, int height, int punch, int nChannels, uint8_t *pixelArray)
{
if (!isValidBlurhash(blurhash)) {
return -1;
}
if (punch < 1) {
punch = 1;
}
int sizeFlag = decodeToInt(blurhash, 0, 1);
int numY = (int)floorf(sizeFlag / 9) + 1;
int numX = (sizeFlag % 9) + 1;
int iter = 0;
Color color;
int quantizedMaxValue = decodeToInt(blurhash, 1, 2);
if (quantizedMaxValue == -1) {
return -1;
}
const float maxValue = ((float)(quantizedMaxValue + 1)) / 166;
const int colors_size = numX * numY;
std::vector<Color> colors(colors_size, {0, 0, 0});
for (iter = 0; iter < colors_size; iter++) {
if (iter == 0) {
int value = decodeToInt(blurhash, 2, 6);
if (value == -1) {
return -1;
}
decodeDC(value, &color);
colors[iter] = color;
} else {
int value = decodeToInt(blurhash, 4 + iter * 2, 6 + iter * 2);
if (value == -1) {
return -1;
}
decodeAC(value, maxValue * punch, &color);
colors[iter] = color;
}
}
int bytesPerRow = width * nChannels;
int x = 0, y = 0, i = 0, j = 0;
int intR = 0, intG = 0, intB = 0;
for (y = 0; y < height; y++) {
for (x = 0; x < width; x++) {
float r = 0, g = 0, b = 0;
for (j = 0; j < numY; j++) {
for (i = 0; i < numX; i++) {
float basics = cos((M_PI * x * i) / width) * cos((M_PI * y * j) / height);
int idx = i + j * numX;
r += colors[idx].r * basics;
g += colors[idx].g * basics;
b += colors[idx].b * basics;
}
}
intR = linearTosRGB(r);
intG = linearTosRGB(g);
intB = linearTosRGB(b);
pixelArray[nChannels * x + 0 + y * bytesPerRow] = clampToUByte(&intR);
pixelArray[nChannels * x + 1 + y * bytesPerRow] = clampToUByte(&intG);
pixelArray[nChannels * x + 2 + y * bytesPerRow] = clampToUByte(&intB);
if (nChannels == 4) {
pixelArray[nChannels * x + 3 + y * bytesPerRow] = 255;
}
}
}
return 0;
}
}
uint8_t *decode(const char *blurhash, int width, int height, int punch, int nChannels)
{
int bytesPerRow = width * nChannels;
uint8_t *pixelArray = createByteArray(bytesPerRow * height);
if (decodeToArray(blurhash, width, height, punch, nChannels, pixelArray) == -1) {
return nullptr;
}
return pixelArray;
}
bool isValidBlurhash(const char *blurhash)
{
const int hashLength = strlen(blurhash);
if (!blurhash || strlen(blurhash) < 6) {
return false;
}
int sizeFlag = decodeToInt(blurhash, 0, 1);
int numY = (int)floorf(sizeFlag / 9) + 1;
int numX = (sizeFlag % 9) + 1;
return hashLength == 4 + 2 * numX * numY;
}

View File

@@ -1,28 +0,0 @@
// SPDX-FileCopyrightText: 2018 Wolt Enterprises
// SPDX-License-Identifier: MIT
#pragma once
#include <stdint.h>
/**
* @brief Returns the pixel array of the result image given the blurhash string.
*
* @param blurhash a string representing the blurhash to be decoded.
* @param width the width of the resulting image.
* @param height the height of the resulting image.
* @param punch the factor to improve the contrast, default = 1.
* @param nChannels the number of channels in the resulting image array, 3 = RGB, 4 = RGBA.
*
* @return A pointer to memory region where pixels are stored in (H, W, C) format.
*/
uint8_t *decode(const char *blurhash, int width, int height, int punch, int nChannels);
/**
* @brief Checks if the Blurhash is valid or not.
*
* @param blurhash a string representing the blurhash.
*
* @return A bool (true if it is a valid blurhash, else false).
*/
bool isValidBlurhash(const char *blurhash);

View File

@@ -1,31 +1,97 @@
// SPDX-FileCopyrightText: 2021 Tobias Fella <tobias.fella@kde.org>
// SPDX-License-Identifier: LGPL-2.0-or-later
// SPDX-FileCopyrightText: 2024 Joshua Goins <josh@redstrate.com>
// SPDX-License-Identifier: MIT
#include "blurhashimageprovider.h"
#include <QImage>
#include <QString>
#include <Quotient/blurhash.h>
#include "blurhash.h"
/*
* Qt unfortunately re-encodes the base83 string in QML.
* The only special ASCII characters used in the blurhash base83 string are:
* #$%*+,-.:;=?@[]^_{|}~
* QUrl::fromPercentEncoding is too greedy, and spits out invalid characters
* for parts of valid base83 like %14.
*/
// clang-format off
static const QMap<QLatin1String, QLatin1String> knownEncodings = {
{QLatin1String("%23A"), QLatin1String(":")},
{QLatin1String("%3F"), QLatin1String("?")},
{QLatin1String("%23"), QLatin1String("#")},
{QLatin1String("%5B"), QLatin1String("[")},
{QLatin1String("%5D"), QLatin1String("]")},
{QLatin1String("%40"), QLatin1String("@")},
{QLatin1String("%24"), QLatin1String("$")},
{QLatin1String("%2A"), QLatin1String("*")},
{QLatin1String("%2B"), QLatin1String("+")},
{QLatin1String("%2C"), QLatin1String(",")},
{QLatin1String("%2D"), QLatin1String("-")},
{QLatin1String("%2E"), QLatin1String(".")},
{QLatin1String("%3D"), QLatin1String("=")},
{QLatin1String("%25"), QLatin1String("%")},
{QLatin1String("%5E"), QLatin1String("^")},
{QLatin1String("%7C"), QLatin1String("|")},
{QLatin1String("%7B"), QLatin1String("{")},
{QLatin1String("%7D"), QLatin1String("}")},
{QLatin1String("%7E"), QLatin1String("~")},
};
// clang-format on
BlurhashImageProvider::BlurhashImageProvider()
: QQuickImageProvider(QQuickImageProvider::Image)
class AsyncImageResponseRunnable : public QObject, public QRunnable
{
Q_OBJECT
Q_SIGNALS:
void done(QImage image);
public:
AsyncImageResponseRunnable(const QString &id, const QSize &requestedSize)
: m_id(id)
, m_requestedSize(requestedSize)
{
if (m_requestedSize.width() == -1)
m_requestedSize.setWidth(64);
if (m_requestedSize.height() == -1)
m_requestedSize.setHeight(64);
}
void run() override
{
if (m_id.isEmpty())
return;
QString decodedId = m_id;
for (auto i = knownEncodings.constBegin(); i != knownEncodings.constEnd(); ++i)
decodedId.replace(i.key(), i.value());
Q_EMIT done(Quotient::BlurHash::decode(decodedId, m_requestedSize));
}
private:
QString m_id;
QSize m_requestedSize;
};
AsyncImageResponse::AsyncImageResponse(const QString &id, const QSize &requestedSize, QThreadPool *pool)
{
const auto runnable = new AsyncImageResponseRunnable(id, requestedSize);
connect(runnable, &AsyncImageResponseRunnable::done, this, &AsyncImageResponse::handleDone);
pool->start(runnable);
}
QImage BlurhashImageProvider::requestImage(const QString &id, QSize *size, const QSize &requestedSize)
void AsyncImageResponse::handleDone(QImage image)
{
if (id.isEmpty()) {
return QImage();
}
*size = requestedSize;
if (size->width() == -1) {
size->setWidth(256);
}
if (size->height() == -1) {
size->setHeight(256);
}
auto data = decode(QUrl::fromPercentEncoding(id.toLatin1()).toLatin1().data(), size->width(), size->height(), 1, 3);
QImage image(data, size->width(), size->height(), size->width() * 3, QImage::Format_RGB888, free, data);
return image;
}
m_image = std::move(image);
Q_EMIT finished();
}
QQuickTextureFactory *AsyncImageResponse::textureFactory() const
{
return QQuickTextureFactory::textureFactoryForImage(m_image);
}
QQuickImageResponse *BlurHashImageProvider::requestImageResponse(const QString &id, const QSize &requestedSize)
{
return new AsyncImageResponse(id, requestedSize, &pool);
}
#include "blurhashimageprovider.moc"

View File

@@ -1,26 +1,25 @@
// SPDX-FileCopyrightText: 2021 Tobias Fella <tobias.fella@kde.org>
// SPDX-License-Identifier: LGPL-2.0-or-later
// SPDX-FileCopyrightText: 2024 Joshua Goins <josh@redstrate.com>
// SPDX-License-Identifier: MIT
#pragma once
#include <QQuickImageProvider>
#include <QQuickAsyncImageProvider>
#include <QThreadPool>
/**
* @class BlurhashImageProvider
*
* A QQuickImageProvider for blurhashes.
*
* @sa QQuickImageProvider
*/
class BlurhashImageProvider : public QQuickImageProvider
class AsyncImageResponse final : public QQuickImageResponse
{
public:
BlurhashImageProvider();
/**
* @brief Return an image for a given ID.
*
* @sa QQuickImageProvider::requestImage
*/
QImage requestImage(const QString &id, QSize *size, const QSize &requestedSize) override;
AsyncImageResponse(const QString &id, const QSize &requestedSize, QThreadPool *pool);
void handleDone(QImage image);
QQuickTextureFactory *textureFactory() const override;
QImage m_image;
};
class BlurHashImageProvider : public QQuickAsyncImageProvider
{
public:
QQuickImageResponse *requestImageResponse(const QString &id, const QSize &requestedSize) override;
private:
QThreadPool pool;
};

View File

@@ -292,7 +292,7 @@ int main(int argc, char *argv[])
ShareHandler::instance().setText(parser.value(shareOption));
}
engine.addImageProvider(u"blurhash"_s, new BlurhashImageProvider);
engine.addImageProvider(u"blurhash"_s, new BlurHashImageProvider);
engine.loadFromModule("org.kde.neochat", "Main");

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);
}
@@ -404,9 +397,7 @@ void RoomManager::joinRoom(Quotient::Connection *account, const QString &roomAli
// If no one gives us a homeserver suggestion, try the server specified in the alias/id.
// Otherwise joining a remote room not on our homeserver will fail.
// This is a hack and we're not supposed to do it. With room ids not containing the server going forward, it won't work anymore for new room versions.
// FIXME: Let's keep it around anyway for now, remove it at some point, though
if (vias.empty() && roomAliasOrId.contains(':'_L1)) {
if (vias.empty()) {
vias.append(roomAliasOrId.mid(roomAliasOrId.lastIndexOf(':'_L1) + 1));
}

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

@@ -18,6 +18,7 @@
#include <qcoro/qcorosignal.h>
#include <Quotient/avatar.h>
#include <Quotient/blurhash.h>
#include <Quotient/connection.h>
#include <Quotient/csapi/account-data.h>
#include <Quotient/csapi/directory.h>
@@ -241,7 +242,7 @@ QCoro::Task<void> NeoChatRoom::doUploadFile(QUrl url, QString body, std::optiona
EventContent::FileContentBase *content;
if (mime.name().startsWith("image/"_L1)) {
QImage image(url.toLocalFile());
content = new EventContent::ImageContent(url, fileInfo.size(), mime, image.size(), fileInfo.fileName());
content = new EventContent::ImageContent(url, fileInfo.size(), mime, image.size(), fileInfo.fileName(), BlurHash::encode(image));
} else if (mime.name().startsWith("audio/"_L1)) {
content = new EventContent::AudioContent(url, fileInfo.size(), mime, fileInfo.fileName());
} else if (mime.name().startsWith("video/"_L1)) {
@@ -359,14 +360,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 +442,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 +533,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 +545,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 +1671,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 +1732,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

@@ -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"
@@ -421,8 +422,7 @@ bool MessageContentModel::hasComponentType(MessageComponentType::Type type)
!= m_components.cend();
}
void MessageContentModel::forEachComponentOfType(MessageComponentType::Type type,
std::function<MessageContentModel::ComponentIt(MessageContentModel::ComponentIt)> function)
void MessageContentModel::forEachComponentOfType(MessageComponentType::Type type, std::function<void(const QModelIndex &)> function)
{
auto it = m_components.begin();
while ((it = std::find_if(it,
@@ -431,12 +431,12 @@ void MessageContentModel::forEachComponentOfType(MessageComponentType::Type type
return component.type == type;
}))
!= m_components.end()) {
it = function(it);
function(index(it - m_components.begin()));
++it;
}
}
void MessageContentModel::forEachComponentOfType(QList<MessageComponentType::Type> types,
std::function<MessageContentModel::ComponentIt(MessageContentModel::ComponentIt)> function)
void MessageContentModel::forEachComponentOfType(QList<MessageComponentType::Type> types, std::function<void(const QModelIndex &)> function)
{
for (const auto &type : types) {
forEachComponentOfType(type, function);
@@ -466,10 +466,6 @@ void MessageContentModel::resetModel()
m_components += messageContentComponents();
endResetModel();
if (m_room->urlPreviewEnabled()) {
forEachComponentOfType({MessageComponentType::Text, MessageComponentType::Quote}, m_linkPreviewFunction);
}
updateReplyModel();
updateReactionModel();
}
@@ -489,10 +485,6 @@ void MessageContentModel::resetContent(bool isEditing, bool isThreading)
m_components += newComponents;
endInsertRows();
if (m_room->urlPreviewEnabled()) {
forEachComponentOfType({MessageComponentType::Text, MessageComponentType::Quote}, m_linkPreviewFunction);
}
updateReplyModel();
updateReactionModel();
}
@@ -506,13 +498,27 @@ QList<MessageComponent> MessageContentModel::messageContentComponents(bool isEdi
QList<MessageComponent> newComponents;
const auto roomMessageEvent = eventCast<const Quotient::RoomMessageEvent>(event.first);
if (roomMessageEvent && roomMessageEvent->rawMsgtype() == u"m.key.verification.request"_s) {
newComponents += MessageComponent{MessageComponentType::Verification, QString(), {}};
return newComponents;
}
if (event.first->isRedacted()) {
newComponents += MessageComponent{MessageComponentType::Text, QString(), {}};
return newComponents;
}
if (isEditing) {
newComponents += MessageComponent{MessageComponentType::ChatBar, QString(), {}};
} else {
newComponents.append(componentsForType(MessageComponentType::typeForEvent(*event.first, m_isReply)));
newComponents.append(componentsForType(MessageComponentType::typeForEvent(*event.first)));
}
if (m_room->urlPreviewEnabled()) {
newComponents = addLinkPreviews(newComponents);
}
const auto roomMessageEvent = eventCast<const Quotient::RoomMessageEvent>(event.first);
#if Quotient_VERSION_MINOR > 9 || (Quotient_VERSION_MINOR == 9 && Quotient_VERSION_PATCH > 1)
if (m_threadsEnabled && roomMessageEvent && (roomMessageEvent->isThreaded() || m_room->threads().contains(roomMessageEvent->id()))
&& roomMessageEvent->id() == roomMessageEvent->threadRootEventId()) {
@@ -591,26 +597,22 @@ QList<MessageComponent> MessageContentModel::componentsForType(MessageComponentT
}
switch (type) {
case MessageComponentType::Verification: {
return {MessageComponent{MessageComponentType::Verification, QString(), {}}};
}
case MessageComponentType::Text: {
if (const auto roomMessageEvent = eventCast<const Quotient::RoomMessageEvent>(event.first)) {
return TextHandler().textComponents(EventHandler::rawMessageBody(*roomMessageEvent),
EventHandler::messageBodyInputFormat(*roomMessageEvent),
m_room,
roomMessageEvent,
roomMessageEvent->isReplaced());
} else {
return TextHandler().textComponents(EventHandler::plainBody(m_room, event.first), Qt::TextFormat::PlainText, m_room, event.first, false);
}
const auto roomMessageEvent = eventCast<const Quotient::RoomMessageEvent>(event.first);
return TextHandler().textComponents(EventHandler::rawMessageBody(*roomMessageEvent),
EventHandler::messageBodyInputFormat(*roomMessageEvent),
m_room,
roomMessageEvent,
roomMessageEvent->isReplaced());
auto body = EventHandler::rawMessageBody(*roomMessageEvent);
if (body.trimmed().isEmpty()) {
return TextHandler().textComponents(i18n("<i>This event does not have any content.</i>"),
Qt::TextFormat::RichText,
m_room,
roomMessageEvent,
roomMessageEvent->isReplaced());
} else {
return TextHandler().textComponents(body,
EventHandler::messageBodyInputFormat(*roomMessageEvent),
m_room,
roomMessageEvent,
roomMessageEvent->isReplaced());
}
}
case MessageComponentType::File: {
QList<MessageComponent> components;
@@ -701,20 +703,42 @@ MessageComponent MessageContentModel::linkPreviewComponent(const QUrl &link)
}
if (linkPreviewer->loaded()) {
return MessageComponent{MessageComponentType::LinkPreview, QString(), {{"link"_L1, link}}};
}
connect(linkPreviewer, &LinkPreviewer::loadedChanged, this, [this, link]() {
const auto linkPreviewer = dynamic_cast<NeoChatConnection *>(m_room->connection())->previewerForLink(link);
if (linkPreviewer != nullptr && linkPreviewer->loaded()) {
forEachComponentOfType(MessageComponentType::LinkPreviewLoad, [this, link](ComponentIt it) {
if (it->attributes["link"_L1].toUrl() == link) {
it->type = MessageComponentType::LinkPreview;
Q_EMIT dataChanged(index(it - m_components.begin()), index(it - m_components.begin()), {ComponentTypeRole});
} else {
connect(linkPreviewer, &LinkPreviewer::loadedChanged, this, [this, link]() {
const auto linkPreviewer = dynamic_cast<NeoChatConnection *>(m_room->connection())->previewerForLink(link);
if (linkPreviewer != nullptr && linkPreviewer->loaded()) {
for (auto it = m_components.begin(); it != m_components.end(); it++) {
if (it->attributes["link"_L1].toUrl() == link) {
it->type = MessageComponentType::LinkPreview;
Q_EMIT dataChanged(index(it - m_components.begin()), index(it - m_components.begin()), {ComponentTypeRole});
}
}
return it;
});
}
});
return MessageComponent{MessageComponentType::LinkPreviewLoad, QString(), {{"link"_L1, link}}};
}
}
QList<MessageComponent> MessageContentModel::addLinkPreviews(QList<MessageComponent> inputComponents)
{
int i = 0;
while (i < inputComponents.size()) {
const auto component = inputComponents.at(i);
if (component.type == MessageComponentType::Text || component.type == MessageComponentType::Quote) {
if (LinkPreviewer::hasPreviewableLinks(component.content)) {
const auto links = LinkPreviewer::linkPreviews(component.content);
for (qsizetype j = 0; j < links.size(); ++j) {
const auto linkPreview = linkPreviewComponent(links[j]);
if (!m_removedLinkPreviews.contains(links[j]) && !linkPreview.isEmpty()) {
inputComponents.insert(i + j + 1, linkPreview);
}
};
}
}
});
return MessageComponent{MessageComponentType::LinkPreviewLoad, QString(), {{"link"_L1, link}}};
i++;
}
return inputComponents;
}
void MessageContentModel::closeLinkPreview(int row)

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"
@@ -134,32 +133,13 @@ private:
void initializeEvent();
void getEvent();
using ComponentIt = QList<MessageComponent>::iterator;
QList<MessageComponent> m_components;
bool hasComponentType(MessageComponentType::Type type);
void forEachComponentOfType(MessageComponentType::Type type, std::function<ComponentIt(ComponentIt)> function);
void forEachComponentOfType(QList<MessageComponentType::Type> types, std::function<ComponentIt(ComponentIt)> function);
void forEachComponentOfType(MessageComponentType::Type type, std::function<void(const QModelIndex &)> function);
void forEachComponentOfType(QList<MessageComponentType::Type> types, std::function<void(const QModelIndex &)> function);
std::function<ComponentIt(const ComponentIt &)> m_fileInfoFunction = [this](ComponentIt it) {
Q_EMIT dataChanged(index(it - m_components.begin()), index(it - m_components.begin()), {MessageContentModel::FileTransferInfoRole});
return ++it;
};
std::function<ComponentIt(const ComponentIt &)> m_linkPreviewFunction = [this](ComponentIt it) {
bool previewAdded = false;
if (LinkPreviewer::hasPreviewableLinks(it->content)) {
const auto links = LinkPreviewer::linkPreviews(it->content);
for (qsizetype j = 0; j < links.size(); ++j) {
const auto linkPreview = linkPreviewComponent(links[j]);
if (!m_removedLinkPreviews.contains(links[j]) && !linkPreview.isEmpty()) {
beginInsertRows({}, std::distance(m_components.begin(), it) + j + 1, std::distance(m_components.begin(), it) + j + 1);
it = m_components.insert(it + j + 1, linkPreview);
previewAdded = true;
endInsertRows();
}
};
}
return previewAdded ? it : ++it;
std::function<void(const QModelIndex &)> m_fileInfoFunction = [this](const QModelIndex &index) {
Q_EMIT dataChanged(index, index, {MessageContentModel::FileTransferInfoRole});
};
void resetModel();
@@ -174,6 +154,7 @@ private:
QList<MessageComponent> componentsForType(MessageComponentType::Type type);
MessageComponent linkPreviewComponent(const QUrl &link);
QList<MessageComponent> addLinkPreviews(QList<MessageComponent> inputComponents);
QList<QUrl> m_removedLinkPreviews;

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

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

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