mirror of
https://github.com/Nheko-Reborn/nheko.git
synced 2024-11-22 03:00:46 +03:00
Add support for retrieving the notification events (#33)
This commit is contained in:
parent
b47007d59a
commit
ed9501023a
11 changed files with 192 additions and 26 deletions
|
@ -21,7 +21,7 @@ ExternalProject_Add(
|
||||||
MatrixStructs
|
MatrixStructs
|
||||||
|
|
||||||
GIT_REPOSITORY https://github.com/mujx/matrix-structs
|
GIT_REPOSITORY https://github.com/mujx/matrix-structs
|
||||||
GIT_TAG 690080daa3bc1984297c4d7103cde9ea07e2e0b7
|
GIT_TAG 55a1a5aad0ead3cc45475fc1aed1bf54a56e352c
|
||||||
|
|
||||||
BUILD_IN_SOURCE 1
|
BUILD_IN_SOURCE 1
|
||||||
SOURCE_DIR ${MATRIX_STRUCTS_ROOT}
|
SOURCE_DIR ${MATRIX_STRUCTS_ROOT}
|
||||||
|
|
|
@ -233,6 +233,12 @@ public:
|
||||||
std::vector<RoomSearchResult> searchRooms(const std::string &query,
|
std::vector<RoomSearchResult> searchRooms(const std::string &query,
|
||||||
std::uint8_t max_items = 5);
|
std::uint8_t max_items = 5);
|
||||||
|
|
||||||
|
void markSentNotification(const std::string &event_id);
|
||||||
|
//! Removes an event from the sent notifications.
|
||||||
|
void removeReadNotification(const std::string &event_id);
|
||||||
|
//! Check if we have sent a desktop notification for the given event id.
|
||||||
|
bool isNotificationSent(const std::string &event_id);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
//! Save an invited room.
|
//! Save an invited room.
|
||||||
void saveInvite(lmdb::txn &txn,
|
void saveInvite(lmdb::txn &txn,
|
||||||
|
@ -422,6 +428,7 @@ private:
|
||||||
lmdb::dbi invitesDb_;
|
lmdb::dbi invitesDb_;
|
||||||
lmdb::dbi mediaDb_;
|
lmdb::dbi mediaDb_;
|
||||||
lmdb::dbi readReceiptsDb_;
|
lmdb::dbi readReceiptsDb_;
|
||||||
|
lmdb::dbi notificationsDb_;
|
||||||
|
|
||||||
QString localUserId_;
|
QString localUserId_;
|
||||||
QString cacheDirectory_;
|
QString cacheDirectory_;
|
||||||
|
|
|
@ -136,6 +136,8 @@ private:
|
||||||
|
|
||||||
//! Update the room with the new notification count.
|
//! Update the room with the new notification count.
|
||||||
void updateRoomNotificationCount(const QString &room_id, uint16_t notification_count);
|
void updateRoomNotificationCount(const QString &room_id, uint16_t notification_count);
|
||||||
|
//! Send desktop notification for the received messages.
|
||||||
|
void sendDesktopNotifications(const mtx::responses::Notifications &);
|
||||||
|
|
||||||
QStringList generateTypingUsers(const QString &room_id,
|
QStringList generateTypingUsers(const QString &room_id,
|
||||||
const std::vector<std::string> &typing_users);
|
const std::vector<std::string> &typing_users);
|
||||||
|
|
|
@ -91,6 +91,7 @@ public:
|
||||||
void redactEvent(const QString &room_id, const QString &event_id);
|
void redactEvent(const QString &room_id, const QString &event_id);
|
||||||
void inviteUser(const QString &room_id, const QString &user);
|
void inviteUser(const QString &room_id, const QString &user);
|
||||||
void createRoom(const mtx::requests::CreateRoom &request);
|
void createRoom(const mtx::requests::CreateRoom &request);
|
||||||
|
void getNotifications() noexcept;
|
||||||
|
|
||||||
QUrl getHomeServer() { return server_; };
|
QUrl getHomeServer() { return server_; };
|
||||||
int transactionId() { return txn_id_; };
|
int transactionId() { return txn_id_; };
|
||||||
|
@ -178,6 +179,7 @@ signals:
|
||||||
void redactionCompleted(const QString &room_id, const QString &event_id);
|
void redactionCompleted(const QString &room_id, const QString &event_id);
|
||||||
void invalidToken();
|
void invalidToken();
|
||||||
void syncError(const QString &error);
|
void syncError(const QString &error);
|
||||||
|
void notificationsRetrieved(const mtx::responses::Notifications ¬ifications);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
QNetworkReply *makeUploadRequest(QSharedPointer<QIODevice> iodev);
|
QNetworkReply *makeUploadRequest(QSharedPointer<QIODevice> iodev);
|
||||||
|
|
|
@ -32,6 +32,9 @@ firstChar(const QString &input);
|
||||||
QString
|
QString
|
||||||
humanReadableFileSize(uint64_t bytes);
|
humanReadableFileSize(uint64_t bytes);
|
||||||
|
|
||||||
|
QString
|
||||||
|
event_body(const mtx::events::collections::TimelineEvents &event);
|
||||||
|
|
||||||
//! Match widgets/events with a description message.
|
//! Match widgets/events with a description message.
|
||||||
template<class T>
|
template<class T>
|
||||||
QString
|
QString
|
||||||
|
@ -131,6 +134,37 @@ erase_if(ContainerT &items, const PredicateT &predicate)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
inline mtx::events::EventType
|
||||||
|
event_type(const mtx::events::collections::TimelineEvents &event)
|
||||||
|
{
|
||||||
|
return mpark::visit([](auto msg) { return msg.type; }, event);
|
||||||
|
}
|
||||||
|
|
||||||
|
inline std::string
|
||||||
|
event_id(const mtx::events::collections::TimelineEvents &event)
|
||||||
|
{
|
||||||
|
return mpark::visit([](auto msg) { return msg.event_id; }, event);
|
||||||
|
}
|
||||||
|
|
||||||
|
inline QString
|
||||||
|
eventId(const mtx::events::collections::TimelineEvents &event)
|
||||||
|
{
|
||||||
|
return QString::fromStdString(event_id(event));
|
||||||
|
}
|
||||||
|
|
||||||
|
inline QString
|
||||||
|
event_sender(const mtx::events::collections::TimelineEvents &event)
|
||||||
|
{
|
||||||
|
return mpark::visit([](auto msg) { return QString::fromStdString(msg.sender); }, event);
|
||||||
|
}
|
||||||
|
|
||||||
|
template<class T>
|
||||||
|
QString
|
||||||
|
message_body(const mtx::events::collections::TimelineEvents &event)
|
||||||
|
{
|
||||||
|
return QString::fromStdString(mpark::get<T>(event).content.body);
|
||||||
|
}
|
||||||
|
|
||||||
//! Calculate the Levenshtein distance between two strings with character skipping.
|
//! Calculate the Levenshtein distance between two strings with character skipping.
|
||||||
int
|
int
|
||||||
levenshtein_distance(const std::string &s1, const std::string &s2);
|
levenshtein_distance(const std::string &s1, const std::string &s2);
|
||||||
|
|
|
@ -211,9 +211,6 @@ private:
|
||||||
bool isScrollbarActivated() { return scroll_area_->verticalScrollBar()->value() != 0; }
|
bool isScrollbarActivated() { return scroll_area_->verticalScrollBar()->value() != 0; }
|
||||||
//! Retrieve the event id of the last item.
|
//! Retrieve the event id of the last item.
|
||||||
QString getLastEventId() const;
|
QString getLastEventId() const;
|
||||||
QString getEventSender(const mtx::events::collections::TimelineEvents &event) const;
|
|
||||||
mtx::events::EventType getEventType(
|
|
||||||
const mtx::events::collections::TimelineEvents &event) const;
|
|
||||||
|
|
||||||
template<class Event, class Widget>
|
template<class Event, class Widget>
|
||||||
TimelineItem *processMessageEvent(const Event &event, TimelineDirection direction);
|
TimelineItem *processMessageEvent(const Event &event, TimelineDirection direction);
|
||||||
|
|
45
src/Cache.cc
45
src/Cache.cc
|
@ -48,6 +48,7 @@ static constexpr const char *MEDIA_DB = "media";
|
||||||
static constexpr const char *SYNC_STATE_DB = "sync_state";
|
static constexpr const char *SYNC_STATE_DB = "sync_state";
|
||||||
//! Read receipts per room/event.
|
//! Read receipts per room/event.
|
||||||
static constexpr const char *READ_RECEIPTS_DB = "read_receipts";
|
static constexpr const char *READ_RECEIPTS_DB = "read_receipts";
|
||||||
|
static constexpr const char *NOTIFICATIONS_DB = "sent_notifications";
|
||||||
|
|
||||||
using CachedReceipts = std::multimap<uint64_t, std::string, std::greater<uint64_t>>;
|
using CachedReceipts = std::multimap<uint64_t, std::string, std::greater<uint64_t>>;
|
||||||
using Receipts = std::map<std::string, std::map<std::string, uint64_t>>;
|
using Receipts = std::map<std::string, std::map<std::string, uint64_t>>;
|
||||||
|
@ -60,6 +61,7 @@ Cache::Cache(const QString &userId, QObject *parent)
|
||||||
, invitesDb_{0}
|
, invitesDb_{0}
|
||||||
, mediaDb_{0}
|
, mediaDb_{0}
|
||||||
, readReceiptsDb_{0}
|
, readReceiptsDb_{0}
|
||||||
|
, notificationsDb_{0}
|
||||||
, localUserId_{userId}
|
, localUserId_{userId}
|
||||||
{}
|
{}
|
||||||
|
|
||||||
|
@ -112,12 +114,13 @@ Cache::setup()
|
||||||
env_.open(statePath.toStdString().c_str());
|
env_.open(statePath.toStdString().c_str());
|
||||||
}
|
}
|
||||||
|
|
||||||
auto txn = lmdb::txn::begin(env_);
|
auto txn = lmdb::txn::begin(env_);
|
||||||
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);
|
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);
|
||||||
txn.commit();
|
txn.commit();
|
||||||
|
|
||||||
qRegisterMetaType<RoomInfo>();
|
qRegisterMetaType<RoomInfo>();
|
||||||
|
@ -1087,6 +1090,36 @@ Cache::getMembers(const std::string &room_id, std::size_t startIndex, std::size_
|
||||||
return members;
|
return members;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
Cache::markSentNotification(const std::string &event_id)
|
||||||
|
{
|
||||||
|
auto txn = lmdb::txn::begin(env_);
|
||||||
|
lmdb::dbi_put(txn, notificationsDb_, lmdb::val(event_id), lmdb::val(std::string("")));
|
||||||
|
txn.commit();
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
Cache::removeReadNotification(const std::string &event_id)
|
||||||
|
{
|
||||||
|
auto txn = lmdb::txn::begin(env_);
|
||||||
|
|
||||||
|
lmdb::dbi_del(txn, notificationsDb_, lmdb::val(event_id), nullptr);
|
||||||
|
|
||||||
|
txn.commit();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool
|
||||||
|
Cache::isNotificationSent(const std::string &event_id)
|
||||||
|
{
|
||||||
|
auto txn = lmdb::txn::begin(env_, nullptr, MDB_RDONLY);
|
||||||
|
|
||||||
|
lmdb::val value;
|
||||||
|
bool res = lmdb::dbi_get(txn, notificationsDb_, lmdb::val(event_id), value);
|
||||||
|
txn.commit();
|
||||||
|
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
QHash<QString, QString> Cache::DisplayNames;
|
QHash<QString, QString> Cache::DisplayNames;
|
||||||
QHash<QString, QString> Cache::AvatarUrls;
|
QHash<QString, QString> Cache::AvatarUrls;
|
||||||
|
|
||||||
|
|
|
@ -36,6 +36,7 @@
|
||||||
#include "TypingDisplay.h"
|
#include "TypingDisplay.h"
|
||||||
#include "UserInfoWidget.h"
|
#include "UserInfoWidget.h"
|
||||||
#include "UserSettingsPage.h"
|
#include "UserSettingsPage.h"
|
||||||
|
#include "Utils.h"
|
||||||
|
|
||||||
#include "dialogs/ReadReceipts.h"
|
#include "dialogs/ReadReceipts.h"
|
||||||
#include "timeline/TimelineViewManager.h"
|
#include "timeline/TimelineViewManager.h"
|
||||||
|
@ -339,6 +340,10 @@ ChatPage::ChatPage(QSharedPointer<MatrixClient> client,
|
||||||
connect(client_.data(), &MatrixClient::redactionFailed, this, [this](const QString &error) {
|
connect(client_.data(), &MatrixClient::redactionFailed, this, [this](const QString &error) {
|
||||||
emit showNotification(QString("Message redaction failed: %1").arg(error));
|
emit showNotification(QString("Message redaction failed: %1").arg(error));
|
||||||
});
|
});
|
||||||
|
connect(client_.data(),
|
||||||
|
&MatrixClient::notificationsRetrieved,
|
||||||
|
this,
|
||||||
|
&ChatPage::sendDesktopNotifications);
|
||||||
|
|
||||||
showContentTimer_ = new QTimer(this);
|
showContentTimer_ = new QTimer(this);
|
||||||
showContentTimer_->setSingleShot(true);
|
showContentTimer_->setSingleShot(true);
|
||||||
|
@ -420,13 +425,20 @@ ChatPage::ChatPage(QSharedPointer<MatrixClient> client,
|
||||||
view_manager_->initialize(rooms);
|
view_manager_->initialize(rooms);
|
||||||
removeLeftRooms(rooms.leave);
|
removeLeftRooms(rooms.leave);
|
||||||
|
|
||||||
|
bool hasNotifications = false;
|
||||||
for (const auto &room : rooms.join) {
|
for (const auto &room : rooms.join) {
|
||||||
auto room_id = QString::fromStdString(room.first);
|
auto room_id = QString::fromStdString(room.first);
|
||||||
|
|
||||||
updateTypingUsers(room_id, room.second.ephemeral.typing);
|
updateTypingUsers(room_id, room.second.ephemeral.typing);
|
||||||
updateRoomNotificationCount(
|
updateRoomNotificationCount(
|
||||||
room_id, room.second.unread_notifications.notification_count);
|
room_id, room.second.unread_notifications.notification_count);
|
||||||
|
|
||||||
|
if (room.second.unread_notifications.notification_count > 0)
|
||||||
|
hasNotifications = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (hasNotifications)
|
||||||
|
client_->getNotifications();
|
||||||
});
|
});
|
||||||
connect(this, &ChatPage::syncRoomlist, room_list_, &RoomList::sync);
|
connect(this, &ChatPage::syncRoomlist, room_list_, &RoomList::sync);
|
||||||
|
|
||||||
|
@ -838,3 +850,29 @@ ChatPage::updateRoomNotificationCount(const QString &room_id, uint16_t notificat
|
||||||
{
|
{
|
||||||
room_list_->updateUnreadMessageCount(room_id, notification_count);
|
room_list_->updateUnreadMessageCount(room_id, notification_count);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
ChatPage::sendDesktopNotifications(const mtx::responses::Notifications &res)
|
||||||
|
{
|
||||||
|
for (const auto &item : res.notifications) {
|
||||||
|
const auto event_id = utils::event_id(item.event);
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (item.read) {
|
||||||
|
cache_->removeReadNotification(event_id);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!cache_->isNotificationSent(event_id)) {
|
||||||
|
// TODO: send desktop notification
|
||||||
|
// qDebug() << "sender" << utils::event_sender(item.event);
|
||||||
|
// qDebug() << "body" << utils::event_body(item.event);
|
||||||
|
|
||||||
|
// We should only sent one notification per event.
|
||||||
|
// cache_->markSentNotification(event_id);
|
||||||
|
}
|
||||||
|
} catch (const lmdb::error &e) {
|
||||||
|
qWarning() << e.what();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1310,3 +1310,41 @@ MatrixClient::redactEvent(const QString &room_id, const QString &event_id)
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
MatrixClient::getNotifications() noexcept
|
||||||
|
{
|
||||||
|
QUrlQuery query;
|
||||||
|
query.addQueryItem("limit", "5");
|
||||||
|
|
||||||
|
QUrl endpoint(server_);
|
||||||
|
endpoint.setQuery(query);
|
||||||
|
endpoint.setPath(clientApiUrl_ + "/notifications");
|
||||||
|
|
||||||
|
QNetworkRequest request(QString(endpoint.toEncoded()));
|
||||||
|
setupAuth(request);
|
||||||
|
|
||||||
|
auto reply = get(request);
|
||||||
|
connect(reply, &QNetworkReply::finished, this, [reply, this]() {
|
||||||
|
reply->deleteLater();
|
||||||
|
|
||||||
|
int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
|
||||||
|
auto data = reply->readAll();
|
||||||
|
|
||||||
|
if (status == 0 || status >= 400) {
|
||||||
|
try {
|
||||||
|
mtx::errors::Error res = nlohmann::json::parse(data);
|
||||||
|
std::cout << nlohmann::json::parse(data).dump(2) << '\n';
|
||||||
|
// TODO: Response with an error signal
|
||||||
|
return;
|
||||||
|
} catch (const std::exception &) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
emit notificationsRetrieved(nlohmann::json::parse(data));
|
||||||
|
} catch (const std::exception &e) {
|
||||||
|
qWarning() << "failed to parse /notifications response" << e.what();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
27
src/Utils.cc
27
src/Utils.cc
|
@ -111,3 +111,30 @@ utils::levenshtein_distance(const std::string &s1, const std::string &s2)
|
||||||
|
|
||||||
return *std::min_element(row1.begin(), row1.end());
|
return *std::min_element(row1.begin(), row1.end());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QString
|
||||||
|
utils::event_body(const mtx::events::collections::TimelineEvents &event)
|
||||||
|
{
|
||||||
|
using namespace mtx::events;
|
||||||
|
using namespace mtx::events::msg;
|
||||||
|
|
||||||
|
if (mpark::holds_alternative<RoomEvent<Audio>>(event)) {
|
||||||
|
return message_body<RoomEvent<Audio>>(event);
|
||||||
|
} else if (mpark::holds_alternative<RoomEvent<Emote>>(event)) {
|
||||||
|
return message_body<RoomEvent<Emote>>(event);
|
||||||
|
} else if (mpark::holds_alternative<RoomEvent<File>>(event)) {
|
||||||
|
return message_body<RoomEvent<File>>(event);
|
||||||
|
} else if (mpark::holds_alternative<RoomEvent<Image>>(event)) {
|
||||||
|
return message_body<RoomEvent<Image>>(event);
|
||||||
|
} else if (mpark::holds_alternative<RoomEvent<Notice>>(event)) {
|
||||||
|
return message_body<RoomEvent<Notice>>(event);
|
||||||
|
} else if (mpark::holds_alternative<Sticker>(event)) {
|
||||||
|
return message_body<Sticker>(event);
|
||||||
|
} else if (mpark::holds_alternative<RoomEvent<Text>>(event)) {
|
||||||
|
return message_body<RoomEvent<Text>>(event);
|
||||||
|
} else if (mpark::holds_alternative<RoomEvent<Video>>(event)) {
|
||||||
|
return message_body<RoomEvent<Video>>(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
return QString();
|
||||||
|
}
|
||||||
|
|
|
@ -727,12 +727,6 @@ TimelineView::event(QEvent *event)
|
||||||
return QWidget::event(event);
|
return QWidget::event(event);
|
||||||
}
|
}
|
||||||
|
|
||||||
QString
|
|
||||||
TimelineView::getEventSender(const mtx::events::collections::TimelineEvents &event) const
|
|
||||||
{
|
|
||||||
return mpark::visit([](auto msg) { return QString::fromStdString(msg.sender); }, event);
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
void
|
||||||
TimelineView::toggleScrollDownButton()
|
TimelineView::toggleScrollDownButton()
|
||||||
{
|
{
|
||||||
|
@ -826,8 +820,8 @@ TimelineView::relativeWidget(TimelineItem *item, int dt) const
|
||||||
TimelineEvent
|
TimelineEvent
|
||||||
TimelineView::findFirstViewableEvent(const std::vector<TimelineEvent> &events)
|
TimelineView::findFirstViewableEvent(const std::vector<TimelineEvent> &events)
|
||||||
{
|
{
|
||||||
auto it = std::find_if(events.begin(), events.end(), [this](const auto &event) {
|
auto it = std::find_if(events.begin(), events.end(), [](const auto &event) {
|
||||||
return mtx::events::EventType::RoomMessage == getEventType(event);
|
return mtx::events::EventType::RoomMessage == utils::event_type(event);
|
||||||
});
|
});
|
||||||
|
|
||||||
return (it == std::end(events)) ? events.front() : *it;
|
return (it == std::end(events)) ? events.front() : *it;
|
||||||
|
@ -836,19 +830,13 @@ TimelineView::findFirstViewableEvent(const std::vector<TimelineEvent> &events)
|
||||||
TimelineEvent
|
TimelineEvent
|
||||||
TimelineView::findLastViewableEvent(const std::vector<TimelineEvent> &events)
|
TimelineView::findLastViewableEvent(const std::vector<TimelineEvent> &events)
|
||||||
{
|
{
|
||||||
auto it = std::find_if(events.rbegin(), events.rend(), [this](const auto &event) {
|
auto it = std::find_if(events.rbegin(), events.rend(), [](const auto &event) {
|
||||||
return mtx::events::EventType::RoomMessage == getEventType(event);
|
return mtx::events::EventType::RoomMessage == utils::event_type(event);
|
||||||
});
|
});
|
||||||
|
|
||||||
return (it == std::rend(events)) ? events.back() : *it;
|
return (it == std::rend(events)) ? events.back() : *it;
|
||||||
}
|
}
|
||||||
|
|
||||||
inline mtx::events::EventType
|
|
||||||
TimelineView::getEventType(const mtx::events::collections::TimelineEvents &event) const
|
|
||||||
{
|
|
||||||
return mpark::visit([](auto msg) { return msg.type; }, event);
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
void
|
||||||
TimelineView::saveMessageInfo(const QString &sender,
|
TimelineView::saveMessageInfo(const QString &sender,
|
||||||
uint64_t origin_server_ts,
|
uint64_t origin_server_ts,
|
||||||
|
|
Loading…
Reference in a new issue