Newer
Older
// SPDX-FileCopyrightText: 2021 Nheko Contributors
//
// SPDX-License-Identifier: GPL-3.0-or-later
#include <QRegularExpression>
#include <QSettings>
#include "MainWindow.h"
#include "MatrixClient.h"
namespace std {
inline uint
qHash(const std::string &key, uint seed = 0)
{
return qHash(QByteArray::fromRawData(key.data(), (int)key.length()), seed);
template<class T>
qml_mtx_events::EventType operator()(const mtx::events::Event<T> &e)
{
using mtx::events::EventType;
switch (e.type) {
case EventType::RoomKeyRequest:
return qml_mtx_events::EventType::KeyRequest;
case EventType::Reaction:
return qml_mtx_events::EventType::Reaction;
case EventType::RoomAliases:
return qml_mtx_events::EventType::Aliases;
case EventType::RoomAvatar:
return qml_mtx_events::EventType::Avatar;
case EventType::RoomCanonicalAlias:
return qml_mtx_events::EventType::CanonicalAlias;
case EventType::RoomCreate:
return qml_mtx_events::EventType::RoomCreate;
case EventType::RoomEncrypted:
return qml_mtx_events::EventType::Encrypted;
case EventType::RoomEncryption:
return qml_mtx_events::EventType::Encryption;
case EventType::RoomGuestAccess:
return qml_mtx_events::EventType::RoomGuestAccess;
case EventType::RoomHistoryVisibility:
return qml_mtx_events::EventType::RoomHistoryVisibility;
return qml_mtx_events::EventType::RoomJoinRules;
case EventType::RoomMember:
return qml_mtx_events::EventType::Member;
case EventType::RoomMessage:
return qml_mtx_events::EventType::UnknownMessage;
case EventType::RoomName:
return qml_mtx_events::EventType::Name;
case EventType::RoomPowerLevels:
return qml_mtx_events::EventType::PowerLevels;
case EventType::RoomTopic:
return qml_mtx_events::EventType::Topic;
case EventType::RoomTombstone:
return qml_mtx_events::EventType::Tombstone;
case EventType::RoomRedaction:
return qml_mtx_events::EventType::Redaction;
case EventType::RoomPinnedEvents:
return qml_mtx_events::EventType::PinnedEvents;
case EventType::Sticker:
return qml_mtx_events::EventType::Sticker;
case EventType::Tag:
return qml_mtx_events::EventType::Tag;
case EventType::Unsupported:
return qml_mtx_events::EventType::Unsupported;
default:
return qml_mtx_events::EventType::UnknownMessage;
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
}
}
qml_mtx_events::EventType operator()(const mtx::events::Event<mtx::events::msg::Audio> &)
{
return qml_mtx_events::EventType::AudioMessage;
}
qml_mtx_events::EventType operator()(const mtx::events::Event<mtx::events::msg::Emote> &)
{
return qml_mtx_events::EventType::EmoteMessage;
}
qml_mtx_events::EventType operator()(const mtx::events::Event<mtx::events::msg::File> &)
{
return qml_mtx_events::EventType::FileMessage;
}
qml_mtx_events::EventType operator()(const mtx::events::Event<mtx::events::msg::Image> &)
{
return qml_mtx_events::EventType::ImageMessage;
}
qml_mtx_events::EventType operator()(const mtx::events::Event<mtx::events::msg::Notice> &)
{
return qml_mtx_events::EventType::NoticeMessage;
}
qml_mtx_events::EventType operator()(const mtx::events::Event<mtx::events::msg::Text> &)
{
return qml_mtx_events::EventType::TextMessage;
}
qml_mtx_events::EventType operator()(const mtx::events::Event<mtx::events::msg::Video> &)
{
return qml_mtx_events::EventType::VideoMessage;
qml_mtx_events::EventType operator()(
const mtx::events::Event<mtx::events::msg::KeyVerificationRequest> &)
{
return qml_mtx_events::EventType::KeyVerificationRequest;
}
qml_mtx_events::EventType operator()(
const mtx::events::Event<mtx::events::msg::KeyVerificationStart> &)
{
return qml_mtx_events::EventType::KeyVerificationStart;
}
qml_mtx_events::EventType operator()(
const mtx::events::Event<mtx::events::msg::KeyVerificationMac> &)
{
return qml_mtx_events::EventType::KeyVerificationMac;
}
qml_mtx_events::EventType operator()(
const mtx::events::Event<mtx::events::msg::KeyVerificationAccept> &)
{
return qml_mtx_events::EventType::KeyVerificationAccept;
}
qml_mtx_events::EventType operator()(
const mtx::events::Event<mtx::events::msg::KeyVerificationReady> &)
{
return qml_mtx_events::EventType::KeyVerificationReady;
}
qml_mtx_events::EventType operator()(
const mtx::events::Event<mtx::events::msg::KeyVerificationCancel> &)
{
return qml_mtx_events::EventType::KeyVerificationCancel;
}
qml_mtx_events::EventType operator()(
const mtx::events::Event<mtx::events::msg::KeyVerificationKey> &)
{
return qml_mtx_events::EventType::KeyVerificationKey;
}
qml_mtx_events::EventType operator()(
const mtx::events::Event<mtx::events::msg::KeyVerificationDone> &)
{
return qml_mtx_events::EventType::KeyVerificationDone;
}
qml_mtx_events::EventType operator()(const mtx::events::Event<mtx::events::msg::Redacted> &)
{
return qml_mtx_events::EventType::Redacted;
qml_mtx_events::EventType operator()(
const mtx::events::Event<mtx::events::msg::CallInvite> &)
{
return qml_mtx_events::EventType::CallInvite;
}
qml_mtx_events::EventType operator()(
const mtx::events::Event<mtx::events::msg::CallAnswer> &)
{
return qml_mtx_events::EventType::CallAnswer;
}
qml_mtx_events::EventType operator()(
const mtx::events::Event<mtx::events::msg::CallHangUp> &)
{
return qml_mtx_events::EventType::CallHangUp;
}
qml_mtx_events::EventType operator()(
const mtx::events::Event<mtx::events::msg::CallCandidates> &)
{
return qml_mtx_events::EventType::CallCandidates;
}
// ::EventType::Type operator()(const Event<mtx::events::msg::Location> &e) { return
// ::EventType::LocationMessage; }
};
toRoomEventType(const mtx::events::collections::TimelineEvents &event)
return std::visit(RoomEventType{}, event);
QString
toRoomEventTypeString(const mtx::events::collections::TimelineEvents &event)
{
return std::visit([](const auto &e) { return QString::fromStdString(to_string(e.type)); },
event);
}
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
mtx::events::EventType
qml_mtx_events::fromRoomEventType(qml_mtx_events::EventType t)
{
switch (t) {
// Unsupported event
case qml_mtx_events::Unsupported:
return mtx::events::EventType::Unsupported;
/// m.room_key_request
case qml_mtx_events::KeyRequest:
return mtx::events::EventType::RoomKeyRequest;
/// m.reaction:
case qml_mtx_events::Reaction:
return mtx::events::EventType::Reaction;
/// m.room.aliases
case qml_mtx_events::Aliases:
return mtx::events::EventType::RoomAliases;
/// m.room.avatar
case qml_mtx_events::Avatar:
return mtx::events::EventType::RoomAvatar;
/// m.call.invite
case qml_mtx_events::CallInvite:
return mtx::events::EventType::CallInvite;
/// m.call.answer
case qml_mtx_events::CallAnswer:
return mtx::events::EventType::CallAnswer;
/// m.call.hangup
case qml_mtx_events::CallHangUp:
return mtx::events::EventType::CallHangUp;
/// m.call.candidates
case qml_mtx_events::CallCandidates:
return mtx::events::EventType::CallCandidates;
/// m.room.canonical_alias
case qml_mtx_events::CanonicalAlias:
return mtx::events::EventType::RoomCanonicalAlias;
/// m.room.create
case qml_mtx_events::RoomCreate:
return mtx::events::EventType::RoomCreate;
/// m.room.encrypted.
case qml_mtx_events::Encrypted:
return mtx::events::EventType::RoomEncrypted;
/// m.room.encryption.
case qml_mtx_events::Encryption:
return mtx::events::EventType::RoomEncryption;
/// m.room.guest_access
case qml_mtx_events::RoomGuestAccess:
return mtx::events::EventType::RoomGuestAccess;
/// m.room.history_visibility
case qml_mtx_events::RoomHistoryVisibility:
return mtx::events::EventType::RoomHistoryVisibility;
/// m.room.join_rules
case qml_mtx_events::RoomJoinRules:
return mtx::events::EventType::RoomJoinRules;
/// m.room.member
case qml_mtx_events::Member:
return mtx::events::EventType::RoomMember;
/// m.room.name
case qml_mtx_events::Name:
return mtx::events::EventType::RoomName;
/// m.room.power_levels
case qml_mtx_events::PowerLevels:
return mtx::events::EventType::RoomPowerLevels;
/// m.room.tombstone
case qml_mtx_events::Tombstone:
return mtx::events::EventType::RoomTombstone;
/// m.room.topic
case qml_mtx_events::Topic:
return mtx::events::EventType::RoomTopic;
/// m.room.redaction
case qml_mtx_events::Redaction:
return mtx::events::EventType::RoomRedaction;
/// m.room.pinned_events
case qml_mtx_events::PinnedEvents:
return mtx::events::EventType::RoomPinnedEvents;
// m.sticker
case qml_mtx_events::Sticker:
return mtx::events::EventType::Sticker;
// m.tag
case qml_mtx_events::Tag:
return mtx::events::EventType::Tag;
/// m.room.message
case qml_mtx_events::AudioMessage:
case qml_mtx_events::EmoteMessage:
case qml_mtx_events::FileMessage:
case qml_mtx_events::ImageMessage:
case qml_mtx_events::LocationMessage:
case qml_mtx_events::NoticeMessage:
case qml_mtx_events::TextMessage:
case qml_mtx_events::VideoMessage:
case qml_mtx_events::Redacted:
case qml_mtx_events::UnknownMessage:
case qml_mtx_events::KeyVerificationRequest:
case qml_mtx_events::KeyVerificationStart:
case qml_mtx_events::KeyVerificationMac:
case qml_mtx_events::KeyVerificationAccept:
case qml_mtx_events::KeyVerificationCancel:
case qml_mtx_events::KeyVerificationKey:
case qml_mtx_events::KeyVerificationDone:
case qml_mtx_events::KeyVerificationReady:
return mtx::events::EventType::RoomMessage;
default:
return mtx::events::EventType::Unsupported;
};
}
TimelineModel::TimelineModel(TimelineViewManager *manager, QString room_id, QObject *parent)
: QAbstractListModel(parent)
, room_id_(room_id)
connect(
this,
&TimelineModel::redactionFailed,
this,
[](const QString &msg) { emit ChatPage::instance()->showNotification(msg); },
Qt::QueuedConnection);
connect(this,
&TimelineModel::newMessageToSend,
this,
&TimelineModel::addPendingMessage,
Qt::QueuedConnection);
connect(this, &TimelineModel::addPendingMessageToStore, &events, &EventStore::addPending);
connect(
&events,
&EventStore::dataChanged,
this,
[this](int from, int to) {
nhlog::ui()->debug(
"data changed {} to {}", events.size() - to - 1, events.size() - from - 1);
emit dataChanged(index(events.size() - to - 1, 0),
index(events.size() - from - 1, 0));
},
Qt::QueuedConnection);
connect(&events, &EventStore::beginInsertRows, this, [this](int from, int to) {
int first = events.size() - to;
int last = events.size() - from;
if (from >= events.size()) {
int batch_size = to - from;
first += batch_size;
last += batch_size;
} else {
first -= 1;
last -= 1;
}
nhlog::ui()->debug("begin insert from {} to {}", first, last);
beginInsertRows(QModelIndex(), first, last);
});
connect(&events, &EventStore::endInsertRows, this, [this]() { endInsertRows(); });
connect(&events, &EventStore::beginResetModel, this, [this]() { beginResetModel(); });
connect(&events, &EventStore::endResetModel, this, [this]() { endResetModel(); });
connect(&events, &EventStore::newEncryptedImage, this, &TimelineModel::newEncryptedImage);
connect(
&events, &EventStore::fetchedMore, this, [this]() { setPaginationInProgress(false); });
connect(&events,
&EventStore::startDMVerification,
this,
[this](mtx::events::RoomEvent<mtx::events::msg::KeyVerificationRequest> msg) {
ChatPage::instance()->receivedRoomDeviceVerificationRequest(msg, this);
connect(&events, &EventStore::updateFlowEventId, this, [this](std::string event_id) {
this->updateFlowEventId(event_id);
});
showEventTimer.callOnTimeout(this, &TimelineModel::scrollTimerEvent);
QHash<int, QByteArray>
TimelineModel::roleNames() const
{
return {
{Type, "type"},
{IsOnlyEmoji, "isOnlyEmoji"},
{Body, "body"},
{FormattedBody, "formattedBody"},
{PreviousMessageUserId, "previousMessageUserId"},
{IsSender, "isSender"},
{UserId, "userId"},
{UserName, "userName"},
{PreviousMessageDay, "previousMessageDay"},
{Day, "day"},
{Height, "height"},
{Width, "width"},
{ProportionalHeight, "proportionalHeight"},
{IsEdited, "isEdited"},
{IsEditable, "isEditable"},
{IsRoomEncrypted, "isRoomEncrypted"},
{RoomName, "roomName"},
{RoomTopic, "roomTopic"},
};
}
int
TimelineModel::rowCount(const QModelIndex &parent) const
{
Q_UNUSED(parent);
TimelineModel::getDump(QString eventId, QString relatedTo) const
if (auto event = events.get(eventId.toStdString(), relatedTo.toStdString()))
TimelineModel::data(const mtx::events::collections::TimelineEvents &event, int role) const
case IsSender:
return QVariant(acc::sender(event) == http::client()->user_id().to_string());
return QVariant(QString::fromStdString(acc::sender(event)));
case UserName:
return QVariant(displayName(QString::fromStdString(acc::sender(event))));
case Day: {
QDateTime prevDate = origin_server_ts(event);
prevDate.setTime(QTime());
return QVariant(prevDate.toMSecsSinceEpoch());
}
case Timestamp:
return QVariant(origin_server_ts(event));
return QVariant(toRoomEventType(event));
case TypeString:
return QVariant(toRoomEventTypeString(event));
case IsOnlyEmoji: {
QString qBody = QString::fromStdString(body(event));
QVector<uint> utf32_string = qBody.toUcs4();
int emojiCount = 0;
for (auto &code : utf32_string) {
if (utils::codepointIsEmoji(code)) {
emojiCount++;
} else {
return QVariant(0);
}
}
return QVariant(emojiCount);
}
return QVariant(
utils::replaceEmoji(QString::fromStdString(body(event)).toHtmlEscaped()));
case FormattedBody: {
const static QRegularExpression replyFallback(
"<mx-reply>.*</mx-reply>", QRegularExpression::DotMatchesEverythingOption);
auto ascent = QFontMetrics(UserSettings::instance()->font()).ascent();
bool isReply = utils::isReply(event);
auto formattedBody_ = QString::fromStdString(formatted_body(event));
if (formattedBody_.isEmpty()) {
auto body_ = QString::fromStdString(body(event));
if (isReply) {
while (body_.startsWith("> "))
body_ = body_.right(body_.size() - body_.indexOf('\n') - 1);
if (body_.startsWith('\n'))
body_ = body_.right(body_.size() - 1);
}
formattedBody_ = body_.toHtmlEscaped().replace('\n', "<br>");
} else {
if (isReply)
formattedBody_ = formattedBody_.remove(replyFallback);
}
// TODO(Nico): Don't parse html with a regex
const static QRegularExpression matchImgUri(
"(<img [^>]*)src=\"mxc://([^\"]*)\"([^>]*>)");
formattedBody_.replace(matchImgUri, "\\1 src=\"image://mxcImage/\\2\"\\3");
const static QRegularExpression matchEmoticonHeight(
"(<img data-mx-emoticon [^>]*)height=\"([^\"]*)\"([^>]*>)");
formattedBody_.replace(matchEmoticonHeight,
QString("\\1 height=\"%1\"\\3").arg(ascent));
return QVariant(utils::replaceEmoji(
utils::linkifyMessage(utils::escapeBlacklistedHtml(formattedBody_))));
return QVariant(QString::fromStdString(url(event)));
return QVariant(QString::fromStdString(thumbnail_url(event)));
case Blurhash:
return QVariant(QString::fromStdString(blurhash(event)));
return QVariant(QString::fromStdString(filename(event)));
return QVariant(utils::humanReadableFileSize(filesize(event)));
return QVariant(QString::fromStdString(mimetype(event)));
return QVariant(qulonglong{media_height(event)});
return QVariant(qulonglong{media_width(event)});
case ProportionalHeight: {
auto w = media_width(event);
if (w == 0)
w = 1;
double prop = media_height(event) / (double)w;
return QVariant(prop > 0 ? prop : 1.);
}
case Id: {
if (auto replaces = relations(event).replaces())
return QVariant(QString::fromStdString(replaces.value()));
else
return QVariant(QString::fromStdString(event_id(event)));
}
auto id = QString::fromStdString(event_id(event));
auto containsOthers = [](const auto &vec) {
for (const auto &e : vec)
if (e.second != http::client()->user_id().to_string())
return true;
return false;
};
if (acc::sender(event) != http::client()->user_id().to_string())
else if (read.contains(id) || containsOthers(cache::readReceipts(id, room_id_)))
return qml_mtx_events::Read;
case IsEdited:
return QVariant(relations(event).replaces().has_value());
case IsEditable:
return QVariant(!is_state_event(event) &&
mtx::accessors::sender(event) ==
http::client()->user_id().to_string() &&
!event_id(event).empty() && event_id(event).front() == '$');
auto encrypted_event = events.get(id, id, false);
return encrypted_event &&
std::holds_alternative<
mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>>(
*encrypted_event);
case Trustlevel: {
auto id = event_id(event);
auto encrypted_event = events.get(id, id, false);
if (encrypted_event) {
if (auto encrypted =
std::get_if<mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>>(
&*encrypted_event)) {
return olm::calculate_trust(encrypted->sender,
encrypted->content.sender_key);
}
}
return crypto::Trust::Unverified;
}
case IsRoomEncrypted: {
return cache::isRoomEncrypted(room_id_.toStdString());
}
return QVariant(QString::fromStdString(relations(event).reply_to().value_or("")));
auto id = relations(event).replaces().value_or(event_id(event));
return QVariant(room_id_);
return QVariant(
utils::replaceEmoji(QString::fromStdString(room_name(event)).toHtmlEscaped()));
return QVariant(utils::replaceEmoji(
utils::linkifyMessage(QString::fromStdString(room_topic(event))
.toHtmlEscaped()
.replace("\n", "<br>"))));
case CallType:
return QVariant(QString::fromStdString(call_type(event)));
case Dump: {
QVariantMap m;
auto names = roleNames();
m.insert(names[Type], data(event, static_cast<int>(Type)));
m.insert(names[TypeString], data(event, static_cast<int>(TypeString)));
m.insert(names[IsOnlyEmoji], data(event, static_cast<int>(IsOnlyEmoji)));
m.insert(names[Body], data(event, static_cast<int>(Body)));
m.insert(names[FormattedBody], data(event, static_cast<int>(FormattedBody)));
m.insert(names[IsSender], data(event, static_cast<int>(IsSender)));
m.insert(names[UserId], data(event, static_cast<int>(UserId)));
m.insert(names[UserName], data(event, static_cast<int>(UserName)));
m.insert(names[Day], data(event, static_cast<int>(Day)));
m.insert(names[Timestamp], data(event, static_cast<int>(Timestamp)));
m.insert(names[Url], data(event, static_cast<int>(Url)));
m.insert(names[ThumbnailUrl], data(event, static_cast<int>(ThumbnailUrl)));
m.insert(names[Blurhash], data(event, static_cast<int>(Blurhash)));
m.insert(names[Filename], data(event, static_cast<int>(Filename)));
m.insert(names[Filesize], data(event, static_cast<int>(Filesize)));
m.insert(names[MimeType], data(event, static_cast<int>(MimeType)));
m.insert(names[Height], data(event, static_cast<int>(Height)));
m.insert(names[Width], data(event, static_cast<int>(Width)));
m.insert(names[ProportionalHeight],
data(event, static_cast<int>(ProportionalHeight)));
m.insert(names[Id], data(event, static_cast<int>(Id)));
m.insert(names[State], data(event, static_cast<int>(State)));
m.insert(names[IsEdited], data(event, static_cast<int>(IsEdited)));
m.insert(names[IsEditable], data(event, static_cast<int>(IsEditable)));
m.insert(names[IsEncrypted], data(event, static_cast<int>(IsEncrypted)));
m.insert(names[IsRoomEncrypted], data(event, static_cast<int>(IsRoomEncrypted)));
m.insert(names[ReplyTo], data(event, static_cast<int>(ReplyTo)));
m.insert(names[RoomName], data(event, static_cast<int>(RoomName)));
m.insert(names[RoomTopic], data(event, static_cast<int>(RoomTopic)));
m.insert(names[CallType], data(event, static_cast<int>(CallType)));
default:
return QVariant();
}
}
QVariant
TimelineModel::data(const QModelIndex &index, int role) const
{
using namespace mtx::accessors;
namespace acc = mtx::accessors;
if (index.row() < 0 && index.row() >= rowCount())
auto event = events.get(rowCount() - index.row() - 1);
if (role == PreviousMessageDay || role == PreviousMessageUserId) {
int prevIdx = rowCount() - index.row() - 2;
if (prevIdx < 0)
return QVariant();
auto tempEv = events.get(prevIdx);
if (!tempEv)
return QVariant();
if (role == PreviousMessageUserId)
return data(*tempEv, UserId);
else
return data(*tempEv, Day);
bool
TimelineModel::canFetchMore(const QModelIndex &) const
{
if (auto first = events.get(0);
first &&
!std::holds_alternative<mtx::events::StateEvent<mtx::events::state::Create>>(*first))
return true;
else
return false;
}
void
TimelineModel::setPaginationInProgress(const bool paginationInProgress)
{
if (m_paginationInProgress == paginationInProgress) {
return;
}
m_paginationInProgress = paginationInProgress;
emit paginationInProgressChanged(m_paginationInProgress);
}
void
TimelineModel::fetchMore(const QModelIndex &)
{
if (m_paginationInProgress) {
nhlog::ui()->warn("Already loading older messages");
return;
}
setPaginationInProgress(true);
void
TimelineModel::syncState(const mtx::responses::State &s)
{
using namespace mtx::events;
for (const auto &e : s.events) {
if (std::holds_alternative<StateEvent<state::Avatar>>(e))
emit roomAvatarUrlChanged();
else if (std::holds_alternative<StateEvent<state::Name>>(e))
emit roomNameChanged();
else if (std::holds_alternative<StateEvent<state::Topic>>(e))
emit roomTopicChanged();
else if (std::holds_alternative<StateEvent<state::Topic>>(e)) {
permissions_.invalidate();
emit permissionsChanged();
} else if (std::holds_alternative<StateEvent<state::Member>>(e)) {
emit roomAvatarUrlChanged();
emit roomNameChanged();
}
}
}
TimelineModel::addEvents(const mtx::responses::Timeline &timeline)
if (timeline.events.empty())
return;
using namespace mtx::events;
for (auto e : timeline.events) {
if (auto encryptedEvent = std::get_if<EncryptedEvent<msg::Encrypted>>(&e)) {
MegolmSessionIndex index;
index.room_id = room_id_.toStdString();
index.session_id = encryptedEvent->content.session_id;
index.sender_key = encryptedEvent->content.sender_key;
auto result = olm::decryptEvent(index, *encryptedEvent);
if (result.event)
e = result.event.value();
}
if (std::holds_alternative<RoomEvent<msg::CallCandidates>>(e) ||
std::holds_alternative<RoomEvent<msg::CallInvite>>(e) ||
std::holds_alternative<RoomEvent<msg::CallAnswer>>(e) ||
std::holds_alternative<RoomEvent<msg::CallHangUp>>(e))
std::visit(
[this](auto &event) {
event.room_id = room_id_.toStdString();
if constexpr (std::is_same_v<std::decay_t<decltype(event)>,
RoomEvent<msg::CallAnswer>> ||
std::is_same_v<std::decay_t<decltype(event)>,
RoomEvent<msg::CallHangUp>>)
emit newCallEvent(event);
else {
if (event.sender != http::client()->user_id().to_string())
emit newCallEvent(event);
}
},
e);
else if (std::holds_alternative<StateEvent<state::Avatar>>(e))
emit roomAvatarUrlChanged();
else if (std::holds_alternative<StateEvent<state::Name>>(e))
emit roomNameChanged();
else if (std::holds_alternative<StateEvent<state::Topic>>(e))
emit roomTopicChanged();
else if (std::holds_alternative<StateEvent<state::PowerLevels>>(e)) {
permissions_.invalidate();
emit permissionsChanged();
} else if (std::holds_alternative<StateEvent<state::Member>>(e)) {
emit roomAvatarUrlChanged();
emit roomNameChanged();
}
}
updateLastMessage();
template<typename T>
auto
isMessage(const mtx::events::RoomEvent<T> &e)
-> std::enable_if_t<std::is_same<decltype(e.content.msgtype), std::string>::value, bool>
{
return true;
}
template<typename T>
auto
isMessage(const mtx::events::Event<T> &)
{
return false;
}
template<typename T>
auto
isMessage(const mtx::events::EncryptedEvent<T> &)
{
return true;
}
auto
isMessage(const mtx::events::RoomEvent<mtx::events::msg::CallInvite> &)
{
return true;
}
auto
isMessage(const mtx::events::RoomEvent<mtx::events::msg::CallAnswer> &)
{
return true;
}
auto
isMessage(const mtx::events::RoomEvent<mtx::events::msg::CallHangUp> &)
{
return true;
}
// Workaround. We also want to see a room at the top, if we just joined it
auto
isYourJoin(const mtx::events::StateEvent<mtx::events::state::Member> &e)
{
return e.content.membership == mtx::events::state::Membership::Join &&
e.state_key == http::client()->user_id().to_string();
}
template<typename T>
auto
isYourJoin(const mtx::events::Event<T> &)
{
return false;
}
void
TimelineModel::updateLastMessage()
for (auto it = events.size() - 1; it >= 0; --it) {
auto event = events.get(it, decryptDescription);
if (std::visit([](const auto &e) -> bool { return isYourJoin(e); }, *event)) {
auto time = mtx::accessors::origin_server_ts(*event);
uint64_t ts = time.toMSecsSinceEpoch();
emit manager_->updateRoomsLastMessage(
room_id_,
DescInfo{QString::fromStdString(mtx::accessors::event_id(*event)),
QString::fromStdString(http::client()->user_id().to_string()),
utils::descriptiveTime(time),
ts,
time});
return;
}
if (!std::visit([](const auto &e) -> bool { return isMessage(e); }, *event))
continue;
auto description = utils::getMessageDescription(
*event,
QString::fromStdString(http::client()->user_id().to_string()),
cache::displayName(room_id_,
QString::fromStdString(mtx::accessors::sender(*event))));
emit manager_->updateRoomsLastMessage(room_id_, description);
return;
}
void
TimelineModel::setCurrentIndex(int index)
{
auto oldIndex = idToIndex(currentId);
if (index != oldIndex)
emit currentIndexChanged(index);
if (!ChatPage::instance()->isActiveWindow())
return;
if (!currentId.startsWith("m")) {
auto oldReadIndex =
cache::getEventIndex(roomId().toStdString(), currentReadId.toStdString());
auto nextEventIndexAndId =
cache::lastInvisibleEventAfter(roomId().toStdString(), currentId.toStdString());
if (nextEventIndexAndId &&
(!oldReadIndex || *oldReadIndex < nextEventIndexAndId->first)) {
readEvent(nextEventIndexAndId->second);
currentReadId = QString::fromStdString(nextEventIndexAndId->second);
}
void
TimelineModel::readEvent(const std::string &id)
{
http::client()->read_event(room_id_.toStdString(), id, [this](mtx::http::RequestErr err) {
if (err) {
nhlog::net()->warn("failed to read_event ({}, {})",
room_id_.toStdString(),
currentId.toStdString());
}
});
}
QString
TimelineModel::displayName(QString id) const
{
return cache::displayName(room_id_, id).toHtmlEscaped();
QString
TimelineModel::avatarUrl(QString id) const
{
QString
TimelineModel::formatDateSeparator(QDate date) const
{
auto now = QDateTime::currentDateTime();
QString fmt = QLocale::system().dateFormat(QLocale::LongFormat);
if (now.date().year() == date.year()) {
QRegularExpression rx("[^a-zA-Z]*y+[^a-zA-Z]*");
fmt = fmt.remove(rx);
}
return date.toString(fmt);
}
void
TimelineModel::viewRawMessage(QString id) const
{
auto e = events.get(id.toStdString(), "", false);
if (!e)
return;
std::string ev = mtx::accessors::serialize_event(*e).dump(4);
auto dialog = new dialogs::RawMessage(QString::fromStdString(ev));
Q_UNUSED(dialog);
}
void
TimelineModel::forwardMessage(QString eventId, QString roomId)
{
auto e = events.get(eventId.toStdString(), "");
if (!e)
return;
emit forwardToRoom(e, roomId);
TimelineModel::viewDecryptedRawMessage(QString id) const
{
auto e = events.get(id.toStdString(), "");
std::string ev = mtx::accessors::serialize_event(*e).dump(4);
auto dialog = new dialogs::RawMessage(QString::fromStdString(ev));
Q_UNUSED(dialog);
}
void
Nicolas Werner
committed
TimelineModel::openUserProfile(QString userid)
Nicolas Werner
committed
UserProfile *userProfile = new UserProfile(room_id_, userid, manager_, this);
Jedi18
committed
connect(
this, &TimelineModel::roomAvatarUrlChanged, userProfile, &UserProfile::updateAvatarUrl);
Nicolas Werner
committed
emit manager_->openProfile(userProfile);
TimelineModel::openRoomSettings()