Newer
Older
tr("You joined this room."),
utils::descriptiveTime(time),
ts,
time};
if (description != lastMessage_) {
lastMessage_ = description;
emit lastMessageChanged();
}
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))));
if (description != lastMessage_) {
lastMessage_ = description;
emit lastMessageChanged();
void
TimelineModel::setCurrentIndex(int index)
{
auto oldIndex = idToIndex(currentId);
currentId = indexToId(index);
if (index != oldIndex)
emit currentIndexChanged(index);
if (MainWindow::instance() != QGuiApplication::focusWindow())
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());
}
});
MTRNord
committed
TimelineModel::displayName(const QString &id) const
return cache::displayName(room_id_, id).toHtmlEscaped();
MTRNord
committed
TimelineModel::avatarUrl(const 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(QStringLiteral("[^a-zA-Z]*y+[^a-zA-Z]*"));
TimelineModel::viewRawMessage(const QString &id)
auto e = events.get(id.toStdString(), "", false);
if (!e)
return;
std::string ev = mtx::accessors::serialize_event(*e).dump(4);
emit showRawMessageDialog(QString::fromStdString(ev));
TimelineModel::forwardMessage(const QString &eventId, QString roomId)
auto e = events.get(eventId.toStdString(), "");
if (!e)
return;
MTRNord
committed
emit forwardToRoom(e, std::move(roomId));
MTRNord
committed
TimelineModel::viewDecryptedRawMessage(const QString &id)
auto e = events.get(id.toStdString(), "");
if (!e)
return;
std::string ev = mtx::accessors::serialize_event(*e).dump(4);
emit showRawMessageDialog(QString::fromStdString(ev));
Nicolas Werner
committed
TimelineModel::openUserProfile(QString userid)
MTRNord
committed
UserProfile *userProfile = new UserProfile(room_id_, std::move(userid), manager_, this);
connect(this, &TimelineModel::roomAvatarUrlChanged, userProfile, &UserProfile::updateAvatarUrl);
emit manager_->openProfile(userProfile);
MTRNord
committed
TimelineModel::replyAction(const QString &id)
TimelineModel::unpin(const QString &id)
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
{
auto pinned =
cache::client()->getStateEvent<mtx::events::state::PinnedEvents>(room_id_.toStdString());
mtx::events::state::PinnedEvents content{};
if (pinned)
content = pinned->content;
auto idStr = id.toStdString();
for (auto it = content.pinned.begin(); it != content.pinned.end(); ++it) {
if (*it == idStr) {
content.pinned.erase(it);
break;
}
}
http::client()->send_state_event(
room_id_.toStdString(),
content,
[idStr](const mtx::responses::EventId &, mtx::http::RequestErr err) {
if (err)
nhlog::net()->error("Failed to unpin {}: {}", idStr, *err);
else
nhlog::net()->debug("Unpinned {}", idStr);
});
}
void
TimelineModel::pin(const QString &id)
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
{
auto pinned =
cache::client()->getStateEvent<mtx::events::state::PinnedEvents>(room_id_.toStdString());
mtx::events::state::PinnedEvents content{};
if (pinned)
content = pinned->content;
auto idStr = id.toStdString();
content.pinned.push_back(idStr);
http::client()->send_state_event(
room_id_.toStdString(),
content,
[idStr](const mtx::responses::EventId &, mtx::http::RequestErr err) {
if (err)
nhlog::net()->error("Failed to pin {}: {}", idStr, *err);
else
nhlog::net()->debug("Pinned {}", idStr);
});
}
void
TimelineModel::editAction(QString id)
{
TimelineModel::relatedInfo(const QString &id)
auto event = events.get(id.toStdString(), "");
if (!event)
return {};
return utils::stripReplyFallbacks(*event, id.toStdString(), room_id_);
}
void
TimelineModel::showReadReceipts(QString id)
emit openReadReceiptsDialog(new ReadReceiptsProxy{id, roomId(), this});
TimelineModel::redactEvent(const QString &id)
if (!id.isEmpty()) {
auto edits = events.edits(id.toStdString());
http::client()->redact_event(
room_id_.toStdString(),
id.toStdString(),
[this, id](const mtx::responses::EventId &, mtx::http::RequestErr err) {
if (err) {
emit redactionFailed(tr("Message redaction failed: %1")
.arg(QString::fromStdString(err->matrix_error.error)));
return;
}
// redact all edits to prevent leaks
for (const auto &e : edits) {
const auto &id_ = mtx::accessors::event_id(e);
http::client()->redact_event(
room_id_.toStdString(),
id_,
[this, id, id_](const mtx::responses::EventId &, mtx::http::RequestErr err) {
if (err) {
emit redactionFailed(tr("Message redaction failed: %1")
.arg(QString::fromStdString(err->matrix_error.error)));
return;
}
emit dataAtIdChanged(id);
});
}
}
TimelineModel::idToIndex(const QString &id) const
auto idx = events.idToIndex(id.toStdString());
if (idx)
return events.size() - *idx - 1;
else
return -1;
}
QString
TimelineModel::indexToId(int index) const
{
auto id = events.indexToId(events.size() - index - 1);
return id ? QString::fromStdString(*id) : QLatin1String("");
void
TimelineModel::markEventsAsRead(const std::vector<QString> &event_ids)
{
for (const auto &id : event_ids) {
read.insert(id);
int idx = idToIndex(id);
if (idx < 0) {
return;
emit dataChanged(index(idx, 0), index(idx, 0));
}
TimelineModel::sendEncryptedMessage(mtx::events::RoomEvent<T> msg, mtx::events::EventType eventType)
const auto room_id = room_id_.toStdString();
using namespace mtx::events;
using namespace mtx::identifiers;
json doc = {{"type", mtx::events::to_string(eventType)},
{"content", json(msg.content)},
{"room_id", room_id}};
try {
mtx::events::EncryptedEvent<mtx::events::msg::Encrypted> event;
event.content = olm::encrypt_group_message(room_id, http::client()->device_id(), doc);
event.event_id = msg.event_id;
event.room_id = room_id;
event.sender = http::client()->user_id().to_string();
event.type = mtx::events::EventType::RoomEncrypted;
event.origin_server_ts = QDateTime::currentMSecsSinceEpoch();
emit this->addPendingMessageToStore(event);
// TODO: Let the user know about the errors.
} catch (const lmdb::error &e) {
nhlog::db()->critical("failed to open outbound megolm session ({}): {}", room_id, e.what());
emit ChatPage::instance()->showNotification(
tr("Failed to encrypt event, sending aborted!"));
} catch (const mtx::crypto::olm_exception &e) {
nhlog::crypto()->critical(
"failed to open outbound megolm session ({}): {}", room_id, e.what());
emit ChatPage::instance()->showNotification(
tr("Failed to encrypt event, sending aborted!"));
}
}
struct SendMessageVisitor
{
explicit SendMessageVisitor(TimelineModel *model)
: model_(model)
{}
template<typename T, mtx::events::EventType Event>
void sendRoomEvent(mtx::events::RoomEvent<T> msg)
{
if (cache::isRoomEncrypted(model_->room_id_.toStdString())) {
auto encInfo = mtx::accessors::file(msg);
if (encInfo)
emit model_->newEncryptedImage(encInfo.value());
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
1435
1436
1437
model_->sendEncryptedMessage(msg, Event);
} else {
msg.type = Event;
emit model_->addPendingMessageToStore(msg);
}
}
// Do-nothing operator for all unhandled events
template<typename T>
void operator()(const mtx::events::Event<T> &)
{}
// Operator for m.room.message events that contain a msgtype in their content
template<typename T,
std::enable_if_t<std::is_same<decltype(T::msgtype), std::string>::value, int> = 0>
void operator()(mtx::events::RoomEvent<T> msg)
{
sendRoomEvent<T, mtx::events::EventType::RoomMessage>(msg);
}
// Special operator for reactions, which are a type of m.room.message, but need to be
// handled distinctly for their differences from normal room messages. Specifically,
// reactions need to have the relation outside of ciphertext, or synapse / the homeserver
// cannot handle it correctly. See the MSC for more details:
// https://github.com/matrix-org/matrix-doc/blob/matthew/msc1849/proposals/1849-aggregations.md#end-to-end-encryption
void operator()(mtx::events::RoomEvent<mtx::events::msg::Reaction> msg)
{
msg.type = mtx::events::EventType::Reaction;
emit model_->addPendingMessageToStore(msg);
}
void operator()(const mtx::events::RoomEvent<mtx::events::msg::CallInvite> &event)
{
sendRoomEvent<mtx::events::msg::CallInvite, mtx::events::EventType::CallInvite>(event);
}
void operator()(const mtx::events::RoomEvent<mtx::events::msg::CallCandidates> &event)
{
sendRoomEvent<mtx::events::msg::CallCandidates, mtx::events::EventType::CallCandidates>(
event);
}
void operator()(const mtx::events::RoomEvent<mtx::events::msg::CallAnswer> &event)
{
sendRoomEvent<mtx::events::msg::CallAnswer, mtx::events::EventType::CallAnswer>(event);
}
void operator()(const mtx::events::RoomEvent<mtx::events::msg::CallHangUp> &event)
{
sendRoomEvent<mtx::events::msg::CallHangUp, mtx::events::EventType::CallHangUp>(event);
}
void operator()(const mtx::events::RoomEvent<mtx::events::msg::KeyVerificationRequest> &msg)
{
sendRoomEvent<mtx::events::msg::KeyVerificationRequest,
mtx::events::EventType::RoomMessage>(msg);
}
void operator()(const mtx::events::RoomEvent<mtx::events::msg::KeyVerificationReady> &msg)
{
sendRoomEvent<mtx::events::msg::KeyVerificationReady,
mtx::events::EventType::KeyVerificationReady>(msg);
}
void operator()(const mtx::events::RoomEvent<mtx::events::msg::KeyVerificationStart> &msg)
{
sendRoomEvent<mtx::events::msg::KeyVerificationStart,
mtx::events::EventType::KeyVerificationStart>(msg);
}
void operator()(const mtx::events::RoomEvent<mtx::events::msg::KeyVerificationAccept> &msg)
{
sendRoomEvent<mtx::events::msg::KeyVerificationAccept,
mtx::events::EventType::KeyVerificationAccept>(msg);
}
void operator()(const mtx::events::RoomEvent<mtx::events::msg::KeyVerificationMac> &msg)
{
sendRoomEvent<mtx::events::msg::KeyVerificationMac,
mtx::events::EventType::KeyVerificationMac>(msg);
}
void operator()(const mtx::events::RoomEvent<mtx::events::msg::KeyVerificationKey> &msg)
{
sendRoomEvent<mtx::events::msg::KeyVerificationKey,
mtx::events::EventType::KeyVerificationKey>(msg);
}
void operator()(const mtx::events::RoomEvent<mtx::events::msg::KeyVerificationDone> &msg)
{
sendRoomEvent<mtx::events::msg::KeyVerificationDone,
mtx::events::EventType::KeyVerificationDone>(msg);
}
void operator()(const mtx::events::RoomEvent<mtx::events::msg::KeyVerificationCancel> &msg)
{
sendRoomEvent<mtx::events::msg::KeyVerificationCancel,
mtx::events::EventType::KeyVerificationCancel>(msg);
}
void operator()(mtx::events::Sticker msg)
{
msg.type = mtx::events::EventType::Sticker;
if (cache::isRoomEncrypted(model_->room_id_.toStdString())) {
model_->sendEncryptedMessage(msg, mtx::events::EventType::Sticker);
} else
emit model_->addPendingMessageToStore(msg);
}
};
void
TimelineModel::addPendingMessage(mtx::events::collections::TimelineEvents event)
{
std::visit(
[](auto &msg) {
// gets overwritten for reactions and stickers in SendMessageVisitor
msg.type = mtx::events::EventType::RoomMessage;
msg.event_id = "m" + http::client()->generate_txn_id();
msg.sender = http::client()->user_id().to_string();
msg.origin_server_ts = QDateTime::currentMSecsSinceEpoch();
},
event);
std::visit(SendMessageVisitor{this}, event);
MTRNord
committed
TimelineModel::openMedia(const QString &eventId)
cacheMedia(eventId, [](const QString &filename) {
QDesktopServices::openUrl(QUrl::fromLocalFile(filename));
});
TimelineModel::saveMedia(const QString &eventId) const
mtx::events::collections::TimelineEvents *event = events.get(eventId.toStdString(), "");
if (!event)
return false;
QString mxcUrl = QString::fromStdString(mtx::accessors::url(*event));
QString originalFilename = QString::fromStdString(mtx::accessors::filename(*event));
QString mimeType = QString::fromStdString(mtx::accessors::mimetype(*event));
auto encryptionInfo = mtx::accessors::file(*event);
qml_mtx_events::EventType eventType = toRoomEventType(*event);
QString dialogTitle;
if (eventType == qml_mtx_events::EventType::ImageMessage) {
dialogTitle = tr("Save image");
} else if (eventType == qml_mtx_events::EventType::VideoMessage) {
dialogTitle = tr("Save video");
} else if (eventType == qml_mtx_events::EventType::AudioMessage) {
dialogTitle = tr("Save audio");
} else {
dialogTitle = tr("Save file");
}
const QString filterString = QMimeDatabase().mimeTypeForName(mimeType).filterString();
const QString downloadsFolder =
QStandardPaths::writableLocation(QStandardPaths::DownloadLocation);
const QString openLocation = downloadsFolder + "/" + originalFilename;
QFileDialog::getSaveFileName(nullptr, dialogTitle, openLocation, filterString);
if (filename.isEmpty())
return false;
http::client()->download(url,
[filename, url, encryptionInfo](const std::string &data,
const std::string &,
const std::string &,
mtx::http::RequestErr err) {
if (err) {
nhlog::net()->warn("failed to retrieve image {}: {} {}",
url,
err->matrix_error.error,
static_cast<int>(err->status_code));
return;
}
try {
auto temp = data;
if (encryptionInfo)
temp = mtx::crypto::to_string(
mtx::crypto::decrypt_file(temp, encryptionInfo.value()));
QFile file(filename);
if (!file.open(QIODevice::WriteOnly))
return;
file.write(QByteArray(temp.data(), (int)temp.size()));
file.close();
return;
} catch (const std::exception &e) {
nhlog::ui()->warn("Error while saving file to: {}", e.what());
}
});
return true;
TimelineModel::cacheMedia(const QString &eventId,
const std::function<void(const QString)> &callback)
mtx::events::collections::TimelineEvents *event = events.get(eventId.toStdString(), "");
if (!event)
return;
QString mxcUrl = QString::fromStdString(mtx::accessors::url(*event));
QString mimeType = QString::fromStdString(mtx::accessors::mimetype(*event));
auto encryptionInfo = mtx::accessors::file(*event);
// If the message is a link to a non mxcUrl, don't download it
if (!mxcUrl.startsWith(QLatin1String("mxc://"))) {
emit mediaCached(mxcUrl, mxcUrl);
return;
}
QString suffix = QMimeDatabase().mimeTypeForName(mimeType).preferredSuffix();
const auto url = mxcUrl.toStdString();
const auto name = QString(mxcUrl).remove(QStringLiteral("mxc://"));
QStringLiteral("%1/media_cache/%2.%3")
.arg(QStandardPaths::writableLocation(QStandardPaths::CacheLocation), name, suffix));
if (QDir::cleanPath(name) != name) {
nhlog::net()->warn("mxcUrl '{}' is not safe, not downloading file", url);
return;
}
emit mediaCached(mxcUrl, filename.filePath());
emit mediaCached(mxcUrl, "file://" + filename.filePath());
if (callback) {
callback(filename.filePath());
1582
1583
1584
1585
1586
1587
1588
1589
1590
1591
1592
1593
1594
1595
1596
1597
1598
1599
1600
1601
1602
1603
1604
1605
1606
1607
1608
1609
1610
1611
1612
1613
1614
1615
1616
1617
1618
return;
}
http::client()->download(
url,
[this, callback, mxcUrl, filename, url, encryptionInfo](const std::string &data,
const std::string &,
const std::string &,
mtx::http::RequestErr err) {
if (err) {
nhlog::net()->warn("failed to retrieve image {}: {} {}",
url,
err->matrix_error.error,
static_cast<int>(err->status_code));
return;
}
try {
auto temp = data;
if (encryptionInfo)
temp =
mtx::crypto::to_string(mtx::crypto::decrypt_file(temp, encryptionInfo.value()));
QFile file(filename.filePath());
if (!file.open(QIODevice::WriteOnly))
return;
file.write(QByteArray(temp.data(), (int)temp.size()));
file.close();
if (callback) {
callback(filename.filePath());
}
} catch (const std::exception &e) {
nhlog::ui()->warn("Error while saving file to: {}", e.what());
}
emit mediaCached(mxcUrl, filename.filePath());
emit mediaCached(mxcUrl, "file://" + filename.filePath());
TimelineModel::cacheMedia(const QString &eventId)
cacheMedia(eventId, nullptr);
void
TimelineModel::showEvent(QString eventId)
{
using namespace std::chrono_literals;
// Direct to eventId
if (eventId[0] == '$') {
int idx = idToIndex(eventId);
if (idx == -1) {
nhlog::ui()->warn("Scrolling to event id {}, failed - no known index",
eventId.toStdString());
return;
eventIdToShow = eventId;
emit scrollTargetChanged();
showEventTimer.start(50ms);
return;
}
// to message index
eventId = indexToId(eventId.toInt());
eventIdToShow = eventId;
emit scrollTargetChanged();
showEventTimer.start(50ms);
return;
}
void
TimelineModel::eventShown()
{
eventIdToShow.clear();
emit scrollTargetChanged();
}
QString
TimelineModel::scrollTarget() const
{
}
void
TimelineModel::scrollTimerEvent()
{
if (eventIdToShow.isEmpty() || showEventTimerCounter > 3) {
showEventTimer.stop();
showEventTimerCounter = 0;
} else {
emit scrollToIndex(idToIndex(eventIdToShow));
showEventTimerCounter++;
}
TimelineModel::requestKeyForEvent(const QString &id)
auto encrypted_event = events.get(id.toStdString(), "", false);
if (encrypted_event) {
if (auto ev = std::get_if<mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>>(
encrypted_event))
events.requestSession(*ev, true);
}
TimelineModel::copyLinkToEvent(const QString &eventId) const
auto alias = cache::client()->getRoomAliases(room_id_.toStdString());
QString room;
if (alias) {
room = QString::fromStdString(alias->alias);
if (room.isEmpty() && !alias->alt_aliases.empty()) {
room = QString::fromStdString(alias->alt_aliases.front());
if (room.isEmpty())
room = room_id_;
vias.push_back(QStringLiteral("via=%1").arg(QString(
QUrl::toPercentEncoding(QString::fromStdString(http::client()->user_id().hostname())))));
auto members = cache::getMembers(room_id_.toStdString(), 0, 100);
for (const auto &m : members) {
if (vias.size() >= 4)
break;
auto user_id = mtx::identifiers::parse<mtx::identifiers::User>(m.user_id.toStdString());
QString server = QStringLiteral("via=%1").arg(
QString(QUrl::toPercentEncoding(QString::fromStdString(user_id.hostname()))));
if (!vias.contains(server))
vias.push_back(server);
}
auto link = QStringLiteral("https://matrix.to/#/%1/%2?%3")
.arg(QString(QUrl::toPercentEncoding(room)),
QString(QUrl::toPercentEncoding(eventId)),
vias.join('&'));
QGuiApplication::clipboard()->setText(link);
MTRNord
committed
TimelineModel::formatTypingUsers(const std::vector<QString> &users, const QColor &bg)
QString temp =
tr("%1 and %2 are typing.",
"Multiple users are typing. First argument is a comma separated list of potentially "
"multiple users. Second argument is the last user of that list. (If only one user is "
"typing, %1 is empty. You should still use it in your string though to silence Qt "
"warnings.)",
(int)users.size());
auto formatUser = [this, bg](const QString &user_id) -> QString {
auto uncoloredUsername = utils::replaceEmoji(displayName(user_id));
QString prefix =
QStringLiteral("<font color=\"%1\">").arg(manager_->userColor(user_id, bg).name());
// color only parts that don't have a font already specified
QString coloredUsername;
int index = 0;
do {
auto startIndex = uncoloredUsername.indexOf(QLatin1String("<font"), index);
if (startIndex - index != 0)
coloredUsername +=
prefix + uncoloredUsername.mid(index, startIndex > 0 ? startIndex - index : -1) +
QStringLiteral("</font>");
auto endIndex = uncoloredUsername.indexOf(QLatin1String("</font>"), startIndex);
if (endIndex > 0)
endIndex += sizeof("</font>") - 1;
coloredUsername +=
QStringView(uncoloredUsername).mid(startIndex, endIndex - startIndex);
index = endIndex;
} while (index > 0 && index < uncoloredUsername.size());
uidWithoutLast.reserve(static_cast<int>(users.size()));
for (size_t i = 0; i + 1 < users.size(); i++) {
uidWithoutLast.append(formatUser(users[i]));
}
return temp.arg(uidWithoutLast.join(QStringLiteral(", ")), formatUser(users.back()));
TimelineModel::formatJoinRuleEvent(const QString &id)
mtx::events::collections::TimelineEvents *e = events.get(id.toStdString(), "");
if (!e)
auto event = std::get_if<mtx::events::StateEvent<mtx::events::state::JoinRules>>(e);
if (!event)
QString user = QString::fromStdString(event->sender);
QString name = utils::replaceEmoji(displayName(user));
switch (event->content.join_rule) {
case mtx::events::state::JoinRule::Public:
return tr("%1 opened the room to the public.").arg(name);
case mtx::events::state::JoinRule::Invite:
return tr("%1 made this room require and invitation to join.").arg(name);
case mtx::events::state::JoinRule::Knock:
return tr("%1 allowed to join this room by knocking.").arg(name);
case mtx::events::state::JoinRule::Restricted: {
QStringList rooms;
for (const auto &r : event->content.allow) {
if (r.type == mtx::events::state::JoinAllowanceType::RoomMembership)
rooms.push_back(QString::fromStdString(r.room_id));
}
return tr("%1 allowed members of the following rooms to automatically join this "
"room: %2")
.arg(name, rooms.join(QStringLiteral(", ")));
}
default:
// Currently, knock and private are reserved keywords and not implemented in Matrix.
TimelineModel::formatGuestAccessEvent(const QString &id)
mtx::events::collections::TimelineEvents *e = events.get(id.toStdString(), "");
if (!e)
auto event = std::get_if<mtx::events::StateEvent<mtx::events::state::GuestAccess>>(e);
if (!event)
QString user = QString::fromStdString(event->sender);
QString name = utils::replaceEmoji(displayName(user));
switch (event->content.guest_access) {
case mtx::events::state::AccessState::CanJoin:
return tr("%1 made the room open to guests.").arg(name);
case mtx::events::state::AccessState::Forbidden:
return tr("%1 has closed the room to guest access.").arg(name);
default:
TimelineModel::formatHistoryVisibilityEvent(const QString &id)
mtx::events::collections::TimelineEvents *e = events.get(id.toStdString(), "");
if (!e)
auto event = std::get_if<mtx::events::StateEvent<mtx::events::state::HistoryVisibility>>(e);
QString user = QString::fromStdString(event->sender);
QString name = utils::replaceEmoji(displayName(user));
switch (event->content.history_visibility) {
case mtx::events::state::Visibility::WorldReadable:
return tr("%1 made the room history world readable. Events may be now read by "
"non-joined people.")
.arg(name);
case mtx::events::state::Visibility::Shared:
return tr("%1 set the room history visible to members from this point on.").arg(name);
case mtx::events::state::Visibility::Invited:
return tr("%1 set the room history visible to members since they were invited.").arg(name);
case mtx::events::state::Visibility::Joined:
return tr("%1 set the room history visible to members since they joined the room.")
.arg(name);
default:
TimelineModel::formatPowerLevelEvent(const QString &id)
mtx::events::collections::TimelineEvents *e = events.get(id.toStdString(), "");
if (!e)
auto event = std::get_if<mtx::events::StateEvent<mtx::events::state::PowerLevels>>(e);
if (!event)
QString user = QString::fromStdString(event->sender);
QString name = utils::replaceEmoji(displayName(user));
// TODO: power levels rendering is actually a bit complex. work on this later.
return tr("%1 has changed the room's permissions.").arg(name);
TimelineModel::formatRedactedEvent(const QString &id)
{
QVariantMap pair{{"first", ""}, {"second", ""}};
mtx::events::collections::TimelineEvents *e = events.get(id.toStdString(), "");
if (!e)
return pair;
auto event = std::get_if<mtx::events::RoomEvent<mtx::events::msg::Redacted>>(e);
if (!event)
return pair;
QString dateTime = QDateTime::fromMSecsSinceEpoch(event->origin_server_ts).toString();
QString reason = QLatin1String("");
auto because = event->unsigned_data.redacted_because;
// User info about who actually sent the redacted event.
QString redactedUser;
QString redactedName;
redactedUser = QString::fromStdString(because->sender).toHtmlEscaped();
redactedName = utils::replaceEmoji(displayName(redactedUser));
reason = QString::fromStdString(because->content.reason).toHtmlEscaped();
pair[QStringLiteral("first")] = tr("Removed by %1").arg(redactedName);
pair[QStringLiteral("second")] =
tr("%1 (%2) removed this message at %3").arg(redactedName, redactedUser, dateTime);
} else {
pair[QStringLiteral("first")] = tr("Removed by %1 because: %2").arg(redactedName, reason);
pair[QStringLiteral("second")] = tr("%1 (%2) removed this message at %3\nReason: %4")
.arg(redactedName, redactedUser, dateTime, reason);
TimelineModel::acceptKnock(const QString &id)
mtx::events::collections::TimelineEvents *e = events.get(id.toStdString(), "");
if (!e)
return;
auto event = std::get_if<mtx::events::StateEvent<mtx::events::state::Member>>(e);
if (!event)
return;
if (!permissions_.canInvite())
return;
if (cache::isRoomMember(event->state_key, room_id_.toStdString()))
return;
using namespace mtx::events::state;
if (event->content.membership != Membership::Knock)
return;
ChatPage::instance()->inviteUser(QString::fromStdString(event->state_key), QLatin1String(""));
TimelineModel::showAcceptKnockButton(const QString &id)
mtx::events::collections::TimelineEvents *e = events.get(id.toStdString(), "");
if (!e)
return false;
auto event = std::get_if<mtx::events::StateEvent<mtx::events::state::Member>>(e);
if (!event)
return false;
if (!permissions_.canInvite())
return false;
if (cache::isRoomMember(event->state_key, room_id_.toStdString()))
return false;
using namespace mtx::events::state;
return event->content.membership == Membership::Knock;
TimelineModel::formatMemberEvent(const QString &id)
mtx::events::collections::TimelineEvents *e = events.get(id.toStdString(), "");
if (!e)
auto event = std::get_if<mtx::events::StateEvent<mtx::events::state::Member>>(e);
if (!event)
mtx::events::StateEvent<mtx::events::state::Member> *prevEvent = nullptr;
if (!event->unsigned_data.replaces_state.empty()) {
auto tempPrevEvent = events.get(event->unsigned_data.replaces_state, event->event_id);
if (tempPrevEvent) {
prevEvent =
std::get_if<mtx::events::StateEvent<mtx::events::state::Member>>(tempPrevEvent);