Compare commits
1 Commits
work/redst
...
work/redst
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
db2238805b |
@@ -12,6 +12,8 @@ add_library(neochat STATIC
|
||||
models/userdirectorylistmodel.h
|
||||
notificationsmanager.cpp
|
||||
notificationsmanager.h
|
||||
blurhash.cpp
|
||||
blurhash.h
|
||||
blurhashimageprovider.cpp
|
||||
blurhashimageprovider.h
|
||||
windowcontroller.cpp
|
||||
|
||||
198
src/app/blurhash.cpp
Normal file
198
src/app/blurhash.cpp
Normal file
@@ -0,0 +1,198 @@
|
||||
// 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;
|
||||
}
|
||||
28
src/app/blurhash.h
Normal file
28
src/app/blurhash.h
Normal file
@@ -0,0 +1,28 @@
|
||||
// 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);
|
||||
@@ -1,97 +1,31 @@
|
||||
// SPDX-FileCopyrightText: 2024 Joshua Goins <josh@redstrate.com>
|
||||
// SPDX-License-Identifier: MIT
|
||||
// SPDX-FileCopyrightText: 2021 Tobias Fella <tobias.fella@kde.org>
|
||||
// SPDX-License-Identifier: LGPL-2.0-or-later
|
||||
|
||||
#include "blurhashimageprovider.h"
|
||||
|
||||
#include <Quotient/blurhash.h>
|
||||
#include <QImage>
|
||||
#include <QString>
|
||||
|
||||
/*
|
||||
* 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
|
||||
#include "blurhash.h"
|
||||
|
||||
class AsyncImageResponseRunnable : public QObject, public QRunnable
|
||||
BlurhashImageProvider::BlurhashImageProvider()
|
||||
: QQuickImageProvider(QQuickImageProvider::Image)
|
||||
{
|
||||
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);
|
||||
QImage BlurhashImageProvider::requestImage(const QString &id, QSize *size, const QSize &requestedSize)
|
||||
{
|
||||
if (id.isEmpty()) {
|
||||
return QImage();
|
||||
}
|
||||
|
||||
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));
|
||||
*size = requestedSize;
|
||||
if (size->width() == -1) {
|
||||
size->setWidth(256);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
void AsyncImageResponse::handleDone(QImage 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"
|
||||
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;
|
||||
}
|
||||
@@ -1,25 +1,26 @@
|
||||
// SPDX-FileCopyrightText: 2024 Joshua Goins <josh@redstrate.com>
|
||||
// SPDX-License-Identifier: MIT
|
||||
// SPDX-FileCopyrightText: 2021 Tobias Fella <tobias.fella@kde.org>
|
||||
// SPDX-License-Identifier: LGPL-2.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QQuickAsyncImageProvider>
|
||||
#include <QThreadPool>
|
||||
#include <QQuickImageProvider>
|
||||
|
||||
class AsyncImageResponse final : public QQuickImageResponse
|
||||
/**
|
||||
* @class BlurhashImageProvider
|
||||
*
|
||||
* A QQuickImageProvider for blurhashes.
|
||||
*
|
||||
* @sa QQuickImageProvider
|
||||
*/
|
||||
class BlurhashImageProvider : public QQuickImageProvider
|
||||
{
|
||||
public:
|
||||
AsyncImageResponse(const QString &id, const QSize &requestedSize, QThreadPool *pool);
|
||||
void handleDone(QImage image);
|
||||
QQuickTextureFactory *textureFactory() const override;
|
||||
QImage m_image;
|
||||
};
|
||||
BlurhashImageProvider();
|
||||
|
||||
class BlurHashImageProvider : public QQuickAsyncImageProvider
|
||||
{
|
||||
public:
|
||||
QQuickImageResponse *requestImageResponse(const QString &id, const QSize &requestedSize) override;
|
||||
|
||||
private:
|
||||
QThreadPool pool;
|
||||
/**
|
||||
* @brief Return an image for a given ID.
|
||||
*
|
||||
* @sa QQuickImageProvider::requestImage
|
||||
*/
|
||||
QImage requestImage(const QString &id, QSize *size, const QSize &requestedSize) override;
|
||||
};
|
||||
|
||||
@@ -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");
|
||||
|
||||
|
||||
@@ -23,6 +23,11 @@ Kirigami.Dialog {
|
||||
|
||||
property NeoChatConnection connection
|
||||
|
||||
readonly property ProfileFieldsHelper profileFieldsHelper: ProfileFieldsHelper {
|
||||
connection: root.connection
|
||||
userId: root.user.id
|
||||
}
|
||||
|
||||
leftPadding: 0
|
||||
rightPadding: 0
|
||||
topPadding: 0
|
||||
@@ -126,14 +131,38 @@ Kirigami.Dialog {
|
||||
}
|
||||
}
|
||||
|
||||
Kirigami.Chip {
|
||||
visible: root.room
|
||||
text: root.room ? QmlUtils.nameForPowerLevelValue(root.room.memberEffectivePowerLevel(root.user.id)) : ""
|
||||
closable: false
|
||||
checkable: false
|
||||
RowLayout {
|
||||
spacing: Kirigami.Units.smallSpacing
|
||||
|
||||
Layout.leftMargin: Kirigami.Units.largeSpacing
|
||||
Layout.bottomMargin: Kirigami.Units.largeSpacing
|
||||
|
||||
Kirigami.Chip {
|
||||
visible: root.room
|
||||
text: root.room ? QmlUtils.nameForPowerLevelValue(root.room.memberEffectivePowerLevel(root.user.id)) : ""
|
||||
closable: false
|
||||
checkable: false
|
||||
}
|
||||
|
||||
QQC2.BusyIndicator {
|
||||
visible: root.connection.supportsProfileFields && root.profileFieldsHelper.loading
|
||||
}
|
||||
|
||||
Kirigami.Chip {
|
||||
id: timezoneChip
|
||||
visible: root.connection.supportsProfileFields && !root.profileFieldsHelper.loading && root.profileFieldsHelper.timezone.length > 0
|
||||
text: root.profileFieldsHelper.timezone
|
||||
closable: false
|
||||
checkable: false
|
||||
}
|
||||
|
||||
Kirigami.Chip {
|
||||
id: pronounsChip
|
||||
visible: root.connection.supportsProfileFields && !root.profileFieldsHelper.loading && root.profileFieldsHelper.pronouns.length > 0
|
||||
text: root.profileFieldsHelper.pronouns
|
||||
closable: false
|
||||
checkable: false
|
||||
}
|
||||
}
|
||||
|
||||
Kirigami.Separator {
|
||||
|
||||
@@ -22,6 +22,7 @@ target_sources(LibNeoChat PRIVATE
|
||||
texthandler.cpp
|
||||
urlhelper.cpp
|
||||
utils.cpp
|
||||
profilefieldshelper.cpp
|
||||
enums/messagecomponenttype.h
|
||||
enums/messagetype.h
|
||||
enums/powerlevel.cpp
|
||||
@@ -32,6 +33,7 @@ target_sources(LibNeoChat PRIVATE
|
||||
events/imagepackevent.cpp
|
||||
events/pollevent.cpp
|
||||
jobs/neochatgetcommonroomsjob.cpp
|
||||
jobs/neochatprofilefieldjobs.cpp
|
||||
models/actionsmodel.cpp
|
||||
models/completionmodel.cpp
|
||||
models/completionproxymodel.cpp
|
||||
@@ -44,6 +46,8 @@ target_sources(LibNeoChat PRIVATE
|
||||
models/stickermodel.cpp
|
||||
models/userfiltermodel.cpp
|
||||
models/userlistmodel.cpp
|
||||
models/timezonemodel.cpp
|
||||
models/timezonemodel.h
|
||||
)
|
||||
|
||||
ecm_add_qml_module(LibNeoChat GENERATE_PLUGIN_SOURCE
|
||||
|
||||
20
src/libneochat/jobs/neochatprofilefieldjobs.cpp
Normal file
20
src/libneochat/jobs/neochatprofilefieldjobs.cpp
Normal file
@@ -0,0 +1,20 @@
|
||||
// SPDX-FileCopyrightText: 2025 Joshua Goins <josh@redstrate.com>
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#include "neochatprofilefieldjobs.h"
|
||||
|
||||
using namespace Quotient;
|
||||
|
||||
NeoChatGetProfileFieldJob::NeoChatGetProfileFieldJob(const QString &userId, const QString &key)
|
||||
: BaseJob(HttpVerb::Get, u"GetProfileFieldJob"_s, makePath("/_matrix/client/unstable/uk.tcpip.msc4133", "/profile/", userId, "/", key))
|
||||
, m_key(key)
|
||||
{
|
||||
}
|
||||
|
||||
NeoChatSetProfileFieldJob::NeoChatSetProfileFieldJob(const QString &userId, const QString &key, const QString &value)
|
||||
: BaseJob(HttpVerb::Put, u"SetProfileFieldJob"_s, makePath("/_matrix/client/unstable/uk.tcpip.msc4133", "/profile/", userId, "/", key))
|
||||
{
|
||||
QJsonObject _dataJson;
|
||||
addParam(_dataJson, key, value);
|
||||
setRequestData({_dataJson});
|
||||
}
|
||||
49
src/libneochat/jobs/neochatprofilefieldjobs.h
Normal file
49
src/libneochat/jobs/neochatprofilefieldjobs.h
Normal file
@@ -0,0 +1,49 @@
|
||||
// SPDX-FileCopyrightText: 2025 Joshua Goins <josh@redstrate.com>
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <Quotient/jobs/basejob.h>
|
||||
|
||||
// NOTE: This is currently being upstreamed to libQuotient, awaiting MSC4133: https://github.com/quotient-im/libQuotient/pull/869
|
||||
|
||||
//! \brief Get a user's profile field.
|
||||
//!
|
||||
//! Get one of the user's profile fields. This API may be used to fetch the user's
|
||||
//! own profile field or to query the profile field of other users; either locally or
|
||||
//! on remote homeservers.
|
||||
class QUOTIENT_API NeoChatGetProfileFieldJob : public Quotient::BaseJob
|
||||
{
|
||||
public:
|
||||
//! \param userId
|
||||
//! The user whose profile field to query.
|
||||
//! \param key
|
||||
//! The key of the profile field.
|
||||
explicit NeoChatGetProfileFieldJob(const QString &userId, const QString &key);
|
||||
|
||||
// Result properties
|
||||
|
||||
//! The value of the profile field.
|
||||
QString value() const
|
||||
{
|
||||
return loadFromJson<QString>(m_key);
|
||||
}
|
||||
|
||||
private:
|
||||
QString m_key;
|
||||
};
|
||||
|
||||
//! \brief Sets a user's profile field.
|
||||
//!
|
||||
//! Set one of the user's own profile fields. This may fail depending on if the server allows the
|
||||
//! user to change their own profile field, or if the field isn't allowed.
|
||||
class QUOTIENT_API NeoChatSetProfileFieldJob : public Quotient::BaseJob
|
||||
{
|
||||
public:
|
||||
//! \param userId
|
||||
//! The user whose avatar URL to set.
|
||||
//!
|
||||
//! \param avatarUrl
|
||||
//! The new avatar URL for this user.
|
||||
explicit NeoChatSetProfileFieldJob(const QString &userId, const QString &key, const QString &value);
|
||||
};
|
||||
48
src/libneochat/models/timezonemodel.cpp
Normal file
48
src/libneochat/models/timezonemodel.cpp
Normal file
@@ -0,0 +1,48 @@
|
||||
// SPDX-FileCopyrightText: 2025 Joshua Goins <josh@redstrate.com>
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#include "timezonemodel.h"
|
||||
|
||||
#include <KLocalizedString>
|
||||
|
||||
using namespace Qt::Literals::StringLiterals;
|
||||
|
||||
TimeZoneModel::TimeZoneModel(QObject *parent)
|
||||
: QAbstractListModel(parent)
|
||||
{
|
||||
m_timezoneIds = QTimeZone::availableTimeZoneIds();
|
||||
}
|
||||
|
||||
QVariant TimeZoneModel::data(const QModelIndex &index, int role) const
|
||||
{
|
||||
switch (role) {
|
||||
case Qt::DisplayRole: {
|
||||
if (index.row() == 0) {
|
||||
return i18nc("@item:inlistbox Prefer not to say which timezone im in", "Prefer not to say");
|
||||
} else {
|
||||
return m_timezoneIds[index.row() - 1];
|
||||
}
|
||||
}
|
||||
default:
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
int TimeZoneModel::rowCount(const QModelIndex &parent) const
|
||||
{
|
||||
Q_UNUSED(parent);
|
||||
|
||||
return m_timezoneIds.count() + 1;
|
||||
}
|
||||
|
||||
int TimeZoneModel::indexOfValue(const QString &code)
|
||||
{
|
||||
const auto it = std::ranges::find(std::as_const(m_timezoneIds), code.toUtf8());
|
||||
if (it != m_timezoneIds.cend()) {
|
||||
return std::distance(m_timezoneIds.cbegin(), it) + 1;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
#include "moc_timezonemodel.cpp"
|
||||
24
src/libneochat/models/timezonemodel.h
Normal file
24
src/libneochat/models/timezonemodel.h
Normal file
@@ -0,0 +1,24 @@
|
||||
// SPDX-FileCopyrightText: 2025 Joshua Goins <josh@redstrate.com>
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QAbstractListModel>
|
||||
#include <QQmlEngine>
|
||||
|
||||
class TimeZoneModel : public QAbstractListModel
|
||||
{
|
||||
Q_OBJECT
|
||||
QML_ELEMENT
|
||||
|
||||
public:
|
||||
explicit TimeZoneModel(QObject *parent = nullptr);
|
||||
|
||||
[[nodiscard]] QVariant data(const QModelIndex &index, int role) const override;
|
||||
[[nodiscard]] int rowCount(const QModelIndex &parent) const override;
|
||||
|
||||
Q_INVOKABLE int indexOfValue(const QString &code);
|
||||
|
||||
private:
|
||||
QList<QByteArray> m_timezoneIds;
|
||||
};
|
||||
@@ -6,6 +6,7 @@
|
||||
#include <QImageReader>
|
||||
#include <QJsonDocument>
|
||||
|
||||
#include "jobs/neochatprofilefieldjobs.h"
|
||||
#include "neochatroom.h"
|
||||
#include "spacehierarchycache.h"
|
||||
|
||||
@@ -139,6 +140,19 @@ void NeoChatConnection::connectSignals()
|
||||
Q_EMIT canCheckMutualRoomsChanged();
|
||||
m_canEraseData = job->unstableFeatures().contains("org.matrix.msc4025"_L1) || job->versions().count("v1.10"_L1);
|
||||
Q_EMIT canEraseDataChanged();
|
||||
m_supportsProfileFields = job->unstableFeatures().contains("uk.tcpip.msc4133"_L1);
|
||||
Q_EMIT supportsProfileFieldsChanged();
|
||||
|
||||
if (m_supportsProfileFields) {
|
||||
callApi<NeoChatGetProfileFieldJob>(BackgroundRequest, userId(), QStringLiteral("us.cloke.msc4175.tz")).then([this](const auto &job) {
|
||||
m_timezone = job->value();
|
||||
Q_EMIT timezoneChanged();
|
||||
});
|
||||
callApi<NeoChatGetProfileFieldJob>(BackgroundRequest, userId(), QStringLiteral("io.fsky.nyx.pronouns")).then([this](const auto &job) {
|
||||
m_pronouns = job->value();
|
||||
Q_EMIT pronounsChanged();
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
Qt::SingleShotConnection);
|
||||
@@ -558,4 +572,33 @@ bool NeoChatConnection::enablePushNotifications() const
|
||||
return m_pushNotificationsEnabled;
|
||||
}
|
||||
|
||||
bool NeoChatConnection::supportsProfileFields() const
|
||||
{
|
||||
return m_supportsProfileFields;
|
||||
}
|
||||
|
||||
QString NeoChatConnection::timezone() const
|
||||
{
|
||||
return m_timezone;
|
||||
}
|
||||
|
||||
void NeoChatConnection::setTimezone(const QString &value)
|
||||
{
|
||||
callApi<NeoChatSetProfileFieldJob>(BackgroundRequest, userId(), QStringLiteral("us.cloke.msc4175.tz"), value);
|
||||
}
|
||||
|
||||
QString NeoChatConnection::pronouns() const
|
||||
{
|
||||
return m_pronouns;
|
||||
}
|
||||
|
||||
void NeoChatConnection::setPronouns(const QString &value)
|
||||
{
|
||||
const QJsonObject pronounsObj{{"summary"_L1, value}};
|
||||
callApi<NeoChatSetProfileFieldJob>(BackgroundRequest,
|
||||
userId(),
|
||||
QStringLiteral("io.fsky.nyx.pronouns"),
|
||||
QString::fromUtf8(QJsonDocument(pronounsObj).toJson(QJsonDocument::Compact)));
|
||||
}
|
||||
|
||||
#include "moc_neochatconnection.cpp"
|
||||
|
||||
@@ -90,6 +90,21 @@ class NeoChatConnection : public Quotient::Connection
|
||||
*/
|
||||
Q_PROPERTY(bool enablePushNotifications READ enablePushNotifications NOTIFY enablePushNotificationsChanged)
|
||||
|
||||
/**
|
||||
* @brief If the server supports profile fields (MSC4133)
|
||||
*/
|
||||
Q_PROPERTY(bool supportsProfileFields READ supportsProfileFields NOTIFY supportsProfileFieldsChanged)
|
||||
|
||||
/**
|
||||
* @brief The timezone profile field for this account.
|
||||
*/
|
||||
Q_PROPERTY(QString timezone READ timezone WRITE setTimezone NOTIFY timezoneChanged)
|
||||
|
||||
/**
|
||||
* @brief The pronouns profile field for this account.
|
||||
*/
|
||||
Q_PROPERTY(QString pronouns READ pronouns WRITE setPronouns NOTIFY pronounsChanged)
|
||||
|
||||
public:
|
||||
/**
|
||||
* @brief Defines the status after an attempt to change the password on an account.
|
||||
@@ -204,6 +219,14 @@ public:
|
||||
bool pushNotificationsAvailable() const;
|
||||
bool enablePushNotifications() const;
|
||||
|
||||
bool supportsProfileFields() const;
|
||||
|
||||
QString timezone() const;
|
||||
void setTimezone(const QString &value);
|
||||
|
||||
QString pronouns() const;
|
||||
void setPronouns(const QString &value);
|
||||
|
||||
LinkPreviewer *previewerForLink(const QUrl &link);
|
||||
|
||||
Q_SIGNALS:
|
||||
@@ -221,6 +244,9 @@ Q_SIGNALS:
|
||||
void canCheckMutualRoomsChanged();
|
||||
void canEraseDataChanged();
|
||||
void enablePushNotificationsChanged();
|
||||
void supportsProfileFieldsChanged();
|
||||
void timezoneChanged();
|
||||
void pronounsChanged();
|
||||
|
||||
/**
|
||||
* @brief Request a message be shown to the user of the given type.
|
||||
@@ -250,4 +276,7 @@ private:
|
||||
bool m_canCheckMutualRooms = false;
|
||||
bool m_canEraseData = false;
|
||||
bool m_pushNotificationsEnabled = false;
|
||||
bool m_supportsProfileFields = false;
|
||||
QString m_timezone;
|
||||
QString m_pronouns;
|
||||
};
|
||||
|
||||
@@ -18,7 +18,6 @@
|
||||
#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>
|
||||
@@ -242,7 +241,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(), BlurHash::encode(image));
|
||||
content = new EventContent::ImageContent(url, fileInfo.size(), mime, image.size(), fileInfo.fileName());
|
||||
} else if (mime.name().startsWith("audio/"_L1)) {
|
||||
content = new EventContent::AudioContent(url, fileInfo.size(), mime, fileInfo.fileName());
|
||||
} else if (mime.name().startsWith("video/"_L1)) {
|
||||
|
||||
110
src/libneochat/profilefieldshelper.cpp
Normal file
110
src/libneochat/profilefieldshelper.cpp
Normal file
@@ -0,0 +1,110 @@
|
||||
// SPDX-FileCopyrightText: 2025 Joshua Goins <josh@redstrate.com>
|
||||
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||
|
||||
#include "profilefieldshelper.h"
|
||||
#include "jobs/neochatprofilefieldjobs.h"
|
||||
#include "neochatconnection.h"
|
||||
|
||||
#include <Quotient/csapi/profile.h>
|
||||
|
||||
using namespace Quotient;
|
||||
|
||||
NeoChatConnection *ProfileFieldsHelper::connection() const
|
||||
{
|
||||
return m_connection.get();
|
||||
}
|
||||
|
||||
void ProfileFieldsHelper::setConnection(NeoChatConnection *connection)
|
||||
{
|
||||
if (m_connection != connection) {
|
||||
m_connection = connection;
|
||||
Q_EMIT connectionChanged();
|
||||
load();
|
||||
}
|
||||
}
|
||||
|
||||
QString ProfileFieldsHelper::userId() const
|
||||
{
|
||||
return m_userId;
|
||||
}
|
||||
|
||||
void ProfileFieldsHelper::setUserId(const QString &id)
|
||||
{
|
||||
if (m_userId != id) {
|
||||
m_userId = id;
|
||||
Q_EMIT userIdChanged();
|
||||
load();
|
||||
}
|
||||
}
|
||||
|
||||
QString ProfileFieldsHelper::timezone() const
|
||||
{
|
||||
if (m_timezone.isEmpty()) {
|
||||
return {};
|
||||
}
|
||||
return QTimeZone(m_timezone.toUtf8()).displayName(QTimeZone::GenericTime);
|
||||
}
|
||||
|
||||
QString ProfileFieldsHelper::pronouns() const
|
||||
{
|
||||
return m_pronouns;
|
||||
}
|
||||
|
||||
bool ProfileFieldsHelper::loading() const
|
||||
{
|
||||
return m_loading;
|
||||
}
|
||||
|
||||
void ProfileFieldsHelper::load()
|
||||
{
|
||||
if (!m_connection || m_userId.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!m_connection->supportsProfileFields()) {
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
|
||||
m_connection->callApi<NeoChatGetProfileFieldJob>(BackgroundRequest, m_userId, QStringLiteral("us.cloke.msc4175.tz"))
|
||||
.then(
|
||||
[this](const auto &job) {
|
||||
m_timezone = job->value();
|
||||
Q_EMIT timezoneChanged();
|
||||
m_fetchedTimezone = true;
|
||||
checkIfFinished();
|
||||
},
|
||||
[this] {
|
||||
m_fetchedTimezone = true;
|
||||
checkIfFinished();
|
||||
});
|
||||
|
||||
m_connection->callApi<NeoChatGetProfileFieldJob>(BackgroundRequest, m_userId, QStringLiteral("io.fsky.nyx.pronouns"))
|
||||
.then(
|
||||
[this](const auto &job) {
|
||||
const QJsonDocument document = QJsonDocument::fromJson(job->value().toUtf8());
|
||||
m_pronouns = document["summary"_L1].toString();
|
||||
Q_EMIT pronounsChanged();
|
||||
m_fetchedPronouns = true;
|
||||
checkIfFinished();
|
||||
},
|
||||
[this] {
|
||||
m_fetchedPronouns = true;
|
||||
checkIfFinished();
|
||||
});
|
||||
}
|
||||
|
||||
void ProfileFieldsHelper::checkIfFinished()
|
||||
{
|
||||
if (m_fetchedTimezone && m_fetchedPronouns) {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
void ProfileFieldsHelper::setLoading(const bool loading)
|
||||
{
|
||||
m_loading = loading;
|
||||
Q_EMIT loadingChanged();
|
||||
}
|
||||
79
src/libneochat/profilefieldshelper.h
Normal file
79
src/libneochat/profilefieldshelper.h
Normal file
@@ -0,0 +1,79 @@
|
||||
// SPDX-FileCopyrightText: 2025 Joshua Goins <josh@redstrate.com>
|
||||
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QObject>
|
||||
#include <QQmlEngine>
|
||||
|
||||
#include <Quotient/jobs/basejob.h>
|
||||
|
||||
class NeoChatConnection;
|
||||
|
||||
/**
|
||||
* @class ProfileFieldsHelper
|
||||
*
|
||||
* This class is designed to help grabbing the profile fields of a user.
|
||||
*/
|
||||
class ProfileFieldsHelper : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
QML_ELEMENT
|
||||
|
||||
/**
|
||||
* @brief The connection to use.
|
||||
*/
|
||||
Q_PROPERTY(NeoChatConnection *connection READ connection WRITE setConnection NOTIFY connectionChanged REQUIRED)
|
||||
|
||||
/**
|
||||
* @brief The id of the user to grab profile fields from.
|
||||
*/
|
||||
Q_PROPERTY(QString userId READ userId WRITE setUserId NOTIFY userIdChanged REQUIRED)
|
||||
|
||||
/**
|
||||
* @brief The timezone field of the user.
|
||||
*/
|
||||
Q_PROPERTY(QString timezone READ timezone NOTIFY timezoneChanged)
|
||||
|
||||
/**
|
||||
* @brief The pronouns field of the user.
|
||||
*/
|
||||
Q_PROPERTY(QString pronouns READ pronouns NOTIFY pronounsChanged)
|
||||
|
||||
/**
|
||||
* @brief If the fields are loading.
|
||||
*/
|
||||
Q_PROPERTY(bool loading READ loading NOTIFY loadingChanged)
|
||||
|
||||
public:
|
||||
[[nodiscard]] NeoChatConnection *connection() const;
|
||||
void setConnection(NeoChatConnection *connection);
|
||||
|
||||
[[nodiscard]] QString userId() const;
|
||||
void setUserId(const QString &id);
|
||||
|
||||
[[nodiscard]] QString timezone() const;
|
||||
[[nodiscard]] QString pronouns() const;
|
||||
|
||||
[[nodiscard]] bool loading() const;
|
||||
|
||||
Q_SIGNALS:
|
||||
void connectionChanged();
|
||||
void userIdChanged();
|
||||
void timezoneChanged();
|
||||
void pronounsChanged();
|
||||
void loadingChanged();
|
||||
|
||||
private:
|
||||
void load();
|
||||
void checkIfFinished();
|
||||
void setLoading(bool loading);
|
||||
|
||||
QPointer<NeoChatConnection> m_connection;
|
||||
QString m_userId;
|
||||
bool m_loading = true;
|
||||
QString m_timezone;
|
||||
bool m_fetchedTimezone = false;
|
||||
QString m_pronouns;
|
||||
bool m_fetchedPronouns = false;
|
||||
};
|
||||
@@ -115,6 +115,39 @@ FormCard.FormCardPage {
|
||||
text: root.connection ? root.connection.label : ""
|
||||
}
|
||||
FormCard.FormDelegateSeparator {}
|
||||
FormCard.FormComboBoxDelegate {
|
||||
id: timezoneLabel
|
||||
|
||||
property string textValue: root.connection ? root.connection.timezone : ""
|
||||
|
||||
visible: root.connection.supportsProfileFields
|
||||
enabled: root.connection.canChangeProfileFields && root.connection.profileFieldAllowed("us.cloke.msc4175.tz")
|
||||
text: i18nc("@label:combobox", "Timezone:")
|
||||
model: TimeZoneModel {}
|
||||
textRole: "display"
|
||||
valueRole: "display"
|
||||
onActivated: index => {
|
||||
// "Prefer not to say" choice clears it.
|
||||
if (index === 0) {
|
||||
textValue = "";
|
||||
return;
|
||||
}
|
||||
|
||||
// Otherwise, set it to the text value which is the IANA identifier
|
||||
textValue = timezoneLabel.currentValue;
|
||||
}
|
||||
Component.onCompleted: currentIndex = model.indexOfValue(textValue)
|
||||
}
|
||||
FormCard.FormDelegateSeparator {}
|
||||
FormCard.FormTextFieldDelegate {
|
||||
id: pronounsLabel
|
||||
visible: root.connection.supportsProfileFields
|
||||
enabled: root.connection.canChangeProfileFields && root.connection.profileFieldAllowed("io.fsky.nyx.pronouns")
|
||||
label: i18nc("@label:textbox", "Pronouns:")
|
||||
placeholderText: i18nc("@placeholder", "she/her")
|
||||
text: root.connection ? root.connection.pronouns : ""
|
||||
}
|
||||
FormCard.FormDelegateSeparator {}
|
||||
FormCard.FormButtonDelegate {
|
||||
text: i18nc("@action:button", "Show QR Code")
|
||||
icon.name: "view-barcode-qr-symbolic"
|
||||
@@ -146,6 +179,12 @@ FormCard.FormCardPage {
|
||||
if (root.connection.label !== accountLabel.text) {
|
||||
root.connection.label = accountLabel.text;
|
||||
}
|
||||
if (root.connection.timezone !== timezoneLabel.textValue) {
|
||||
root.connection.timezone = timezoneLabel.textValue;
|
||||
}
|
||||
if (root.connection.pronouns !== pronounsLabel.text) {
|
||||
root.connection.pronouns = pronounsLabel.text;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user