mirror of
https://github.com/Nheko-Reborn/nheko.git
synced 2024-11-25 20:48:52 +03:00
Merge pull request #475 from LorenDB/htmlFormattedNotifs
Better notifications
This commit is contained in:
commit
f6de66576c
21 changed files with 587 additions and 458 deletions
|
@ -297,6 +297,9 @@ set(SRC_FILES
|
||||||
src/ui/UserProfile.cpp
|
src/ui/UserProfile.cpp
|
||||||
src/ui/RoomSettings.cpp
|
src/ui/RoomSettings.cpp
|
||||||
|
|
||||||
|
# Generic notification stuff
|
||||||
|
src/notifications/Manager.cpp
|
||||||
|
|
||||||
src/AvatarProvider.cpp
|
src/AvatarProvider.cpp
|
||||||
src/BlurhashProvider.cpp
|
src/BlurhashProvider.cpp
|
||||||
src/Cache.cpp
|
src/Cache.cpp
|
||||||
|
@ -557,7 +560,7 @@ set(TRANSLATION_DEPS ${LANG_QRC} ${QRC} ${QM_SRC})
|
||||||
|
|
||||||
if (APPLE)
|
if (APPLE)
|
||||||
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -framework Foundation -framework Cocoa")
|
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -framework Foundation -framework Cocoa")
|
||||||
set(SRC_FILES ${SRC_FILES} src/notifications/ManagerMac.mm src/emoji/MacHelper.mm)
|
set(SRC_FILES ${SRC_FILES} src/notifications/ManagerMac.mm src/notifications/ManagerMac.cpp src/emoji/MacHelper.mm)
|
||||||
if(${CMAKE_VERSION} VERSION_GREATER_EQUAL "3.16.0")
|
if(${CMAKE_VERSION} VERSION_GREATER_EQUAL "3.16.0")
|
||||||
set_source_files_properties( src/notifications/ManagerMac.mm src/emoji/MacHelper.mm PROPERTIES SKIP_PRECOMPILE_HEADERS ON)
|
set_source_files_properties( src/notifications/ManagerMac.mm src/emoji/MacHelper.mm PROPERTIES SKIP_PRECOMPILE_HEADERS ON)
|
||||||
endif()
|
endif()
|
||||||
|
|
|
@ -5,6 +5,7 @@
|
||||||
|
|
||||||
#include <QBuffer>
|
#include <QBuffer>
|
||||||
#include <QPixmapCache>
|
#include <QPixmapCache>
|
||||||
|
#include <QPointer>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
|
|
||||||
|
@ -12,13 +13,14 @@
|
||||||
#include "Cache.h"
|
#include "Cache.h"
|
||||||
#include "Logging.h"
|
#include "Logging.h"
|
||||||
#include "MatrixClient.h"
|
#include "MatrixClient.h"
|
||||||
|
#include "MxcImageProvider.h"
|
||||||
#include "Utils.h"
|
#include "Utils.h"
|
||||||
|
|
||||||
static QPixmapCache avatar_cache;
|
static QPixmapCache avatar_cache;
|
||||||
|
|
||||||
namespace AvatarProvider {
|
namespace AvatarProvider {
|
||||||
void
|
void
|
||||||
resolve(const QString &avatarUrl, int size, QObject *receiver, AvatarCallback callback)
|
resolve(QString avatarUrl, int size, QObject *receiver, AvatarCallback callback)
|
||||||
{
|
{
|
||||||
const auto cacheKey = QString("%1_size_%2").arg(avatarUrl).arg(size);
|
const auto cacheKey = QString("%1_size_%2").arg(avatarUrl).arg(size);
|
||||||
|
|
||||||
|
@ -33,43 +35,31 @@ resolve(const QString &avatarUrl, int size, QObject *receiver, AvatarCallback ca
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto data = cache::image(cacheKey);
|
MxcImageProvider::download(avatarUrl.remove(QStringLiteral("mxc://")),
|
||||||
if (!data.isNull()) {
|
QSize(size, size),
|
||||||
pixmap = QPixmap::fromImage(utils::readImage(&data));
|
[callback, cacheKey, recv = QPointer<QObject>(receiver)](
|
||||||
avatar_cache.insert(cacheKey, pixmap);
|
QString, QSize, QImage img, QString) {
|
||||||
callback(pixmap);
|
if (!recv)
|
||||||
return;
|
return;
|
||||||
}
|
|
||||||
|
|
||||||
auto proxy = std::make_shared<AvatarProxy>();
|
auto proxy = std::make_shared<AvatarProxy>();
|
||||||
QObject::connect(proxy.get(),
|
QObject::connect(proxy.get(),
|
||||||
&AvatarProxy::avatarDownloaded,
|
&AvatarProxy::avatarDownloaded,
|
||||||
receiver,
|
recv,
|
||||||
[callback, cacheKey](QByteArray data) {
|
[callback, cacheKey](QPixmap pm) {
|
||||||
QPixmap pm = QPixmap::fromImage(utils::readImage(&data));
|
if (!pm.isNull())
|
||||||
avatar_cache.insert(cacheKey, pm);
|
avatar_cache.insert(
|
||||||
|
cacheKey, pm);
|
||||||
callback(pm);
|
callback(pm);
|
||||||
});
|
});
|
||||||
|
|
||||||
mtx::http::ThumbOpts opts;
|
if (img.isNull()) {
|
||||||
opts.width = size;
|
emit proxy->avatarDownloaded(QPixmap{});
|
||||||
opts.height = size;
|
return;
|
||||||
opts.mxc_url = avatarUrl.toStdString();
|
|
||||||
|
|
||||||
http::client()->get_thumbnail(
|
|
||||||
opts,
|
|
||||||
[opts, cacheKey, proxy = std::move(proxy)](const std::string &res,
|
|
||||||
mtx::http::RequestErr err) {
|
|
||||||
if (err) {
|
|
||||||
nhlog::net()->warn("failed to download avatar: {} - ({} {})",
|
|
||||||
opts.mxc_url,
|
|
||||||
mtx::errors::to_string(err->matrix_error.errcode),
|
|
||||||
err->matrix_error.error);
|
|
||||||
} else {
|
|
||||||
cache::saveImage(cacheKey.toStdString(), res);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
emit proxy->avatarDownloaded(QByteArray(res.data(), (int)res.size()));
|
auto pm = QPixmap::fromImage(std::move(img));
|
||||||
|
emit proxy->avatarDownloaded(pm);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -80,8 +70,8 @@ resolve(const QString &room_id,
|
||||||
QObject *receiver,
|
QObject *receiver,
|
||||||
AvatarCallback callback)
|
AvatarCallback callback)
|
||||||
{
|
{
|
||||||
const auto avatarUrl = cache::avatarUrl(room_id, user_id);
|
auto avatarUrl = cache::avatarUrl(room_id, user_id);
|
||||||
|
|
||||||
resolve(avatarUrl, size, receiver, callback);
|
resolve(std::move(avatarUrl), size, receiver, callback);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,19 +8,19 @@
|
||||||
#include <QPixmap>
|
#include <QPixmap>
|
||||||
#include <functional>
|
#include <functional>
|
||||||
|
|
||||||
|
using AvatarCallback = std::function<void(QPixmap)>;
|
||||||
|
|
||||||
class AvatarProxy : public QObject
|
class AvatarProxy : public QObject
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
void avatarDownloaded(const QByteArray &data);
|
void avatarDownloaded(QPixmap pm);
|
||||||
};
|
};
|
||||||
|
|
||||||
using AvatarCallback = std::function<void(QPixmap)>;
|
|
||||||
|
|
||||||
namespace AvatarProvider {
|
namespace AvatarProvider {
|
||||||
void
|
void
|
||||||
resolve(const QString &avatarUrl, int size, QObject *receiver, AvatarCallback cb);
|
resolve(QString avatarUrl, int size, QObject *receiver, AvatarCallback cb);
|
||||||
void
|
void
|
||||||
resolve(const QString &room_id,
|
resolve(const QString &room_id,
|
||||||
const QString &user_id,
|
const QString &user_id,
|
||||||
|
|
161
src/Cache.cpp
161
src/Cache.cpp
|
@ -55,9 +55,6 @@ constexpr auto BATCH_SIZE = 100;
|
||||||
//! Format: room_id -> RoomInfo
|
//! Format: room_id -> RoomInfo
|
||||||
constexpr auto ROOMS_DB("rooms");
|
constexpr auto ROOMS_DB("rooms");
|
||||||
constexpr auto INVITES_DB("invites");
|
constexpr auto INVITES_DB("invites");
|
||||||
//! Keeps already downloaded media for reuse.
|
|
||||||
//! Format: matrix_url -> binary data.
|
|
||||||
constexpr auto MEDIA_DB("media");
|
|
||||||
//! Information that must be kept between sync requests.
|
//! Information that must be kept between sync requests.
|
||||||
constexpr auto SYNC_STATE_DB("sync_state");
|
constexpr auto SYNC_STATE_DB("sync_state");
|
||||||
//! Read receipts per room/event.
|
//! Read receipts per room/event.
|
||||||
|
@ -244,7 +241,6 @@ Cache::setup()
|
||||||
syncStateDb_ = lmdb::dbi::open(txn, SYNC_STATE_DB, MDB_CREATE);
|
syncStateDb_ = lmdb::dbi::open(txn, SYNC_STATE_DB, MDB_CREATE);
|
||||||
roomsDb_ = lmdb::dbi::open(txn, ROOMS_DB, MDB_CREATE);
|
roomsDb_ = lmdb::dbi::open(txn, ROOMS_DB, MDB_CREATE);
|
||||||
invitesDb_ = lmdb::dbi::open(txn, INVITES_DB, MDB_CREATE);
|
invitesDb_ = lmdb::dbi::open(txn, INVITES_DB, MDB_CREATE);
|
||||||
mediaDb_ = lmdb::dbi::open(txn, MEDIA_DB, MDB_CREATE);
|
|
||||||
readReceiptsDb_ = lmdb::dbi::open(txn, READ_RECEIPTS_DB, MDB_CREATE);
|
readReceiptsDb_ = lmdb::dbi::open(txn, READ_RECEIPTS_DB, MDB_CREATE);
|
||||||
notificationsDb_ = lmdb::dbi::open(txn, NOTIFICATIONS_DB, MDB_CREATE);
|
notificationsDb_ = lmdb::dbi::open(txn, NOTIFICATIONS_DB, MDB_CREATE);
|
||||||
|
|
||||||
|
@ -700,82 +696,6 @@ Cache::secret(const std::string &name)
|
||||||
return secret.toStdString();
|
return secret.toStdString();
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
|
||||||
// Media Management
|
|
||||||
//
|
|
||||||
|
|
||||||
void
|
|
||||||
Cache::saveImage(const std::string &url, const std::string &img_data)
|
|
||||||
{
|
|
||||||
if (url.empty() || img_data.empty())
|
|
||||||
return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
auto txn = lmdb::txn::begin(env_);
|
|
||||||
|
|
||||||
mediaDb_.put(txn, url, img_data);
|
|
||||||
|
|
||||||
txn.commit();
|
|
||||||
} catch (const lmdb::error &e) {
|
|
||||||
nhlog::db()->critical("saveImage: {}", e.what());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
Cache::saveImage(const QString &url, const QByteArray &image)
|
|
||||||
{
|
|
||||||
saveImage(url.toStdString(), std::string(image.constData(), image.length()));
|
|
||||||
}
|
|
||||||
|
|
||||||
QByteArray
|
|
||||||
Cache::image(lmdb::txn &txn, const std::string &url)
|
|
||||||
{
|
|
||||||
if (url.empty())
|
|
||||||
return QByteArray();
|
|
||||||
|
|
||||||
try {
|
|
||||||
std::string_view image;
|
|
||||||
bool res = mediaDb_.get(txn, url, image);
|
|
||||||
|
|
||||||
if (!res)
|
|
||||||
return QByteArray();
|
|
||||||
|
|
||||||
return QByteArray(image.data(), (int)image.size());
|
|
||||||
} catch (const lmdb::error &e) {
|
|
||||||
nhlog::db()->critical("image: {}, {}", e.what(), url);
|
|
||||||
}
|
|
||||||
|
|
||||||
return QByteArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
QByteArray
|
|
||||||
Cache::image(const QString &url)
|
|
||||||
{
|
|
||||||
if (url.isEmpty())
|
|
||||||
return QByteArray();
|
|
||||||
|
|
||||||
auto key = url.toStdString();
|
|
||||||
|
|
||||||
try {
|
|
||||||
auto txn = lmdb::txn::begin(env_, nullptr, MDB_RDONLY);
|
|
||||||
|
|
||||||
std::string_view image;
|
|
||||||
|
|
||||||
bool res = mediaDb_.get(txn, key, image);
|
|
||||||
|
|
||||||
txn.commit();
|
|
||||||
|
|
||||||
if (!res)
|
|
||||||
return QByteArray();
|
|
||||||
|
|
||||||
return QByteArray(image.data(), (int)image.size());
|
|
||||||
} catch (const lmdb::error &e) {
|
|
||||||
nhlog::db()->critical("image: {} {}", e.what(), url.toStdString());
|
|
||||||
}
|
|
||||||
|
|
||||||
return QByteArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
void
|
||||||
Cache::removeInvite(lmdb::txn &txn, const std::string &room_id)
|
Cache::removeInvite(lmdb::txn &txn, const std::string &room_id)
|
||||||
{
|
{
|
||||||
|
@ -860,7 +780,6 @@ Cache::deleteData()
|
||||||
lmdb::dbi_close(env_, syncStateDb_);
|
lmdb::dbi_close(env_, syncStateDb_);
|
||||||
lmdb::dbi_close(env_, roomsDb_);
|
lmdb::dbi_close(env_, roomsDb_);
|
||||||
lmdb::dbi_close(env_, invitesDb_);
|
lmdb::dbi_close(env_, invitesDb_);
|
||||||
lmdb::dbi_close(env_, mediaDb_);
|
|
||||||
lmdb::dbi_close(env_, readReceiptsDb_);
|
lmdb::dbi_close(env_, readReceiptsDb_);
|
||||||
lmdb::dbi_close(env_, notificationsDb_);
|
lmdb::dbi_close(env_, notificationsDb_);
|
||||||
|
|
||||||
|
@ -2470,50 +2389,6 @@ Cache::getInviteRoomTopic(lmdb::txn &txn, lmdb::dbi &db)
|
||||||
return QString();
|
return QString();
|
||||||
}
|
}
|
||||||
|
|
||||||
QImage
|
|
||||||
Cache::getRoomAvatar(const QString &room_id)
|
|
||||||
{
|
|
||||||
return getRoomAvatar(room_id.toStdString());
|
|
||||||
}
|
|
||||||
|
|
||||||
QImage
|
|
||||||
Cache::getRoomAvatar(const std::string &room_id)
|
|
||||||
{
|
|
||||||
auto txn = lmdb::txn::begin(env_, nullptr, MDB_RDONLY);
|
|
||||||
|
|
||||||
std::string_view response;
|
|
||||||
|
|
||||||
if (!roomsDb_.get(txn, room_id, response)) {
|
|
||||||
txn.commit();
|
|
||||||
return QImage();
|
|
||||||
}
|
|
||||||
|
|
||||||
std::string media_url;
|
|
||||||
|
|
||||||
try {
|
|
||||||
RoomInfo info = json::parse(response);
|
|
||||||
media_url = std::move(info.avatar_url);
|
|
||||||
|
|
||||||
if (media_url.empty()) {
|
|
||||||
txn.commit();
|
|
||||||
return QImage();
|
|
||||||
}
|
|
||||||
} catch (const json::exception &e) {
|
|
||||||
nhlog::db()->warn("failed to parse room info: {}, {}",
|
|
||||||
e.what(),
|
|
||||||
std::string(response.data(), response.size()));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!mediaDb_.get(txn, media_url, response)) {
|
|
||||||
txn.commit();
|
|
||||||
return QImage();
|
|
||||||
}
|
|
||||||
|
|
||||||
txn.commit();
|
|
||||||
|
|
||||||
return QImage::fromData(QByteArray(response.data(), (int)response.size()));
|
|
||||||
}
|
|
||||||
|
|
||||||
std::vector<std::string>
|
std::vector<std::string>
|
||||||
Cache::joinedRooms()
|
Cache::joinedRooms()
|
||||||
{
|
{
|
||||||
|
@ -2615,8 +2490,7 @@ Cache::getMembers(const std::string &room_id, std::size_t startIndex, std::size_
|
||||||
MemberInfo tmp = json::parse(user_data);
|
MemberInfo tmp = json::parse(user_data);
|
||||||
members.emplace_back(
|
members.emplace_back(
|
||||||
RoomMember{QString::fromStdString(std::string(user_id)),
|
RoomMember{QString::fromStdString(std::string(user_id)),
|
||||||
QString::fromStdString(tmp.name),
|
QString::fromStdString(tmp.name)});
|
||||||
QImage::fromData(image(txn, tmp.avatar_url))});
|
|
||||||
} catch (const json::exception &e) {
|
} catch (const json::exception &e) {
|
||||||
nhlog::db()->warn("{}", e.what());
|
nhlog::db()->warn("{}", e.what());
|
||||||
}
|
}
|
||||||
|
@ -4240,18 +4114,6 @@ hasEnoughPowerLevel(const std::vector<mtx::events::EventType> &eventTypes,
|
||||||
return instance_->hasEnoughPowerLevel(eventTypes, room_id, user_id);
|
return instance_->hasEnoughPowerLevel(eventTypes, room_id, user_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
//! Retrieves the saved room avatar.
|
|
||||||
QImage
|
|
||||||
getRoomAvatar(const QString &id)
|
|
||||||
{
|
|
||||||
return instance_->getRoomAvatar(id);
|
|
||||||
}
|
|
||||||
QImage
|
|
||||||
getRoomAvatar(const std::string &id)
|
|
||||||
{
|
|
||||||
return instance_->getRoomAvatar(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
void
|
||||||
updateReadReceipt(lmdb::txn &txn, const std::string &room_id, const Receipts &receipts)
|
updateReadReceipt(lmdb::txn &txn, const std::string &room_id, const Receipts &receipts)
|
||||||
{
|
{
|
||||||
|
@ -4276,27 +4138,6 @@ lastInvisibleEventAfter(const std::string &room_id, std::string_view event_id)
|
||||||
return instance_->lastInvisibleEventAfter(room_id, event_id);
|
return instance_->lastInvisibleEventAfter(room_id, event_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
QByteArray
|
|
||||||
image(const QString &url)
|
|
||||||
{
|
|
||||||
return instance_->image(url);
|
|
||||||
}
|
|
||||||
QByteArray
|
|
||||||
image(lmdb::txn &txn, const std::string &url)
|
|
||||||
{
|
|
||||||
return instance_->image(txn, url);
|
|
||||||
}
|
|
||||||
void
|
|
||||||
saveImage(const std::string &url, const std::string &data)
|
|
||||||
{
|
|
||||||
instance_->saveImage(url, data);
|
|
||||||
}
|
|
||||||
void
|
|
||||||
saveImage(const QString &url, const QByteArray &data)
|
|
||||||
{
|
|
||||||
instance_->saveImage(url, data);
|
|
||||||
}
|
|
||||||
|
|
||||||
RoomInfo
|
RoomInfo
|
||||||
singleRoomInfo(const std::string &room_id)
|
singleRoomInfo(const std::string &room_id)
|
||||||
{
|
{
|
||||||
|
|
22
src/Cache.h
22
src/Cache.h
|
@ -6,8 +6,6 @@
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <QDateTime>
|
#include <QDateTime>
|
||||||
#include <QDir>
|
|
||||||
#include <QImage>
|
|
||||||
#include <QString>
|
#include <QString>
|
||||||
|
|
||||||
#if __has_include(<lmdbxx/lmdb++.h>)
|
#if __has_include(<lmdbxx/lmdb++.h>)
|
||||||
|
@ -135,12 +133,6 @@ hasEnoughPowerLevel(const std::vector<mtx::events::EventType> &eventTypes,
|
||||||
const std::string &room_id,
|
const std::string &room_id,
|
||||||
const std::string &user_id);
|
const std::string &user_id);
|
||||||
|
|
||||||
//! Retrieves the saved room avatar.
|
|
||||||
QImage
|
|
||||||
getRoomAvatar(const QString &id);
|
|
||||||
QImage
|
|
||||||
getRoomAvatar(const std::string &id);
|
|
||||||
|
|
||||||
//! Adds a user to the read list for the given event.
|
//! Adds a user to the read list for the given event.
|
||||||
//!
|
//!
|
||||||
//! There should be only one user id present in a receipt list per room.
|
//! There should be only one user id present in a receipt list per room.
|
||||||
|
@ -162,20 +154,6 @@ getEventIndex(const std::string &room_id, std::string_view event_id);
|
||||||
std::optional<std::pair<uint64_t, std::string>>
|
std::optional<std::pair<uint64_t, std::string>>
|
||||||
lastInvisibleEventAfter(const std::string &room_id, std::string_view event_id);
|
lastInvisibleEventAfter(const std::string &room_id, std::string_view event_id);
|
||||||
|
|
||||||
QByteArray
|
|
||||||
image(const QString &url);
|
|
||||||
QByteArray
|
|
||||||
image(lmdb::txn &txn, const std::string &url);
|
|
||||||
inline QByteArray
|
|
||||||
image(const std::string &url)
|
|
||||||
{
|
|
||||||
return image(QString::fromStdString(url));
|
|
||||||
}
|
|
||||||
void
|
|
||||||
saveImage(const std::string &url, const std::string &data);
|
|
||||||
void
|
|
||||||
saveImage(const QString &url, const QByteArray &data);
|
|
||||||
|
|
||||||
RoomInfo
|
RoomInfo
|
||||||
singleRoomInfo(const std::string &room_id);
|
singleRoomInfo(const std::string &room_id);
|
||||||
std::map<QString, RoomInfo>
|
std::map<QString, RoomInfo>
|
||||||
|
|
|
@ -25,7 +25,6 @@ struct RoomMember
|
||||||
{
|
{
|
||||||
QString user_id;
|
QString user_id;
|
||||||
QString display_name;
|
QString display_name;
|
||||||
QImage avatar;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
//! Used to uniquely identify a list of read receipts.
|
//! Used to uniquely identify a list of read receipts.
|
||||||
|
|
|
@ -118,10 +118,6 @@ public:
|
||||||
const std::string &room_id,
|
const std::string &room_id,
|
||||||
const std::string &user_id);
|
const std::string &user_id);
|
||||||
|
|
||||||
//! Retrieves the saved room avatar.
|
|
||||||
QImage getRoomAvatar(const QString &id);
|
|
||||||
QImage getRoomAvatar(const std::string &id);
|
|
||||||
|
|
||||||
//! Adds a user to the read list for the given event.
|
//! Adds a user to the read list for the given event.
|
||||||
//!
|
//!
|
||||||
//! There should be only one user id present in a receipt list per room.
|
//! There should be only one user id present in a receipt list per room.
|
||||||
|
@ -137,11 +133,6 @@ public:
|
||||||
using UserReceipts = std::multimap<uint64_t, std::string, std::greater<uint64_t>>;
|
using UserReceipts = std::multimap<uint64_t, std::string, std::greater<uint64_t>>;
|
||||||
UserReceipts readReceipts(const QString &event_id, const QString &room_id);
|
UserReceipts readReceipts(const QString &event_id, const QString &room_id);
|
||||||
|
|
||||||
QByteArray image(const QString &url);
|
|
||||||
QByteArray image(lmdb::txn &txn, const std::string &url);
|
|
||||||
void saveImage(const std::string &url, const std::string &data);
|
|
||||||
void saveImage(const QString &url, const QByteArray &data);
|
|
||||||
|
|
||||||
RoomInfo singleRoomInfo(const std::string &room_id);
|
RoomInfo singleRoomInfo(const std::string &room_id);
|
||||||
std::vector<std::string> roomsWithStateUpdates(const mtx::responses::Sync &res);
|
std::vector<std::string> roomsWithStateUpdates(const mtx::responses::Sync &res);
|
||||||
std::vector<std::string> roomsWithTagUpdates(const mtx::responses::Sync &res);
|
std::vector<std::string> roomsWithTagUpdates(const mtx::responses::Sync &res);
|
||||||
|
@ -528,7 +519,6 @@ private:
|
||||||
lmdb::dbi syncStateDb_;
|
lmdb::dbi syncStateDb_;
|
||||||
lmdb::dbi roomsDb_;
|
lmdb::dbi roomsDb_;
|
||||||
lmdb::dbi invitesDb_;
|
lmdb::dbi invitesDb_;
|
||||||
lmdb::dbi mediaDb_;
|
|
||||||
lmdb::dbi readReceiptsDb_;
|
lmdb::dbi readReceiptsDb_;
|
||||||
lmdb::dbi notificationsDb_;
|
lmdb::dbi notificationsDb_;
|
||||||
|
|
||||||
|
|
|
@ -6,6 +6,7 @@
|
||||||
#include "Cache.h"
|
#include "Cache.h"
|
||||||
#include "Logging.h"
|
#include "Logging.h"
|
||||||
#include "MatrixClient.h"
|
#include "MatrixClient.h"
|
||||||
|
#include "MxcImageProvider.h"
|
||||||
#include "Splitter.h"
|
#include "Splitter.h"
|
||||||
#include "UserSettingsPage.h"
|
#include "UserSettingsPage.h"
|
||||||
|
|
||||||
|
@ -253,37 +254,16 @@ CommunitiesList::highlightSelectedCommunity(const QString &community_id)
|
||||||
void
|
void
|
||||||
CommunitiesList::fetchCommunityAvatar(const QString &id, const QString &avatarUrl)
|
CommunitiesList::fetchCommunityAvatar(const QString &id, const QString &avatarUrl)
|
||||||
{
|
{
|
||||||
auto savedImgData = cache::image(avatarUrl);
|
MxcImageProvider::download(
|
||||||
if (!savedImgData.isNull()) {
|
QString(avatarUrl).remove(QStringLiteral("mxc://")),
|
||||||
QPixmap pix;
|
QSize(96, 96),
|
||||||
pix.loadFromData(savedImgData);
|
[this, id](QString, QSize, QImage img, QString) {
|
||||||
emit avatarRetrieved(id, pix);
|
if (img.isNull()) {
|
||||||
|
nhlog::net()->warn("failed to download avatar: {})", id.toStdString());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (avatarUrl.isEmpty())
|
emit avatarRetrieved(id, QPixmap::fromImage(img));
|
||||||
return;
|
|
||||||
|
|
||||||
mtx::http::ThumbOpts opts;
|
|
||||||
opts.mxc_url = avatarUrl.toStdString();
|
|
||||||
http::client()->get_thumbnail(
|
|
||||||
opts, [this, opts, id](const std::string &res, mtx::http::RequestErr err) {
|
|
||||||
if (err) {
|
|
||||||
nhlog::net()->warn("failed to download avatar: {} - ({} {})",
|
|
||||||
opts.mxc_url,
|
|
||||||
mtx::errors::to_string(err->matrix_error.errcode),
|
|
||||||
err->matrix_error.error);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
cache::saveImage(opts.mxc_url, res);
|
|
||||||
|
|
||||||
auto data = QByteArray(res.data(), (int)res.size());
|
|
||||||
|
|
||||||
QPixmap pix;
|
|
||||||
pix.loadFromData(data);
|
|
||||||
|
|
||||||
emit avatarRetrieved(id, pix);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -4,106 +4,201 @@
|
||||||
|
|
||||||
#include "MxcImageProvider.h"
|
#include "MxcImageProvider.h"
|
||||||
|
|
||||||
|
#include <optional>
|
||||||
|
|
||||||
#include <mtxclient/crypto/client.hpp>
|
#include <mtxclient/crypto/client.hpp>
|
||||||
|
|
||||||
#include "Cache.h"
|
#include <QByteArray>
|
||||||
|
#include <QDir>
|
||||||
|
#include <QFileInfo>
|
||||||
|
#include <QStandardPaths>
|
||||||
|
|
||||||
#include "Logging.h"
|
#include "Logging.h"
|
||||||
#include "MatrixClient.h"
|
#include "MatrixClient.h"
|
||||||
#include "Utils.h"
|
#include "Utils.h"
|
||||||
|
|
||||||
|
QHash<QString, mtx::crypto::EncryptedFile> infos;
|
||||||
|
|
||||||
|
QQuickImageResponse *
|
||||||
|
MxcImageProvider::requestImageResponse(const QString &id, const QSize &requestedSize)
|
||||||
|
{
|
||||||
|
MxcImageResponse *response = new MxcImageResponse(id, requestedSize);
|
||||||
|
pool.start(response);
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
MxcImageProvider::addEncryptionInfo(mtx::crypto::EncryptedFile info)
|
||||||
|
{
|
||||||
|
infos.insert(QString::fromStdString(info.url), info);
|
||||||
|
}
|
||||||
void
|
void
|
||||||
MxcImageResponse::run()
|
MxcImageResponse::run()
|
||||||
{
|
{
|
||||||
if (m_requestedSize.isValid() && !m_encryptionInfo) {
|
MxcImageProvider::download(
|
||||||
QString fileName = QString("%1_%2x%3_crop")
|
m_id, m_requestedSize, [this](QString, QSize, QImage image, QString) {
|
||||||
.arg(m_id)
|
if (image.isNull()) {
|
||||||
.arg(m_requestedSize.width())
|
m_error = "Failed to download image.";
|
||||||
.arg(m_requestedSize.height());
|
} else {
|
||||||
|
m_image = image;
|
||||||
auto data = cache::image(fileName);
|
}
|
||||||
if (!data.isNull()) {
|
|
||||||
m_image = utils::readImage(&data);
|
|
||||||
|
|
||||||
if (!m_image.isNull()) {
|
|
||||||
m_image = m_image.scaled(
|
|
||||||
m_requestedSize, Qt::KeepAspectRatio, Qt::SmoothTransformation);
|
|
||||||
m_image.setText("mxc url", "mxc://" + m_id);
|
|
||||||
|
|
||||||
if (!m_image.isNull()) {
|
|
||||||
emit finished();
|
emit finished();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
MxcImageProvider::download(const QString &id,
|
||||||
|
const QSize &requestedSize,
|
||||||
|
std::function<void(QString, QSize, QImage, QString)> then)
|
||||||
|
{
|
||||||
|
std::optional<mtx::crypto::EncryptedFile> encryptionInfo;
|
||||||
|
auto temp = infos.find("mxc://" + id);
|
||||||
|
if (temp != infos.end())
|
||||||
|
encryptionInfo = *temp;
|
||||||
|
|
||||||
|
if (requestedSize.isValid() && !encryptionInfo) {
|
||||||
|
QString fileName =
|
||||||
|
QString("%1_%2x%3_crop")
|
||||||
|
.arg(QString::fromUtf8(id.toUtf8().toBase64(QByteArray::Base64UrlEncoding |
|
||||||
|
QByteArray::OmitTrailingEquals)))
|
||||||
|
.arg(requestedSize.width())
|
||||||
|
.arg(requestedSize.height());
|
||||||
|
QFileInfo fileInfo(QStandardPaths::writableLocation(QStandardPaths::CacheLocation) +
|
||||||
|
"/media_cache",
|
||||||
|
fileName);
|
||||||
|
QDir().mkpath(fileInfo.absolutePath());
|
||||||
|
|
||||||
|
if (fileInfo.exists()) {
|
||||||
|
QImage image(fileInfo.absoluteFilePath());
|
||||||
|
if (!image.isNull()) {
|
||||||
|
image = image.scaled(
|
||||||
|
requestedSize, Qt::KeepAspectRatio, Qt::SmoothTransformation);
|
||||||
|
|
||||||
|
if (!image.isNull()) {
|
||||||
|
then(id, requestedSize, image, fileInfo.absoluteFilePath());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
mtx::http::ThumbOpts opts;
|
mtx::http::ThumbOpts opts;
|
||||||
opts.mxc_url = "mxc://" + m_id.toStdString();
|
opts.mxc_url = "mxc://" + id.toStdString();
|
||||||
opts.width = m_requestedSize.width() > 0 ? m_requestedSize.width() : -1;
|
opts.width = requestedSize.width() > 0 ? requestedSize.width() : -1;
|
||||||
opts.height = m_requestedSize.height() > 0 ? m_requestedSize.height() : -1;
|
opts.height = requestedSize.height() > 0 ? requestedSize.height() : -1;
|
||||||
opts.method = "crop";
|
opts.method = "crop";
|
||||||
http::client()->get_thumbnail(
|
http::client()->get_thumbnail(
|
||||||
opts, [this, fileName](const std::string &res, mtx::http::RequestErr err) {
|
opts,
|
||||||
|
[fileInfo, requestedSize, then, id](const std::string &res,
|
||||||
|
mtx::http::RequestErr err) {
|
||||||
if (err || res.empty()) {
|
if (err || res.empty()) {
|
||||||
nhlog::net()->error("Failed to download image {}",
|
then(id, QSize(), {}, "");
|
||||||
m_id.toStdString());
|
|
||||||
m_error = "Failed download";
|
|
||||||
emit finished();
|
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto data = QByteArray(res.data(), (int)res.size());
|
auto data = QByteArray(res.data(), (int)res.size());
|
||||||
cache::saveImage(fileName, data);
|
QImage image = utils::readImage(data);
|
||||||
m_image = utils::readImage(&data);
|
if (!image.isNull()) {
|
||||||
if (!m_image.isNull()) {
|
image = image.scaled(
|
||||||
m_image = m_image.scaled(
|
requestedSize, Qt::KeepAspectRatio, Qt::SmoothTransformation);
|
||||||
m_requestedSize, Qt::KeepAspectRatio, Qt::SmoothTransformation);
|
|
||||||
}
|
}
|
||||||
m_image.setText("mxc url", "mxc://" + m_id);
|
image.setText("mxc url", "mxc://" + id);
|
||||||
|
if (image.save(fileInfo.absoluteFilePath(), "png"))
|
||||||
|
nhlog::ui()->debug("Wrote: {}",
|
||||||
|
fileInfo.absoluteFilePath().toStdString());
|
||||||
|
else
|
||||||
|
nhlog::ui()->debug("Failed to write: {}",
|
||||||
|
fileInfo.absoluteFilePath().toStdString());
|
||||||
|
|
||||||
emit finished();
|
then(id, requestedSize, image, fileInfo.absoluteFilePath());
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
auto data = cache::image(m_id);
|
try {
|
||||||
|
QString fileName = QString::fromUtf8(id.toUtf8().toBase64(
|
||||||
|
QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals));
|
||||||
|
QFileInfo fileInfo(
|
||||||
|
QStandardPaths::writableLocation(QStandardPaths::CacheLocation) +
|
||||||
|
"/media_cache",
|
||||||
|
fileName);
|
||||||
|
QDir().mkpath(fileInfo.absolutePath());
|
||||||
|
|
||||||
if (!data.isNull()) {
|
if (fileInfo.exists()) {
|
||||||
m_image = utils::readImage(&data);
|
if (encryptionInfo) {
|
||||||
m_image.setText("mxc url", "mxc://" + m_id);
|
QFile f(fileInfo.absoluteFilePath());
|
||||||
|
f.open(QIODevice::ReadOnly);
|
||||||
|
|
||||||
if (!m_image.isNull()) {
|
QByteArray fileData = f.readAll();
|
||||||
emit finished();
|
auto tempData =
|
||||||
|
mtx::crypto::to_string(mtx::crypto::decrypt_file(
|
||||||
|
fileData.toStdString(), encryptionInfo.value()));
|
||||||
|
auto data =
|
||||||
|
QByteArray(tempData.data(), (int)tempData.size());
|
||||||
|
QImage image = utils::readImage(data);
|
||||||
|
image.setText("mxc url", "mxc://" + id);
|
||||||
|
if (!image.isNull()) {
|
||||||
|
then(id,
|
||||||
|
requestedSize,
|
||||||
|
image,
|
||||||
|
fileInfo.absoluteFilePath());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
QImage image(fileInfo.absoluteFilePath());
|
||||||
|
if (!image.isNull()) {
|
||||||
|
then(id,
|
||||||
|
requestedSize,
|
||||||
|
image,
|
||||||
|
fileInfo.absoluteFilePath());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
http::client()->download(
|
http::client()->download(
|
||||||
"mxc://" + m_id.toStdString(),
|
"mxc://" + id.toStdString(),
|
||||||
[this](const std::string &res,
|
[fileInfo, requestedSize, then, id, encryptionInfo](
|
||||||
|
const std::string &res,
|
||||||
const std::string &,
|
const std::string &,
|
||||||
const std::string &originalFilename,
|
const std::string &originalFilename,
|
||||||
mtx::http::RequestErr err) {
|
mtx::http::RequestErr err) {
|
||||||
if (err) {
|
if (err) {
|
||||||
nhlog::net()->error("Failed to download image {}",
|
then(id, QSize(), {}, "");
|
||||||
m_id.toStdString());
|
|
||||||
m_error = "Failed download";
|
|
||||||
emit finished();
|
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto temp = res;
|
auto tempData = res;
|
||||||
if (m_encryptionInfo)
|
QFile f(fileInfo.absoluteFilePath());
|
||||||
temp = mtx::crypto::to_string(
|
if (!f.open(QIODevice::Truncate | QIODevice::WriteOnly)) {
|
||||||
mtx::crypto::decrypt_file(temp, m_encryptionInfo.value()));
|
then(id, QSize(), {}, "");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
f.write(tempData.data(), tempData.size());
|
||||||
|
f.close();
|
||||||
|
|
||||||
auto data = QByteArray(temp.data(), (int)temp.size());
|
if (encryptionInfo) {
|
||||||
cache::saveImage(m_id, data);
|
tempData =
|
||||||
m_image = utils::readImage(&data);
|
mtx::crypto::to_string(mtx::crypto::decrypt_file(
|
||||||
m_image.setText("original filename",
|
tempData, encryptionInfo.value()));
|
||||||
|
auto data =
|
||||||
|
QByteArray(tempData.data(), (int)tempData.size());
|
||||||
|
QImage image = utils::readImage(data);
|
||||||
|
image.setText("original filename",
|
||||||
QString::fromStdString(originalFilename));
|
QString::fromStdString(originalFilename));
|
||||||
m_image.setText("mxc url", "mxc://" + m_id);
|
image.setText("mxc url", "mxc://" + id);
|
||||||
|
then(
|
||||||
|
id, requestedSize, image, fileInfo.absoluteFilePath());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
emit finished();
|
QImage image(fileInfo.absoluteFilePath());
|
||||||
|
image.setText("original filename",
|
||||||
|
QString::fromStdString(originalFilename));
|
||||||
|
image.setText("mxc url", "mxc://" + id);
|
||||||
|
image.save(fileInfo.absoluteFilePath());
|
||||||
|
then(id, requestedSize, image, fileInfo.absoluteFilePath());
|
||||||
});
|
});
|
||||||
|
} catch (std::exception &e) {
|
||||||
|
nhlog::net()->error("Exception while downloading media: {}", e.what());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,21 +10,18 @@
|
||||||
#include <QImage>
|
#include <QImage>
|
||||||
#include <QThreadPool>
|
#include <QThreadPool>
|
||||||
|
|
||||||
#include <mtx/common.hpp>
|
#include <functional>
|
||||||
|
|
||||||
#include <boost/optional.hpp>
|
#include <mtx/common.hpp>
|
||||||
|
|
||||||
class MxcImageResponse
|
class MxcImageResponse
|
||||||
: public QQuickImageResponse
|
: public QQuickImageResponse
|
||||||
, public QRunnable
|
, public QRunnable
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
MxcImageResponse(const QString &id,
|
MxcImageResponse(const QString &id, const QSize &requestedSize)
|
||||||
const QSize &requestedSize,
|
|
||||||
boost::optional<mtx::crypto::EncryptedFile> encryptionInfo)
|
|
||||||
: m_id(id)
|
: m_id(id)
|
||||||
, m_requestedSize(requestedSize)
|
, m_requestedSize(requestedSize)
|
||||||
, m_encryptionInfo(encryptionInfo)
|
|
||||||
{
|
{
|
||||||
setAutoDelete(false);
|
setAutoDelete(false);
|
||||||
}
|
}
|
||||||
|
@ -40,7 +37,6 @@ public:
|
||||||
QString m_id, m_error;
|
QString m_id, m_error;
|
||||||
QSize m_requestedSize;
|
QSize m_requestedSize;
|
||||||
QImage m_image;
|
QImage m_image;
|
||||||
boost::optional<mtx::crypto::EncryptedFile> m_encryptionInfo;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
class MxcImageProvider
|
class MxcImageProvider
|
||||||
|
@ -50,24 +46,13 @@ class MxcImageProvider
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
public slots:
|
public slots:
|
||||||
QQuickImageResponse *requestImageResponse(const QString &id,
|
QQuickImageResponse *requestImageResponse(const QString &id,
|
||||||
const QSize &requestedSize) override
|
const QSize &requestedSize) override;
|
||||||
{
|
|
||||||
boost::optional<mtx::crypto::EncryptedFile> info;
|
|
||||||
auto temp = infos.find("mxc://" + id);
|
|
||||||
if (temp != infos.end())
|
|
||||||
info = *temp;
|
|
||||||
|
|
||||||
MxcImageResponse *response = new MxcImageResponse(id, requestedSize, info);
|
static void addEncryptionInfo(mtx::crypto::EncryptedFile info);
|
||||||
pool.start(response);
|
static void download(const QString &id,
|
||||||
return response;
|
const QSize &requestedSize,
|
||||||
}
|
std::function<void(QString, QSize, QImage, QString)> then);
|
||||||
|
|
||||||
void addEncryptionInfo(mtx::crypto::EncryptedFile info)
|
|
||||||
{
|
|
||||||
infos.insert(QString::fromStdString(info.url), info);
|
|
||||||
}
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
QThreadPool pool;
|
QThreadPool pool;
|
||||||
QHash<QString, mtx::crypto::EncryptedFile> infos;
|
|
||||||
};
|
};
|
||||||
|
|
|
@ -8,7 +8,6 @@
|
||||||
#include <QMenu>
|
#include <QMenu>
|
||||||
#include <QMouseEvent>
|
#include <QMouseEvent>
|
||||||
#include <QPainter>
|
#include <QPainter>
|
||||||
#include <QSettings>
|
|
||||||
#include <QtGlobal>
|
#include <QtGlobal>
|
||||||
|
|
||||||
#include "AvatarProvider.h"
|
#include "AvatarProvider.h"
|
||||||
|
|
|
@ -24,6 +24,7 @@
|
||||||
|
|
||||||
#include "Cache.h"
|
#include "Cache.h"
|
||||||
#include "Config.h"
|
#include "Config.h"
|
||||||
|
#include "EventAccessors.h"
|
||||||
#include "MatrixClient.h"
|
#include "MatrixClient.h"
|
||||||
#include "UserSettingsPage.h"
|
#include "UserSettingsPage.h"
|
||||||
|
|
||||||
|
@ -50,6 +51,35 @@ createDescriptionInfo(const Event &event, const QString &localUser, const QStrin
|
||||||
ts};
|
ts};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
RelatedInfo
|
||||||
|
utils::stripReplyFallbacks(const TimelineEvent &event, std::string id, QString room_id_)
|
||||||
|
{
|
||||||
|
RelatedInfo related = {};
|
||||||
|
related.quoted_user = QString::fromStdString(mtx::accessors::sender(event));
|
||||||
|
related.related_event = std::move(id);
|
||||||
|
related.type = mtx::accessors::msg_type(event);
|
||||||
|
|
||||||
|
// get body, strip reply fallback, then transform the event to text, if it is a media event
|
||||||
|
// etc
|
||||||
|
related.quoted_body = QString::fromStdString(mtx::accessors::body(event));
|
||||||
|
QRegularExpression plainQuote("^>.*?$\n?", QRegularExpression::MultilineOption);
|
||||||
|
while (related.quoted_body.startsWith(">"))
|
||||||
|
related.quoted_body.remove(plainQuote);
|
||||||
|
if (related.quoted_body.startsWith("\n"))
|
||||||
|
related.quoted_body.remove(0, 1);
|
||||||
|
related.quoted_body = utils::getQuoteBody(related);
|
||||||
|
related.quoted_body.replace("@room", QString::fromUtf8("@\u2060room"));
|
||||||
|
|
||||||
|
// get quoted body and strip reply fallback
|
||||||
|
related.quoted_formatted_body = mtx::accessors::formattedBodyWithFallback(event);
|
||||||
|
related.quoted_formatted_body.remove(QRegularExpression(
|
||||||
|
"<mx-reply>.*</mx-reply>", QRegularExpression::DotMatchesEverythingOption));
|
||||||
|
related.quoted_formatted_body.replace("@room", "@\u2060aroom");
|
||||||
|
related.room = room_id_;
|
||||||
|
|
||||||
|
return related;
|
||||||
|
}
|
||||||
|
|
||||||
QString
|
QString
|
||||||
utils::localUser()
|
utils::localUser()
|
||||||
{
|
{
|
||||||
|
@ -688,11 +718,17 @@ utils::restoreCombobox(QComboBox *combo, const QString &value)
|
||||||
}
|
}
|
||||||
|
|
||||||
QImage
|
QImage
|
||||||
utils::readImage(const QByteArray *data)
|
utils::readImage(const QByteArray &data)
|
||||||
{
|
{
|
||||||
QBuffer buf;
|
QBuffer buf;
|
||||||
buf.setData(*data);
|
buf.setData(data);
|
||||||
QImageReader reader(&buf);
|
QImageReader reader(&buf);
|
||||||
reader.setAutoTransform(true);
|
reader.setAutoTransform(true);
|
||||||
return reader.read();
|
return reader.read();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool
|
||||||
|
utils::isReply(const mtx::events::collections::TimelineEvents &e)
|
||||||
|
{
|
||||||
|
return mtx::accessors::relations(e).reply_to().has_value();
|
||||||
|
}
|
||||||
|
|
|
@ -40,6 +40,9 @@ namespace utils {
|
||||||
|
|
||||||
using TimelineEvent = mtx::events::collections::TimelineEvents;
|
using TimelineEvent = mtx::events::collections::TimelineEvents;
|
||||||
|
|
||||||
|
RelatedInfo
|
||||||
|
stripReplyFallbacks(const TimelineEvent &event, std::string id, QString room_id_);
|
||||||
|
|
||||||
bool
|
bool
|
||||||
codepointIsEmoji(uint code);
|
codepointIsEmoji(uint code);
|
||||||
|
|
||||||
|
@ -309,5 +312,8 @@ restoreCombobox(QComboBox *combo, const QString &value);
|
||||||
|
|
||||||
//! Read image respecting exif orientation
|
//! Read image respecting exif orientation
|
||||||
QImage
|
QImage
|
||||||
readImage(const QByteArray *data);
|
readImage(const QByteArray &data);
|
||||||
|
|
||||||
|
bool
|
||||||
|
isReply(const mtx::events::collections::TimelineEvents &e);
|
||||||
}
|
}
|
||||||
|
|
40
src/notifications/Manager.cpp
Normal file
40
src/notifications/Manager.cpp
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
// SPDX-FileCopyrightText: 2021 Nheko Contributors
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
#include "notifications/Manager.h"
|
||||||
|
|
||||||
|
#include "Cache.h"
|
||||||
|
#include "EventAccessors.h"
|
||||||
|
#include "Utils.h"
|
||||||
|
|
||||||
|
QString
|
||||||
|
NotificationsManager::getMessageTemplate(const mtx::responses::Notification ¬ification)
|
||||||
|
{
|
||||||
|
const auto sender =
|
||||||
|
cache::displayName(QString::fromStdString(notification.room_id),
|
||||||
|
QString::fromStdString(mtx::accessors::sender(notification.event)));
|
||||||
|
|
||||||
|
// TODO: decrypt this message if the decryption setting is on in the UserSettings
|
||||||
|
if (auto msg = std::get_if<mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>>(
|
||||||
|
¬ification.event);
|
||||||
|
msg != nullptr) {
|
||||||
|
return tr("%1 sent an encrypted message").arg(sender);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mtx::accessors::msg_type(notification.event) == mtx::events::MessageType::Emote) {
|
||||||
|
return tr("* %1 %2",
|
||||||
|
"Format an emote message in a notification, %1 is the sender, %2 the "
|
||||||
|
"message")
|
||||||
|
.arg(sender);
|
||||||
|
} else if (utils::isReply(notification.event)) {
|
||||||
|
return tr("%1 replied: %2",
|
||||||
|
"Format a reply in a notification. %1 is the sender, %2 the message")
|
||||||
|
.arg(sender);
|
||||||
|
} else {
|
||||||
|
return tr("%1: %2",
|
||||||
|
"Format a normal message in a notification. %1 is the sender, %2 the "
|
||||||
|
"message")
|
||||||
|
.arg(sender);
|
||||||
|
}
|
||||||
|
}
|
|
@ -10,7 +10,12 @@
|
||||||
|
|
||||||
#include <mtx/responses/notifications.hpp>
|
#include <mtx/responses/notifications.hpp>
|
||||||
|
|
||||||
|
// convenience definition
|
||||||
#if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) || defined(Q_OS_HAIKU)
|
#if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) || defined(Q_OS_HAIKU)
|
||||||
|
#define NHEKO_DBUS_SYS
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#if defined(NHEKO_DBUS_SYS)
|
||||||
#include <QtDBus/QDBusArgument>
|
#include <QtDBus/QDBusArgument>
|
||||||
#include <QtDBus/QDBusInterface>
|
#include <QtDBus/QDBusInterface>
|
||||||
#endif
|
#endif
|
||||||
|
@ -38,20 +43,51 @@ public:
|
||||||
signals:
|
signals:
|
||||||
void notificationClicked(const QString roomId, const QString eventId);
|
void notificationClicked(const QString roomId, const QString eventId);
|
||||||
void sendNotificationReply(const QString roomId, const QString eventId, const QString body);
|
void sendNotificationReply(const QString roomId, const QString eventId, const QString body);
|
||||||
|
void systemPostNotificationCb(const QString &room_id,
|
||||||
|
const QString &event_id,
|
||||||
|
const QString &roomName,
|
||||||
|
const QString &text,
|
||||||
|
const QImage &icon);
|
||||||
|
|
||||||
public slots:
|
public slots:
|
||||||
void removeNotification(const QString &roomId, const QString &eventId);
|
void removeNotification(const QString &roomId, const QString &eventId);
|
||||||
|
|
||||||
#if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) || defined(Q_OS_HAIKU)
|
#if defined(NHEKO_DBUS_SYS)
|
||||||
public:
|
public:
|
||||||
void closeNotifications(QString roomId);
|
void closeNotifications(QString roomId);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
QDBusInterface dbus;
|
QDBusInterface dbus;
|
||||||
|
|
||||||
|
void systemPostNotification(const QString &room_id,
|
||||||
|
const QString &event_id,
|
||||||
|
const QString &roomName,
|
||||||
|
const QString &text,
|
||||||
|
const QImage &icon);
|
||||||
void closeNotification(uint id);
|
void closeNotification(uint id);
|
||||||
|
|
||||||
// notification ID to (room ID, event ID)
|
// notification ID to (room ID, event ID)
|
||||||
QMap<uint, roomEventId> notificationIds;
|
QMap<uint, roomEventId> notificationIds;
|
||||||
|
|
||||||
|
const bool hasMarkup_;
|
||||||
|
const bool hasImages_;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#if defined(Q_OS_MACOS)
|
||||||
|
private:
|
||||||
|
// Objective-C(++) doesn't like to do lots of regular C++, so the actual notification
|
||||||
|
// posting is split out
|
||||||
|
void objCxxPostNotification(const QString &title,
|
||||||
|
const QString &subtitle,
|
||||||
|
const QString &informativeText,
|
||||||
|
const QImage &bodyImage);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#if defined(Q_OS_WINDOWS)
|
||||||
|
private:
|
||||||
|
void systemPostNotification(const QString &line1,
|
||||||
|
const QString &line2,
|
||||||
|
const QString &iconPath);
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
// these slots are platform specific (D-Bus only)
|
// these slots are platform specific (D-Bus only)
|
||||||
|
@ -60,9 +96,12 @@ private slots:
|
||||||
void actionInvoked(uint id, QString action);
|
void actionInvoked(uint id, QString action);
|
||||||
void notificationClosed(uint id, uint reason);
|
void notificationClosed(uint id, uint reason);
|
||||||
void notificationReplied(uint id, QString reply);
|
void notificationReplied(uint id, QString reply);
|
||||||
|
|
||||||
|
private:
|
||||||
|
QString getMessageTemplate(const mtx::responses::Notification ¬ification);
|
||||||
};
|
};
|
||||||
|
|
||||||
#if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) || defined(Q_OS_HAIKU)
|
#if defined(NHEKO_DBUS_SYS)
|
||||||
QDBusArgument &
|
QDBusArgument &
|
||||||
operator<<(QDBusArgument &arg, const QImage &image);
|
operator<<(QDBusArgument &arg, const QImage &image);
|
||||||
const QDBusArgument &
|
const QDBusArgument &
|
||||||
|
|
|
@ -1,3 +1,8 @@
|
||||||
|
// SPDX-FileCopyrightText: 2012 Roland Hieber <rohieb@rohieb.name>
|
||||||
|
// SPDX-FileCopyrightText: 2021 Nheko Contributors
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
#include "notifications/Manager.h"
|
#include "notifications/Manager.h"
|
||||||
|
|
||||||
#include <QDBusConnection>
|
#include <QDBusConnection>
|
||||||
|
@ -7,12 +12,19 @@
|
||||||
#include <QDBusPendingReply>
|
#include <QDBusPendingReply>
|
||||||
#include <QDebug>
|
#include <QDebug>
|
||||||
#include <QImage>
|
#include <QImage>
|
||||||
|
#include <QRegularExpression>
|
||||||
|
#include <QStringBuilder>
|
||||||
|
#include <QTextDocumentFragment>
|
||||||
|
|
||||||
|
#include <functional>
|
||||||
|
#include <variant>
|
||||||
|
|
||||||
|
#include <mtx/responses/notifications.hpp>
|
||||||
|
|
||||||
#include "Cache.h"
|
#include "Cache.h"
|
||||||
#include "EventAccessors.h"
|
#include "EventAccessors.h"
|
||||||
#include "MatrixClient.h"
|
#include "MxcImageProvider.h"
|
||||||
#include "Utils.h"
|
#include "Utils.h"
|
||||||
#include <mtx/responses/notifications.hpp>
|
|
||||||
|
|
||||||
NotificationsManager::NotificationsManager(QObject *parent)
|
NotificationsManager::NotificationsManager(QObject *parent)
|
||||||
: QObject(parent)
|
: QObject(parent)
|
||||||
|
@ -21,6 +33,18 @@ NotificationsManager::NotificationsManager(QObject *parent)
|
||||||
"org.freedesktop.Notifications",
|
"org.freedesktop.Notifications",
|
||||||
QDBusConnection::sessionBus(),
|
QDBusConnection::sessionBus(),
|
||||||
this)
|
this)
|
||||||
|
, hasMarkup_{std::invoke([this]() -> bool {
|
||||||
|
for (auto x : dbus.call("GetCapabilities").arguments())
|
||||||
|
if (x.toStringList().contains("body-markup"))
|
||||||
|
return true;
|
||||||
|
return false;
|
||||||
|
})}
|
||||||
|
, hasImages_{std::invoke([this]() -> bool {
|
||||||
|
for (auto x : dbus.call("GetCapabilities").arguments())
|
||||||
|
if (x.toStringList().contains("body-images"))
|
||||||
|
return true;
|
||||||
|
return false;
|
||||||
|
})}
|
||||||
{
|
{
|
||||||
qDBusRegisterMetaType<QImage>();
|
qDBusRegisterMetaType<QImage>();
|
||||||
|
|
||||||
|
@ -42,12 +66,13 @@ NotificationsManager::NotificationsManager(QObject *parent)
|
||||||
"NotificationReplied",
|
"NotificationReplied",
|
||||||
this,
|
this,
|
||||||
SLOT(notificationReplied(uint, QString)));
|
SLOT(notificationReplied(uint, QString)));
|
||||||
}
|
|
||||||
|
|
||||||
// SPDX-FileCopyrightText: 2012 Roland Hieber <rohieb@rohieb.name>
|
connect(this,
|
||||||
// SPDX-FileCopyrightText: 2021 Nheko Contributors
|
&NotificationsManager::systemPostNotificationCb,
|
||||||
//
|
this,
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
&NotificationsManager::systemPostNotification,
|
||||||
|
Qt::QueuedConnection);
|
||||||
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
NotificationsManager::postNotification(const mtx::responses::Notification ¬ification,
|
NotificationsManager::postNotification(const mtx::responses::Notification ¬ification,
|
||||||
|
@ -55,10 +80,78 @@ NotificationsManager::postNotification(const mtx::responses::Notification ¬if
|
||||||
{
|
{
|
||||||
const auto room_id = QString::fromStdString(notification.room_id);
|
const auto room_id = QString::fromStdString(notification.room_id);
|
||||||
const auto event_id = QString::fromStdString(mtx::accessors::event_id(notification.event));
|
const auto event_id = QString::fromStdString(mtx::accessors::event_id(notification.event));
|
||||||
const auto sender = cache::displayName(
|
const auto room_name =
|
||||||
room_id, QString::fromStdString(mtx::accessors::sender(notification.event)));
|
QString::fromStdString(cache::singleRoomInfo(notification.room_id).name);
|
||||||
const auto text = utils::event_body(notification.event);
|
|
||||||
|
|
||||||
|
auto postNotif = [this, room_id, event_id, room_name, icon](QString text) {
|
||||||
|
emit systemPostNotificationCb(room_id, event_id, room_name, text, icon);
|
||||||
|
};
|
||||||
|
|
||||||
|
QString template_ = getMessageTemplate(notification);
|
||||||
|
// TODO: decrypt this message if the decryption setting is on in the UserSettings
|
||||||
|
if (std::holds_alternative<mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>>(
|
||||||
|
notification.event)) {
|
||||||
|
postNotif(template_);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasMarkup_) {
|
||||||
|
if (hasImages_ && mtx::accessors::msg_type(notification.event) ==
|
||||||
|
mtx::events::MessageType::Image) {
|
||||||
|
MxcImageProvider::download(
|
||||||
|
QString::fromStdString(mtx::accessors::url(notification.event))
|
||||||
|
.remove("mxc://"),
|
||||||
|
QSize(200, 80),
|
||||||
|
[postNotif, notification, template_](
|
||||||
|
QString, QSize, QImage, QString imgPath) {
|
||||||
|
if (imgPath.isEmpty())
|
||||||
|
postNotif(template_
|
||||||
|
.arg(utils::stripReplyFallbacks(
|
||||||
|
notification.event, {}, {})
|
||||||
|
.quoted_formatted_body)
|
||||||
|
.replace("<em>", "<i>")
|
||||||
|
.replace("</em>", "</i>")
|
||||||
|
.replace("<strong>", "<b>")
|
||||||
|
.replace("</strong>", "</b>"));
|
||||||
|
else
|
||||||
|
postNotif(template_.arg(
|
||||||
|
QStringLiteral("<br><img src=\"file:///") % imgPath %
|
||||||
|
"\" alt=\"" %
|
||||||
|
mtx::accessors::formattedBodyWithFallback(
|
||||||
|
notification.event) %
|
||||||
|
"\">"));
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
postNotif(
|
||||||
|
template_
|
||||||
|
.arg(
|
||||||
|
utils::stripReplyFallbacks(notification.event, {}, {}).quoted_formatted_body)
|
||||||
|
.replace("<em>", "<i>")
|
||||||
|
.replace("</em>", "</i>")
|
||||||
|
.replace("<strong>", "<b>")
|
||||||
|
.replace("</strong>", "</b>"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
postNotif(
|
||||||
|
template_.arg(utils::stripReplyFallbacks(notification.event, {}, {}).quoted_body));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This function is based on code from
|
||||||
|
* https://github.com/rohieb/StratumsphereTrayIcon
|
||||||
|
* Copyright (C) 2012 Roland Hieber <rohieb@rohieb.name>
|
||||||
|
* Licensed under the GNU General Public License, version 3
|
||||||
|
*/
|
||||||
|
void
|
||||||
|
NotificationsManager::systemPostNotification(const QString &room_id,
|
||||||
|
const QString &event_id,
|
||||||
|
const QString &roomName,
|
||||||
|
const QString &text,
|
||||||
|
const QImage &icon)
|
||||||
|
{
|
||||||
QVariantMap hints;
|
QVariantMap hints;
|
||||||
hints["image-data"] = icon;
|
hints["image-data"] = icon;
|
||||||
hints["sound-name"] = "message-new-instant";
|
hints["sound-name"] = "message-new-instant";
|
||||||
|
@ -66,14 +159,8 @@ NotificationsManager::postNotification(const mtx::responses::Notification ¬if
|
||||||
argumentList << "nheko"; // app_name
|
argumentList << "nheko"; // app_name
|
||||||
argumentList << (uint)0; // replace_id
|
argumentList << (uint)0; // replace_id
|
||||||
argumentList << ""; // app_icon
|
argumentList << ""; // app_icon
|
||||||
argumentList << QString::fromStdString(
|
argumentList << roomName; // summary
|
||||||
cache::singleRoomInfo(notification.room_id).name); // summary
|
argumentList << text; // body
|
||||||
|
|
||||||
// body
|
|
||||||
if (mtx::accessors::msg_type(notification.event) == mtx::events::MessageType::Emote)
|
|
||||||
argumentList << "* " + sender + " " + text;
|
|
||||||
else
|
|
||||||
argumentList << sender + ": " + text;
|
|
||||||
|
|
||||||
// The list of actions has always the action name and then a localized version of that
|
// The list of actions has always the action name and then a localized version of that
|
||||||
// action. Currently we just use an empty string for that.
|
// action. Currently we just use an empty string for that.
|
||||||
|
@ -84,10 +171,7 @@ NotificationsManager::postNotification(const mtx::responses::Notification ¬if
|
||||||
argumentList << hints; // hints
|
argumentList << hints; // hints
|
||||||
argumentList << (int)-1; // timeout in ms
|
argumentList << (int)-1; // timeout in ms
|
||||||
|
|
||||||
static QDBusInterface notifyApp("org.freedesktop.Notifications",
|
QDBusPendingCall call = dbus.asyncCallWithArgumentList("Notify", argumentList);
|
||||||
"/org/freedesktop/Notifications",
|
|
||||||
"org.freedesktop.Notifications");
|
|
||||||
QDBusPendingCall call = notifyApp.asyncCallWithArgumentList("Notify", argumentList);
|
|
||||||
auto watcher = new QDBusPendingCallWatcher{call, this};
|
auto watcher = new QDBusPendingCallWatcher{call, this};
|
||||||
connect(
|
connect(
|
||||||
watcher, &QDBusPendingCallWatcher::finished, this, [watcher, this, room_id, event_id]() {
|
watcher, &QDBusPendingCallWatcher::finished, this, [watcher, this, room_id, event_id]() {
|
||||||
|
@ -103,10 +187,7 @@ NotificationsManager::postNotification(const mtx::responses::Notification ¬if
|
||||||
void
|
void
|
||||||
NotificationsManager::closeNotification(uint id)
|
NotificationsManager::closeNotification(uint id)
|
||||||
{
|
{
|
||||||
static QDBusInterface closeCall("org.freedesktop.Notifications",
|
auto call = dbus.asyncCall("CloseNotification", (uint)id); // replace_id
|
||||||
"/org/freedesktop/Notifications",
|
|
||||||
"org.freedesktop.Notifications");
|
|
||||||
auto call = closeCall.asyncCall("CloseNotification", (uint)id); // replace_id
|
|
||||||
auto watcher = new QDBusPendingCallWatcher{call, this};
|
auto watcher = new QDBusPendingCallWatcher{call, this};
|
||||||
connect(watcher, &QDBusPendingCallWatcher::finished, this, [watcher]() {
|
connect(watcher, &QDBusPendingCallWatcher::finished, this, [watcher]() {
|
||||||
if (watcher->reply().type() == QDBusMessage::ErrorMessage) {
|
if (watcher->reply().type() == QDBusMessage::ErrorMessage) {
|
||||||
|
|
66
src/notifications/ManagerMac.cpp
Normal file
66
src/notifications/ManagerMac.cpp
Normal file
|
@ -0,0 +1,66 @@
|
||||||
|
// SPDX-FileCopyrightText: 2021 Nheko Contributors
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
#include "Manager.h"
|
||||||
|
|
||||||
|
#include <QRegularExpression>
|
||||||
|
#include <QTextDocumentFragment>
|
||||||
|
|
||||||
|
#include "Cache.h"
|
||||||
|
#include "EventAccessors.h"
|
||||||
|
#include "MxcImageProvider.h"
|
||||||
|
#include "Utils.h"
|
||||||
|
|
||||||
|
#include <mtx/responses/notifications.hpp>
|
||||||
|
|
||||||
|
#include <variant>
|
||||||
|
|
||||||
|
static QString
|
||||||
|
formatNotification(const mtx::responses::Notification ¬ification)
|
||||||
|
{
|
||||||
|
return utils::stripReplyFallbacks(notification.event, {}, {}).quoted_body;
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
NotificationsManager::postNotification(const mtx::responses::Notification ¬ification,
|
||||||
|
const QImage &icon)
|
||||||
|
{
|
||||||
|
Q_UNUSED(icon)
|
||||||
|
|
||||||
|
const auto room_name =
|
||||||
|
QString::fromStdString(cache::singleRoomInfo(notification.room_id).name);
|
||||||
|
const auto sender =
|
||||||
|
cache::displayName(QString::fromStdString(notification.room_id),
|
||||||
|
QString::fromStdString(mtx::accessors::sender(notification.event)));
|
||||||
|
|
||||||
|
const auto isEncrypted =
|
||||||
|
std::get_if<mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>>(
|
||||||
|
¬ification.event) != nullptr;
|
||||||
|
const auto isReply = utils::isReply(notification.event);
|
||||||
|
if (isEncrypted) {
|
||||||
|
// TODO: decrypt this message if the decryption setting is on in the UserSettings
|
||||||
|
const QString messageInfo = (isReply ? tr("%1 replied with an encrypted message")
|
||||||
|
: tr("%1 sent an encrypted message"))
|
||||||
|
.arg(sender);
|
||||||
|
objCxxPostNotification(room_name, messageInfo, "", QImage());
|
||||||
|
} else {
|
||||||
|
const QString messageInfo =
|
||||||
|
(isReply ? tr("%1 replied to a message") : tr("%1 sent a message")).arg(sender);
|
||||||
|
if (mtx::accessors::msg_type(notification.event) == mtx::events::MessageType::Image)
|
||||||
|
MxcImageProvider::download(
|
||||||
|
QString::fromStdString(mtx::accessors::url(notification.event))
|
||||||
|
.remove("mxc://"),
|
||||||
|
QSize(200, 80),
|
||||||
|
[this, notification, room_name, messageInfo](
|
||||||
|
QString, QSize, QImage image, QString) {
|
||||||
|
objCxxPostNotification(room_name,
|
||||||
|
messageInfo,
|
||||||
|
formatNotification(notification),
|
||||||
|
image);
|
||||||
|
});
|
||||||
|
else
|
||||||
|
objCxxPostNotification(
|
||||||
|
room_name, messageInfo, formatNotification(notification), QImage());
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,13 +1,10 @@
|
||||||
#include "notifications/Manager.h"
|
#include "notifications/Manager.h"
|
||||||
|
|
||||||
#include <Foundation/Foundation.h>
|
#import <Foundation/Foundation.h>
|
||||||
#include <QtMac>
|
#import <AppKit/NSImage.h>
|
||||||
|
|
||||||
#include "Cache.h"
|
#include <QtMac>
|
||||||
#include "EventAccessors.h"
|
#include <QImage>
|
||||||
#include "MatrixClient.h"
|
|
||||||
#include "Utils.h"
|
|
||||||
#include <mtx/responses/notifications.hpp>
|
|
||||||
|
|
||||||
@interface NSUserNotification (CFIPrivate)
|
@interface NSUserNotification (CFIPrivate)
|
||||||
- (void)set_identityImage:(NSImage *)image;
|
- (void)set_identityImage:(NSImage *)image;
|
||||||
|
@ -19,24 +16,22 @@ NotificationsManager::NotificationsManager(QObject *parent): QObject(parent)
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
NotificationsManager::postNotification(const mtx::responses::Notification ¬ification,
|
NotificationsManager::objCxxPostNotification(const QString &title,
|
||||||
const QImage &icon)
|
const QString &subtitle,
|
||||||
|
const QString &informativeText,
|
||||||
|
const QImage &bodyImage)
|
||||||
{
|
{
|
||||||
Q_UNUSED(icon);
|
|
||||||
|
|
||||||
const auto sender = cache::displayName(QString::fromStdString(notification.room_id), QString::fromStdString(mtx::accessors::sender(notification.event)));
|
|
||||||
const auto text = utils::event_body(notification.event);
|
|
||||||
|
|
||||||
NSUserNotification *notif = [[NSUserNotification alloc] init];
|
NSUserNotification *notif = [[NSUserNotification alloc] init];
|
||||||
|
|
||||||
notif.title = QString::fromStdString(cache::singleRoomInfo(notification.room_id).name).toNSString();
|
notif.title = title.toNSString();
|
||||||
notif.subtitle = QString("%1 sent a message").arg(sender).toNSString();
|
notif.subtitle = subtitle.toNSString();
|
||||||
if (mtx::accessors::msg_type(notification.event) == mtx::events::MessageType::Emote)
|
notif.informativeText = informativeText.toNSString();
|
||||||
notif.informativeText = QString("* ").append(sender).append(" ").append(text).toNSString();
|
|
||||||
else
|
|
||||||
notif.informativeText = text.toNSString();
|
|
||||||
notif.soundName = NSUserNotificationDefaultSoundName;
|
notif.soundName = NSUserNotificationDefaultSoundName;
|
||||||
|
|
||||||
|
if (!bodyImage.isNull())
|
||||||
|
notif.contentImage = [[NSImage alloc] initWithCGImage: bodyImage.toCGImage() size: NSZeroSize];
|
||||||
|
|
||||||
[[NSUserNotificationCenter defaultUserNotificationCenter] deliverNotification: notif];
|
[[NSUserNotificationCenter defaultUserNotificationCenter] deliverNotification: notif];
|
||||||
[notif autorelease];
|
[notif autorelease];
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,11 +5,15 @@
|
||||||
#include "notifications/Manager.h"
|
#include "notifications/Manager.h"
|
||||||
#include "wintoastlib.h"
|
#include "wintoastlib.h"
|
||||||
|
|
||||||
|
#include <QRegularExpression>
|
||||||
|
#include <QStandardPaths>
|
||||||
|
#include <QTextDocumentFragment>
|
||||||
|
|
||||||
|
#include <variant>
|
||||||
|
|
||||||
#include "Cache.h"
|
#include "Cache.h"
|
||||||
#include "EventAccessors.h"
|
#include "EventAccessors.h"
|
||||||
#include "MatrixClient.h"
|
|
||||||
#include "Utils.h"
|
#include "Utils.h"
|
||||||
#include <mtx/responses/notifications.hpp>
|
|
||||||
|
|
||||||
using namespace WinToastLib;
|
using namespace WinToastLib;
|
||||||
|
|
||||||
|
@ -45,34 +49,57 @@ void
|
||||||
NotificationsManager::postNotification(const mtx::responses::Notification ¬ification,
|
NotificationsManager::postNotification(const mtx::responses::Notification ¬ification,
|
||||||
const QImage &icon)
|
const QImage &icon)
|
||||||
{
|
{
|
||||||
Q_UNUSED(icon)
|
|
||||||
|
|
||||||
const auto room_name =
|
const auto room_name =
|
||||||
QString::fromStdString(cache::singleRoomInfo(notification.room_id).name);
|
QString::fromStdString(cache::singleRoomInfo(notification.room_id).name);
|
||||||
const auto sender =
|
const auto sender =
|
||||||
cache::displayName(QString::fromStdString(notification.room_id),
|
cache::displayName(QString::fromStdString(notification.room_id),
|
||||||
QString::fromStdString(mtx::accessors::sender(notification.event)));
|
QString::fromStdString(mtx::accessors::sender(notification.event)));
|
||||||
const auto text = utils::event_body(notification.event);
|
|
||||||
|
|
||||||
|
const auto isEncrypted =
|
||||||
|
std::get_if<mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>>(
|
||||||
|
¬ification.event) != nullptr;
|
||||||
|
const auto isReply = utils::isReply(notification.event);
|
||||||
|
|
||||||
|
auto formatNotification = [this, notification, sender] {
|
||||||
|
const auto template_ = getMessageTemplate(notification);
|
||||||
|
if (std::holds_alternative<
|
||||||
|
mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>>(
|
||||||
|
notification.event)) {
|
||||||
|
return template_;
|
||||||
|
}
|
||||||
|
|
||||||
|
return template_.arg(
|
||||||
|
utils::stripReplyFallbacks(notification.event, {}, {}).quoted_body);
|
||||||
|
};
|
||||||
|
|
||||||
|
const auto line1 =
|
||||||
|
(room_name == sender) ? sender : QString("%1 - %2").arg(sender).arg(room_name);
|
||||||
|
const auto line2 = (isEncrypted ? (isReply ? tr("%1 replied with an encrypted message")
|
||||||
|
: tr("%1 sent an encrypted message"))
|
||||||
|
: formatNotification());
|
||||||
|
|
||||||
|
auto iconPath = QStandardPaths::writableLocation(QStandardPaths::CacheLocation) +
|
||||||
|
room_name + "-room-avatar.png";
|
||||||
|
if (!icon.save(iconPath))
|
||||||
|
iconPath.clear();
|
||||||
|
|
||||||
|
systemPostNotification(line1, line2, iconPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
NotificationsManager::systemPostNotification(const QString &line1,
|
||||||
|
const QString &line2,
|
||||||
|
const QString &iconPath)
|
||||||
|
{
|
||||||
if (!isInitialized)
|
if (!isInitialized)
|
||||||
init();
|
init();
|
||||||
|
|
||||||
auto templ = WinToastTemplate(WinToastTemplate::ImageAndText02);
|
auto templ = WinToastTemplate(WinToastTemplate::ImageAndText02);
|
||||||
if (room_name != sender)
|
templ.setTextField(line1.toStdWString(), WinToastTemplate::FirstLine);
|
||||||
templ.setTextField(QString("%1 - %2").arg(sender).arg(room_name).toStdWString(),
|
templ.setTextField(line2.toStdWString(), WinToastTemplate::SecondLine);
|
||||||
WinToastTemplate::FirstLine);
|
|
||||||
else
|
if (!iconPath.isNull())
|
||||||
templ.setTextField(QString("%1").arg(sender).toStdWString(),
|
templ.setImagePath(iconPath.toStdWString());
|
||||||
WinToastTemplate::FirstLine);
|
|
||||||
if (mtx::accessors::msg_type(notification.event) == mtx::events::MessageType::Emote)
|
|
||||||
templ.setTextField(
|
|
||||||
QString("* ").append(sender).append(" ").append(text).toStdWString(),
|
|
||||||
WinToastTemplate::SecondLine);
|
|
||||||
else
|
|
||||||
templ.setTextField(QString("%1").arg(text).toStdWString(),
|
|
||||||
WinToastTemplate::SecondLine);
|
|
||||||
// TODO: implement room or user avatar
|
|
||||||
// templ.setImagePath(L"C:/example.png");
|
|
||||||
|
|
||||||
WinToast::instance()->showToast(templ, new CustomHandler());
|
WinToast::instance()->showToast(templ, new CustomHandler());
|
||||||
}
|
}
|
||||||
|
|
|
@ -302,7 +302,9 @@ InputBar::message(QString msg, MarkdownOverride useMarkdown)
|
||||||
|
|
||||||
// NOTE(Nico): rich replies always need a formatted_body!
|
// NOTE(Nico): rich replies always need a formatted_body!
|
||||||
text.format = "org.matrix.custom.html";
|
text.format = "org.matrix.custom.html";
|
||||||
if (ChatPage::instance()->userSettings()->markdown())
|
if ((ChatPage::instance()->userSettings()->markdown() &&
|
||||||
|
useMarkdown == MarkdownOverride::NOT_SPECIFIED) ||
|
||||||
|
useMarkdown == MarkdownOverride::ON)
|
||||||
text.formatted_body =
|
text.formatted_body =
|
||||||
utils::getFormattedQuoteBody(related, utils::markdownToHtml(msg))
|
utils::getFormattedQuoteBody(related, utils::markdownToHtml(msg))
|
||||||
.toStdString();
|
.toStdString();
|
||||||
|
@ -572,7 +574,7 @@ InputBar::showPreview(const QMimeData &source, QString path, const QStringList &
|
||||||
auto mimeClass = mime.split("/")[0];
|
auto mimeClass = mime.split("/")[0];
|
||||||
nhlog::ui()->debug("Mime: {}", mime.toStdString());
|
nhlog::ui()->debug("Mime: {}", mime.toStdString());
|
||||||
if (mimeClass == "image") {
|
if (mimeClass == "image") {
|
||||||
QImage img = utils::readImage(&data);
|
QImage img = utils::readImage(data);
|
||||||
|
|
||||||
dimensions = img.size();
|
dimensions = img.size();
|
||||||
if (img.height() > 200 && img.width() > 360)
|
if (img.height() > 200 && img.width() > 360)
|
||||||
|
|
|
@ -369,7 +369,7 @@ TimelineModel::data(const mtx::events::collections::TimelineEvents &event, int r
|
||||||
|
|
||||||
auto ascent = QFontMetrics(UserSettings::instance()->font()).ascent();
|
auto ascent = QFontMetrics(UserSettings::instance()->font()).ascent();
|
||||||
|
|
||||||
bool isReply = relations(event).reply_to().has_value();
|
bool isReply = utils::isReply(event);
|
||||||
|
|
||||||
auto formattedBody_ = QString::fromStdString(formatted_body(event));
|
auto formattedBody_ = QString::fromStdString(formatted_body(event));
|
||||||
if (formattedBody_.isEmpty()) {
|
if (formattedBody_.isEmpty()) {
|
||||||
|
@ -870,30 +870,7 @@ TimelineModel::relatedInfo(QString id)
|
||||||
if (!event)
|
if (!event)
|
||||||
return {};
|
return {};
|
||||||
|
|
||||||
RelatedInfo related = {};
|
return utils::stripReplyFallbacks(*event, id.toStdString(), room_id_);
|
||||||
related.quoted_user = QString::fromStdString(mtx::accessors::sender(*event));
|
|
||||||
related.related_event = id.toStdString();
|
|
||||||
related.type = mtx::accessors::msg_type(*event);
|
|
||||||
|
|
||||||
// get body, strip reply fallback, then transform the event to text, if it is a media event
|
|
||||||
// etc
|
|
||||||
related.quoted_body = QString::fromStdString(mtx::accessors::body(*event));
|
|
||||||
QRegularExpression plainQuote("^>.*?$\n?", QRegularExpression::MultilineOption);
|
|
||||||
while (related.quoted_body.startsWith(">"))
|
|
||||||
related.quoted_body.remove(plainQuote);
|
|
||||||
if (related.quoted_body.startsWith("\n"))
|
|
||||||
related.quoted_body.remove(0, 1);
|
|
||||||
related.quoted_body = utils::getQuoteBody(related);
|
|
||||||
related.quoted_body.replace("@room", QString::fromUtf8("@\u2060room"));
|
|
||||||
|
|
||||||
// get quoted body and strip reply fallback
|
|
||||||
related.quoted_formatted_body = mtx::accessors::formattedBodyWithFallback(*event);
|
|
||||||
related.quoted_formatted_body.remove(QRegularExpression(
|
|
||||||
"<mx-reply>.*</mx-reply>", QRegularExpression::DotMatchesEverythingOption));
|
|
||||||
related.quoted_formatted_body.replace("@room", "@\u2060aroom");
|
|
||||||
related.room = room_id_;
|
|
||||||
|
|
||||||
return related;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
|
|
Loading…
Reference in a new issue