Newer
Older
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();
emit roomMemberCountChanged();
} else if (std::holds_alternative<StateEvent<state::Encryption>>(e)) {
this->isEncrypted_ = cache::isRoomEncrypted(room_id_.toStdString());
emit encryptionChanged();
} else if (std::holds_alternative<StateEvent<state::space::Parent>>(e)) {
this->parentChecked = false;
emit parentSpaceChanged();
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>
{
}
template<typename T>
auto
isMessage(const mtx::events::Event<T> &)
{
template<typename T>
auto
isMessage(const mtx::events::EncryptedEvent<T> &)
{
isMessage(const mtx::events::RoomEvent<mtx::events::voip::CallInvite> &)
isMessage(const mtx::events::RoomEvent<mtx::events::voip::CallAnswer> &)
isMessage(const mtx::events::RoomEvent<mtx::events::voip::CallHangUp> &)
auto
isMessage(const mtx::events::RoomEvent<mtx::events::voip::CallReject> &)
{
return true;
}
auto
isMessage(const mtx::events::RoomEvent<mtx::events::voip::CallSelectAnswer> &)
{
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> &)
{
DescInfo
TimelineModel::lastMessage() const
{
if (lastMessage_.event_id.isEmpty())
QTimer::singleShot(0, this, &TimelineModel::updateLastMessage);
return lastMessage_;
}
void
TimelineModel::updateLastMessage()
// only try to generate a preview for the last 1000 messages
auto end = std::max(events.size() - 1001, 0);
for (auto it = events.size() - 1; it >= end; --it) {
auto event = events.get(it, decryptDescription);
if (!event)
continue;
if (std::visit([](const auto &e) -> bool { return isYourJoin(e); }, *event)) {
auto time = mtx::accessors::origin_server_ts(*event);
uint64_t ts = time.toMSecsSinceEpoch();
auto description =
DescInfo{QString::fromStdString(mtx::accessors::event_id(*event)),
QString::fromStdString(http::client()->user_id().to_string()),
tr("You joined this room."),
utils::descriptiveTime(time),
ts,
time};
if (description != lastMessage_) {
if (lastMessage_.timestamp == 0) {
cache::client()->updateLastMessageTimestamp(room_id_.toStdString(),
description.timestamp);
}
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_) {
if (lastMessage_.timestamp == 0) {
cache::client()->updateLastMessageTimestamp(room_id_.toStdString(),
description.timestamp);
}
lastMessage_ = description;
emit lastMessageChanged();
void
TimelineModel::setCurrentIndex(int index)
{
auto oldIndex = idToIndex(currentId);
currentId = indexToId(index);
if (index != oldIndex)
emit currentIndexChanged(index);
if (!QGuiApplication::focusWindow() || !QGuiApplication::focusWindow()->isActive() ||
MainWindow::instance()->windowForRoom(roomId()) != 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());
}
},
!UserSettings::instance()->readReceipts());
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);
TimelineModel::unpin(const QString &id)
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
{
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)
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
{
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);
});
}
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(const QString &id)
emit openReadReceiptsDialog(new ReadReceiptsProxy{id, roomId(), this});
void
TimelineModel::redactAllFromUser(const QString &userid, const QString &reason)
{
auto user = userid.toStdString();
std::vector<QString> toRedact;
for (auto it = events.size() - 1; it >= 0; --it) {
auto event = events.get(it, false);
if (event && mtx::accessors::sender(*event) == user &&
!std::holds_alternative<mtx::events::RoomEvent<mtx::events::msg::Redacted>>(*event)) {
toRedact.push_back(QString::fromStdString(mtx::accessors::event_id(*event)));
}
}
for (const auto &e : toRedact) {
redactEvent(e, reason);
std::this_thread::sleep_for(std::chrono::milliseconds(50));
}
}
TimelineModel::redactEvent(const QString &id, const QString &reason)
if (!id.isEmpty()) {
auto edits = events.edits(id.toStdString());
http::client()->redact_event(
room_id_.toStdString(),
id.toStdString(),
[this, id, reason](const mtx::responses::EventId &, mtx::http::RequestErr err) {
if (err->status_code == 429 && err->matrix_error.retry_after.count() != 0) {
QTimer::singleShot(err->matrix_error.retry_after, this, [this, id, reason]() {
this->redactEvent(id, reason);
});
return;
}
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::updateLastReadId(const QString ¤tRoomId)
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
{
if (currentRoomId == room_id_) {
last_event_id = cache::getFullyReadEventId(room_id_.toStdString());
auto lastVisibleEventIndexAndId =
cache::lastVisibleEvent(room_id_.toStdString(), last_event_id);
if (lastVisibleEventIndexAndId) {
fullyReadEventId_ = lastVisibleEventIndexAndId->second;
emit fullyReadEventIdChanged();
}
}
}
void
TimelineModel::lastReadIdOnWindowFocus()
{
/* this stops it from removing the line when focusing another window
* and from removing the line when refocusing nheko */
if (ChatPage::instance()->isRoomActive(room_id_) &&
cache::calculateRoomReadStatus(room_id_.toStdString())) {
updateLastReadId(room_id_);
}
}
/*
* if the event2order db didn't have the messages we needed when the room was opened
* try again after these new messages were fetched
*/
void
TimelineModel::checkAfterFetch()
{
if (fullyReadEventId_.empty()) {
auto lastVisibleEventIndexAndId =
cache::lastVisibleEvent(room_id_.toStdString(), last_event_id);
if (lastVisibleEventIndexAndId) {
fullyReadEventId_ = lastVisibleEventIndexAndId->second;
emit fullyReadEventIdChanged();
}
}
}
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;
nlohmann::json doc = {{"type", mtx::events::to_string(eventType)},
{"content", nlohmann::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());
encInfo = mtx::accessors::thumbnail_file(msg);
if (encInfo)
emit model_->newEncryptedImage(encInfo.value());
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::voip::CallInvite> &event)
sendRoomEvent<mtx::events::voip::CallInvite, mtx::events::EventType::CallInvite>(event);
void operator()(const mtx::events::RoomEvent<mtx::events::voip::CallCandidates> &event)
sendRoomEvent<mtx::events::voip::CallCandidates, mtx::events::EventType::CallCandidates>(
void operator()(const mtx::events::RoomEvent<mtx::events::voip::CallAnswer> &event)
sendRoomEvent<mtx::events::voip::CallAnswer, mtx::events::EventType::CallAnswer>(event);
void operator()(const mtx::events::RoomEvent<mtx::events::voip::CallHangUp> &event)
sendRoomEvent<mtx::events::voip::CallHangUp, mtx::events::EventType::CallHangUp>(event);
void operator()(const mtx::events::RoomEvent<mtx::events::voip::CallSelectAnswer> &event)
{
sendRoomEvent<mtx::events::voip::CallSelectAnswer,
mtx::events::EventType::CallSelectAnswer>(event);
}
void operator()(const mtx::events::RoomEvent<mtx::events::voip::CallReject> &event)
{
sendRoomEvent<mtx::events::voip::CallReject, mtx::events::EventType::CallReject>(event);
}
void operator()(const mtx::events::RoomEvent<mtx::events::voip::CallNegotiate> &event)
{
sendRoomEvent<mtx::events::voip::CallNegotiate, mtx::events::EventType::CallNegotiate>(
event);
}
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
1619
1620
1621
1622
1623
1624
1625
1626
1627
1628
1629
1630
1631
1632
1633
1634
1635
1636
1637
1638
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);
fullyReadEventId_ = this->EventId;
emit fullyReadEventIdChanged();
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());
1786
1787
1788
1789
1790
1791
1792
1793
1794
1795
1796
1797
1798
1799
1800
1801
1802
1803
1804
1805
1806
1807
1808
1809
1810
1811
1812
1813
1814
1815
1816
1817
1818
1819
1820
1821
1822
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);
}
QString
TimelineModel::getBareRoomLink(const QString &roomId)
cache::client()->getStateEvent<mtx::events::state::CanonicalAlias>(roomId.toStdString());
room = QString::fromStdString(alias->content.alias);
if (room.isEmpty() && !alias->content.alt_aliases.empty()) {
room = QString::fromStdString(alias->content.alt_aliases.front());
room = roomId;
return QStringLiteral("https://matrix.to/#/%1").arg(QString(QUrl::toPercentEncoding(room)));
}
QString
TimelineModel::getRoomVias(const QString &roomId)
{
QStringList vias;
for (const auto &m : utils::roomVias(roomId.toStdString())) {
QString server =
QStringLiteral("via=%1").arg(QString(QUrl::toPercentEncoding(QString::fromStdString(m))));
if (!vias.contains(server))
vias.push_back(server);
}
return vias.join("&");
}
void
TimelineModel::copyLinkToEvent(const QString &eventId) const
{
auto link = QStringLiteral("%1/%2?%3")
.arg(getBareRoomLink(room_id_),
QString(QUrl::toPercentEncoding(eventId)),
getRoomVias(room_id_));
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()));