aboutsummaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
Diffstat (limited to 'lib')
-rw-r--r--lib/avatar.cpp2
-rw-r--r--lib/connection.cpp78
-rw-r--r--lib/connection.h40
-rw-r--r--lib/connectiondata.cpp6
-rw-r--r--lib/csapi/account-data.cpp28
-rw-r--r--lib/csapi/account-data.h66
-rw-r--r--lib/csapi/capabilities.h8
-rw-r--r--lib/csapi/room_upgrades.h4
-rw-r--r--lib/events/event.cpp3
-rw-r--r--lib/events/eventcontent.cpp6
-rw-r--r--lib/events/eventcontent.h2
-rw-r--r--lib/events/roommemberevent.cpp2
-rw-r--r--lib/events/stateevent.cpp2
-rw-r--r--lib/jobs/basejob.cpp24
-rw-r--r--lib/jobs/downloadfilejob.cpp5
-rw-r--r--lib/jobs/mediathumbnailjob.cpp2
-rw-r--r--lib/networkaccessmanager.cpp3
-rw-r--r--lib/networksettings.cpp2
-rw-r--r--lib/room.cpp225
-rw-r--r--lib/room.h42
-rw-r--r--lib/settings.cpp21
-rw-r--r--lib/settings.h2
-rw-r--r--lib/syncdata.cpp8
-rw-r--r--lib/user.cpp42
-rw-r--r--lib/user.h6
-rw-r--r--lib/util.cpp45
-rw-r--r--lib/util.h12
27 files changed, 494 insertions, 192 deletions
diff --git a/lib/avatar.cpp b/lib/avatar.cpp
index c0ef3cba..9279ef9d 100644
--- a/lib/avatar.cpp
+++ b/lib/avatar.cpp
@@ -191,7 +191,7 @@ bool Avatar::Private::checkUrl(const QUrl& url) const
}
QString Avatar::Private::localFile() const {
- static const auto cachePath = cacheLocation("avatars");
+ static const auto cachePath = cacheLocation(QStringLiteral("avatars"));
return cachePath % _url.authority() % '_' % _url.fileName() % ".png";
}
diff --git a/lib/connection.cpp b/lib/connection.cpp
index 26b40c03..d75d8e56 100644
--- a/lib/connection.cpp
+++ b/lib/connection.cpp
@@ -133,6 +133,10 @@ class Connection::Private
packAndSendAccountData(
makeEvent<EventT>(std::forward<ContentT>(content)));
}
+ QString topLevelStatePath() const
+ {
+ return q->stateCacheDir().filePath("state.json");
+ }
};
Connection::Connection(const QUrl& server, QObject* parent)
@@ -271,8 +275,7 @@ void Connection::reloadCapabilities()
Q_ASSERT(!d->capabilities.roomVersions.omitted());
emit capabilitiesLoaded();
for (auto* r: d->roomMap)
- if (r->joinState() == JoinState::Join && r->successorId().isEmpty())
- r->checkVersion();
+ r->checkVersion();
});
}
@@ -322,11 +325,14 @@ void Connection::checkAndConnect(const QString& userId,
void Connection::logout()
{
auto job = callApi<LogoutJob>();
- connect( job, &LogoutJob::success, this, [this] {
- stopSync();
- d->data->setToken({});
- emit stateChanged();
- emit loggedOut();
+ connect( job, &LogoutJob::finished, this, [job,this] {
+ if (job->status().good() || job->error() == BaseJob::ContentAccessError)
+ {
+ stopSync();
+ d->data->setToken({});
+ emit stateChanged();
+ emit loggedOut();
+ }
});
}
@@ -610,8 +616,17 @@ CreateRoomJob* Connection::createRoom(RoomVisibility visibility,
: QStringLiteral("private"),
alias, name, topic, invites, invite3pids, roomVersion,
creationContent, initialState, presetName, isDirect);
- connect(job, &BaseJob::success, this, [this,job] {
- emit createdRoom(provideRoom(job->roomId(), JoinState::Join));
+ connect(job, &BaseJob::success, this, [this,job,invites,isDirect] {
+ auto* room = provideRoom(job->roomId(), JoinState::Join);
+ if (!room)
+ {
+ Q_ASSERT_X(room, "Connection::createRoom", "Failed to create a room");
+ return;
+ }
+ emit createdRoom(room);
+ if (isDirect)
+ for (const auto& i: invites)
+ addToDirectChats(room, user(i));
});
return job;
}
@@ -709,8 +724,8 @@ void Connection::doInDirectChat(User* u,
CreateRoomJob* Connection::createDirectChat(const QString& userId,
const QString& topic, const QString& name)
{
- return createRoom(UnpublishRoom, "", name, topic, {userId},
- "trusted_private_chat", {}, true);
+ return createRoom(UnpublishRoom, {}, name, topic, {userId},
+ QStringLiteral("trusted_private_chat"), {}, true);
}
ForgetRoomJob* Connection::forgetRoom(const QString& id)
@@ -789,6 +804,11 @@ QUrl Connection::homeserver() const
return d->data->baseUrl();
}
+QString Connection::domain() const
+{
+ return d->userId.section(':', 1);
+}
+
Room* Connection::room(const QString& roomId, JoinStates states) const
{
Room* room = d->roomMap.value({roomId, false}, nullptr);
@@ -951,7 +971,8 @@ QHash<QString, QVector<Room*>> Connection::tagsToRooms() const
QHash<QString, QVector<Room*>> result;
for (auto* r: qAsConst(d->roomMap))
{
- for (const auto& tagName: r->tagNames())
+ const auto& tagNames = r->tagNames();
+ for (const auto& tagName: tagNames)
result[tagName].push_back(r);
}
for (auto it = result.begin(); it != result.end(); ++it)
@@ -966,9 +987,12 @@ QStringList Connection::tagNames() const
{
QStringList tags ({FavouriteTag});
for (auto* r: qAsConst(d->roomMap))
- for (const auto& tag: r->tagNames())
+ {
+ const auto& tagNames = r->tagNames();
+ for (const auto& tag: tagNames)
if (tag != LowPriorityTag && !tags.contains(tag))
tags.push_back(tag);
+ }
tags.push_back(LowPriorityTag);
return tags;
}
@@ -1157,6 +1181,9 @@ Room* Connection::provideRoom(const QString& id, Omittable<JoinState> joinState)
emit leftRoom(room, prevInvite);
if (prevInvite)
{
+ const auto dcUsers = prevInvite->directChatUsers();
+ for (auto* u: dcUsers)
+ addToDirectChats(room, u);
qCDebug(MAIN) << "Deleting Invite state for room" << prevInvite->id();
emit prevInvite->beforeDestruction(prevInvite);
prevInvite->deleteLater();
@@ -1209,7 +1236,8 @@ void Connection::saveRoomState(Room* r) const
if (!d->cacheState)
return;
- QFile outRoomFile { stateCachePath() % SyncData::fileNameForRoom(r->id()) };
+ QFile outRoomFile {
+ stateCacheDir().filePath(SyncData::fileNameForRoom(r->id())) };
if (outRoomFile.open(QFile::WriteOnly))
{
QJsonDocument json { r->toJson() };
@@ -1230,7 +1258,7 @@ void Connection::saveState() const
QElapsedTimer et; et.start();
- QFile outFile { stateCachePath() % "state.json" };
+ QFile outFile { d->topLevelStatePath() };
if (!outFile.open(QFile::WriteOnly))
{
qCWarning(MAIN) << "Error opening" << outFile.fileName()
@@ -1248,18 +1276,19 @@ void Connection::saveState() const
{
QJsonObject rooms;
QJsonObject inviteRooms;
- for (const auto* i : roomMap()) // Pass on rooms in Leave state
+ const auto& rs = roomMap(); // Pass on rooms in Leave state
+ for (const auto* i : rs)
(i->joinState() == JoinState::Invite ? inviteRooms : rooms)
.insert(i->id(), QJsonValue::Null);
QJsonObject roomObj;
if (!rooms.isEmpty())
- roomObj.insert("join", rooms);
+ roomObj.insert(QStringLiteral("join"), rooms);
if (!inviteRooms.isEmpty())
- roomObj.insert("invite", inviteRooms);
+ roomObj.insert(QStringLiteral("invite"), inviteRooms);
- rootObj.insert("next_batch", d->data->lastEvent());
- rootObj.insert("rooms", roomObj);
+ rootObj.insert(QStringLiteral("next_batch"), d->data->lastEvent());
+ rootObj.insert(QStringLiteral("rooms"), roomObj);
}
{
QJsonArray accountDataEvents {
@@ -1269,7 +1298,7 @@ void Connection::saveState() const
accountDataEvents.append(
basicEventJson(e.first, e.second->contentJson()));
- rootObj.insert("account_data",
+ rootObj.insert(QStringLiteral("account_data"),
QJsonObject {{ QStringLiteral("events"), accountDataEvents }});
}
@@ -1289,7 +1318,7 @@ void Connection::loadState()
QElapsedTimer et; et.start();
- SyncData sync { stateCachePath() % "state.json" };
+ SyncData sync { d->topLevelStatePath() };
if (sync.nextBatch().isEmpty()) // No token means no cache by definition
return;
@@ -1307,6 +1336,11 @@ void Connection::loadState()
QString Connection::stateCachePath() const
{
+ return stateCacheDir().path() % '/';
+}
+
+QDir Connection::stateCacheDir() const
+{
auto safeUserId = userId();
safeUserId.replace(':', '_');
return cacheLocation(safeUserId);
diff --git a/lib/connection.h b/lib/connection.h
index b22d63da..2ff27ea6 100644
--- a/lib/connection.h
+++ b/lib/connection.h
@@ -26,6 +26,7 @@
#include <QtCore/QObject>
#include <QtCore/QUrl>
#include <QtCore/QSize>
+#include <QtCore/QDir>
#include <functional>
#include <memory>
@@ -104,6 +105,7 @@ namespace QMatrixClient
Q_PROPERTY(QByteArray accessToken READ accessToken NOTIFY stateChanged)
Q_PROPERTY(QString defaultRoomVersion READ defaultRoomVersion NOTIFY capabilitiesLoaded)
Q_PROPERTY(QUrl homeserver READ homeserver WRITE setHomeserver NOTIFY homeserverChanged)
+ Q_PROPERTY(QString domain READ domain NOTIFY homeserverChanged)
Q_PROPERTY(bool cacheState READ cacheState WRITE setCacheState NOTIFY cacheStateChanged)
Q_PROPERTY(bool lazyLoading READ lazyLoading WRITE setLazyLoading NOTIFY lazyLoadingChanged)
@@ -218,10 +220,10 @@ namespace QMatrixClient
QList<User*> directChatUsers(const Room* room) const;
/** Check whether a particular user is in the ignore list */
- bool isIgnored(const User* user) const;
+ Q_INVOKABLE bool isIgnored(const User* user) const;
/** Get the whole list of ignored users */
- IgnoredUsersList ignoredUsers() const;
+ Q_INVOKABLE IgnoredUsersList ignoredUsers() const;
/** Add the user to the ignore list
* The change signal is emitted synchronously, without waiting
@@ -229,19 +231,22 @@ namespace QMatrixClient
*
* \sa ignoredUsersListChanged
*/
- void addToIgnoredUsers(const User* user);
+ Q_INVOKABLE void addToIgnoredUsers(const User* user);
/** Remove the user from the ignore list */
/** Similar to adding, the change signal is emitted synchronously.
*
* \sa ignoredUsersListChanged
*/
- void removeFromIgnoredUsers(const User* user);
+ Q_INVOKABLE void removeFromIgnoredUsers(const User* user);
/** Get the full list of users known to this account */
QMap<QString, User*> users() const;
+ /** Get the base URL of the homeserver to connect to */
QUrl homeserver() const;
+ /** Get the domain name used for ids/aliases on the server */
+ QString domain() const;
/** Find a room by its id and a mask of applicable states */
Q_INVOKABLE Room* room(const QString& roomId,
JoinStates states = JoinState::Invite|JoinState::Join) const;
@@ -301,8 +306,8 @@ namespace QMatrixClient
* Call this before first sync to load from previously saved file.
*
* \param fromFile A local path to read the state from. Uses QUrl
- * to be QML-friendly. Empty parameter means using a path
- * defined by stateCachePath().
+ * to be QML-friendly. Empty parameter means saving to the directory
+ * defined by stateCachePath() / stateCacheDir().
*/
Q_INVOKABLE void loadState();
/**
@@ -311,23 +316,30 @@ namespace QMatrixClient
* loadState() on a next run of the client.
*
* \param toFile A local path to save the state to. Uses QUrl to be
- * QML-friendly. Empty parameter means using a path defined by
- * stateCachePath().
+ * QML-friendly. Empty parameter means saving to the directory
+ * defined by stateCachePath() / stateCacheDir().
*/
Q_INVOKABLE void saveState() const;
/// This method saves the current state of a single room.
void saveRoomState(Room* r) const;
+ /// Get the default directory path to save the room state to
+ /** \sa stateCacheDir */
+ Q_INVOKABLE QString stateCachePath() const;
+
+ /// Get the default directory to save the room state to
/**
- * The default path to store the cached room state, defined as
- * follows:
+ * This function returns the default directory to store the cached
+ * room state, defined as follows:
+ * \code
* QStandardPaths::writeableLocation(QStandardPaths::CacheLocation) + _safeUserId + "_state.json"
- * where `_safeUserId` is userId() with `:` (colon) replaced with
- * `_` (underscore)
- * /see loadState(), saveState()
+ * \endcode
+ * where `_safeUserId` is userId() with `:` (colon) replaced by
+ * `_` (underscore), as colons are reserved characters on Windows.
+ * \sa loadState, saveState, stateCachePath
*/
- Q_INVOKABLE QString stateCachePath() const;
+ QDir stateCacheDir() const;
bool cacheState() const;
void setCacheState(bool newValue);
diff --git a/lib/connectiondata.cpp b/lib/connectiondata.cpp
index eb516ef7..91cda09f 100644
--- a/lib/connectiondata.cpp
+++ b/lib/connectiondata.cpp
@@ -25,7 +25,7 @@ using namespace QMatrixClient;
struct ConnectionData::Private
{
- explicit Private(const QUrl& url) : baseUrl(url) { }
+ explicit Private(QUrl url) : baseUrl(std::move(url)) { }
QUrl baseUrl;
QByteArray accessToken;
@@ -37,7 +37,7 @@ struct ConnectionData::Private
};
ConnectionData::ConnectionData(QUrl baseUrl)
- : d(std::make_unique<Private>(baseUrl))
+ : d(std::make_unique<Private>(std::move(baseUrl)))
{ }
ConnectionData::~ConnectionData() = default;
@@ -98,7 +98,7 @@ QString ConnectionData::lastEvent() const
void ConnectionData::setLastEvent(QString identifier)
{
- d->lastEvent = identifier;
+ d->lastEvent = std::move(identifier);
}
QByteArray ConnectionData::generateTxnId() const
diff --git a/lib/csapi/account-data.cpp b/lib/csapi/account-data.cpp
index 5021c73a..96b32a92 100644
--- a/lib/csapi/account-data.cpp
+++ b/lib/csapi/account-data.cpp
@@ -21,6 +21,20 @@ SetAccountDataJob::SetAccountDataJob(const QString& userId, const QString& type,
setRequestData(Data(toJson(content)));
}
+QUrl GetAccountDataJob::makeRequestUrl(QUrl baseUrl, const QString& userId, const QString& type)
+{
+ return BaseJob::makeRequestUrl(std::move(baseUrl),
+ basePath % "/user/" % userId % "/account_data/" % type);
+}
+
+static const auto GetAccountDataJobName = QStringLiteral("GetAccountDataJob");
+
+GetAccountDataJob::GetAccountDataJob(const QString& userId, const QString& type)
+ : BaseJob(HttpVerb::Get, GetAccountDataJobName,
+ basePath % "/user/" % userId % "/account_data/" % type)
+{
+}
+
static const auto SetAccountDataPerRoomJobName = QStringLiteral("SetAccountDataPerRoomJob");
SetAccountDataPerRoomJob::SetAccountDataPerRoomJob(const QString& userId, const QString& roomId, const QString& type, const QJsonObject& content)
@@ -30,3 +44,17 @@ SetAccountDataPerRoomJob::SetAccountDataPerRoomJob(const QString& userId, const
setRequestData(Data(toJson(content)));
}
+QUrl GetAccountDataPerRoomJob::makeRequestUrl(QUrl baseUrl, const QString& userId, const QString& roomId, const QString& type)
+{
+ return BaseJob::makeRequestUrl(std::move(baseUrl),
+ basePath % "/user/" % userId % "/rooms/" % roomId % "/account_data/" % type);
+}
+
+static const auto GetAccountDataPerRoomJobName = QStringLiteral("GetAccountDataPerRoomJob");
+
+GetAccountDataPerRoomJob::GetAccountDataPerRoomJob(const QString& userId, const QString& roomId, const QString& type)
+ : BaseJob(HttpVerb::Get, GetAccountDataPerRoomJobName,
+ basePath % "/user/" % userId % "/rooms/" % roomId % "/account_data/" % type)
+{
+}
+
diff --git a/lib/csapi/account-data.h b/lib/csapi/account-data.h
index f3656a14..b067618f 100644
--- a/lib/csapi/account-data.h
+++ b/lib/csapi/account-data.h
@@ -22,8 +22,8 @@ namespace QMatrixClient
public:
/*! Set some account_data for the user.
* \param userId
- * The id of the user to set account_data for. The access token must be
- * authorized to make requests for this user id.
+ * The ID of the user to set account_data for. The access token must be
+ * authorized to make requests for this user ID.
* \param type
* The event type of the account_data to set. Custom types should be
* namespaced to avoid clashes.
@@ -33,6 +33,33 @@ namespace QMatrixClient
explicit SetAccountDataJob(const QString& userId, const QString& type, const QJsonObject& content = {});
};
+ /// Get some account_data for the user.
+ ///
+ /// Get some account_data for the client. This config is only visible to the user
+ /// that set the account_data.
+ class GetAccountDataJob : public BaseJob
+ {
+ public:
+ /*! Get some account_data for the user.
+ * \param userId
+ * The ID of the user to get account_data for. The access token must be
+ * authorized to make requests for this user ID.
+ * \param type
+ * The event type of the account_data to get. Custom types should be
+ * namespaced to avoid clashes.
+ */
+ explicit GetAccountDataJob(const QString& userId, const QString& type);
+
+ /*! Construct a URL without creating a full-fledged job object
+ *
+ * This function can be used when a URL for
+ * GetAccountDataJob is necessary but the job
+ * itself isn't.
+ */
+ static QUrl makeRequestUrl(QUrl baseUrl, const QString& userId, const QString& type);
+
+ };
+
/// Set some account_data for the user.
///
/// Set some account_data for the client on a given room. This config is only
@@ -43,10 +70,10 @@ namespace QMatrixClient
public:
/*! Set some account_data for the user.
* \param userId
- * The id of the user to set account_data for. The access token must be
- * authorized to make requests for this user id.
+ * The ID of the user to set account_data for. The access token must be
+ * authorized to make requests for this user ID.
* \param roomId
- * The id of the room to set account_data on.
+ * The ID of the room to set account_data on.
* \param type
* The event type of the account_data to set. Custom types should be
* namespaced to avoid clashes.
@@ -55,4 +82,33 @@ namespace QMatrixClient
*/
explicit SetAccountDataPerRoomJob(const QString& userId, const QString& roomId, const QString& type, const QJsonObject& content = {});
};
+
+ /// Get some account_data for the user.
+ ///
+ /// Get some account_data for the client on a given room. This config is only
+ /// visible to the user that set the account_data.
+ class GetAccountDataPerRoomJob : public BaseJob
+ {
+ public:
+ /*! Get some account_data for the user.
+ * \param userId
+ * The ID of the user to set account_data for. The access token must be
+ * authorized to make requests for this user ID.
+ * \param roomId
+ * The ID of the room to get account_data for.
+ * \param type
+ * The event type of the account_data to get. Custom types should be
+ * namespaced to avoid clashes.
+ */
+ explicit GetAccountDataPerRoomJob(const QString& userId, const QString& roomId, const QString& type);
+
+ /*! Construct a URL without creating a full-fledged job object
+ *
+ * This function can be used when a URL for
+ * GetAccountDataPerRoomJob is necessary but the job
+ * itself isn't.
+ */
+ static QUrl makeRequestUrl(QUrl baseUrl, const QString& userId, const QString& roomId, const QString& type);
+
+ };
} // namespace QMatrixClient
diff --git a/lib/csapi/capabilities.h b/lib/csapi/capabilities.h
index 39e2f4d1..06a8bf0d 100644
--- a/lib/csapi/capabilities.h
+++ b/lib/csapi/capabilities.h
@@ -39,8 +39,8 @@ namespace QMatrixClient
QHash<QString, QString> available;
};
- /// Gets information about the server's supported feature set
- /// and other relevant capabilities.
+ /// The custom capabilities the server supports, using the
+ /// Java package naming convention.
struct Capabilities
{
/// Capability to indicate if the user can change their password.
@@ -68,8 +68,8 @@ namespace QMatrixClient
// Result properties
- /// Gets information about the server's supported feature set
- /// and other relevant capabilities.
+ /// The custom capabilities the server supports, using the
+ /// Java package naming convention.
const Capabilities& capabilities() const;
protected:
diff --git a/lib/csapi/room_upgrades.h b/lib/csapi/room_upgrades.h
index 6f712f10..4da5941a 100644
--- a/lib/csapi/room_upgrades.h
+++ b/lib/csapi/room_upgrades.h
@@ -13,9 +13,7 @@ namespace QMatrixClient
/// Upgrades a room to a new room version.
///
- /// Upgrades the given room to a particular room version, migrating as much
- /// data as possible over to the new room. See the `room_upgrades <#room-upgrades>`_
- /// module for more information on what this entails.
+ /// Upgrades the given room to a particular room version.
class UpgradeRoomJob : public BaseJob
{
public:
diff --git a/lib/events/event.cpp b/lib/events/event.cpp
index c98dfbb6..6505d89a 100644
--- a/lib/events/event.cpp
+++ b/lib/events/event.cpp
@@ -38,7 +38,8 @@ event_type_t EventTypeRegistry::initializeTypeId(event_mtype_t matrixTypeId)
QString EventTypeRegistry::getMatrixType(event_type_t typeId)
{
- return typeId < get().eventTypes.size() ? get().eventTypes[typeId] : "";
+ return typeId < get().eventTypes.size()
+ ? get().eventTypes[typeId] : QString();
}
Event::Event(Type type, const QJsonObject& json)
diff --git a/lib/events/eventcontent.cpp b/lib/events/eventcontent.cpp
index 9a5e872c..77f756cd 100644
--- a/lib/events/eventcontent.cpp
+++ b/lib/events/eventcontent.cpp
@@ -50,6 +50,12 @@ FileInfo::FileInfo(const QUrl& u, const QJsonObject& infoJson,
mimeType = QMimeDatabase().mimeTypeForData(QByteArray());
}
+bool FileInfo::isValid() const
+{
+ return url.scheme() == "mxc"
+ && (url.authority() + url.path()).count('/') == 1;
+}
+
void FileInfo::fillInfoJson(QJsonObject* infoJson) const
{
Q_ASSERT(infoJson);
diff --git a/lib/events/eventcontent.h b/lib/events/eventcontent.h
index 0588c0e2..ab31a75d 100644
--- a/lib/events/eventcontent.h
+++ b/lib/events/eventcontent.h
@@ -94,6 +94,8 @@ namespace QMatrixClient
FileInfo(const QUrl& u, const QJsonObject& infoJson,
const QString& originalFilename = {});
+ bool isValid() const;
+
void fillInfoJson(QJsonObject* infoJson) const;
/**
diff --git a/lib/events/roommemberevent.cpp b/lib/events/roommemberevent.cpp
index a5ac3c5f..6da76526 100644
--- a/lib/events/roommemberevent.cpp
+++ b/lib/events/roommemberevent.cpp
@@ -52,7 +52,7 @@ using namespace QMatrixClient;
MemberEventContent::MemberEventContent(const QJsonObject& json)
: membership(fromJson<MembershipType>(json["membership"_ls]))
, isDirect(json["is_direct"_ls].toBool())
- , displayName(json["displayname"_ls].toString())
+ , displayName(sanitized(json["displayname"_ls].toString()))
, avatarUrl(json["avatar_url"_ls].toString())
{ }
diff --git a/lib/events/stateevent.cpp b/lib/events/stateevent.cpp
index e96614d2..a84f302b 100644
--- a/lib/events/stateevent.cpp
+++ b/lib/events/stateevent.cpp
@@ -27,7 +27,7 @@ using namespace QMatrixClient;
RoomEvent::factory_t::addMethod(
[] (const QJsonObject& json, const QString& matrixType) -> StateEventPtr
{
- if (!json.contains("state_key"))
+ if (!json.contains("state_key"_ls))
return nullptr;
if (auto e = StateEventBase::factory_t::make(json, matrixType))
diff --git a/lib/jobs/basejob.cpp b/lib/jobs/basejob.cpp
index 8c3381ae..0d9b9f10 100644
--- a/lib/jobs/basejob.cpp
+++ b/lib/jobs/basejob.cpp
@@ -186,7 +186,7 @@ QUrl BaseJob::makeRequestUrl(QUrl baseUrl,
if (!pathBase.endsWith('/') && !path.startsWith('/'))
pathBase.push_back('/');
- baseUrl.setPath( pathBase + path );
+ baseUrl.setPath(pathBase + path, QUrl::TolerantMode);
baseUrl.setQuery(query);
return baseUrl;
}
@@ -334,8 +334,7 @@ void BaseJob::gotReply()
tr("Requested room version: %1")
.arg(json.value("room_version").toString());
} else if (!json.isEmpty()) // Not localisable on the client side
- setStatus(IncorrectRequestError,
- json.value("error"_ls).toString());
+ setStatus(d->status.code, json.value("error"_ls).toString());
}
}
@@ -430,7 +429,7 @@ BaseJob::Status BaseJob::doCheckReply(QNetworkReply* reply) const
BaseJob::Status BaseJob::parseReply(QNetworkReply* reply)
{
d->rawResponse = reply->readAll();
- QJsonParseError error;
+ QJsonParseError error { 0, QJsonParseError::MissingObject };
const auto& json = QJsonDocument::fromJson(d->rawResponse, &error);
if( error.error == QJsonParseError::NoError )
return parseJson(json);
@@ -600,10 +599,25 @@ QUrl BaseJob::errorUrl() const
void BaseJob::setStatus(Status s)
{
+ // The crash that led to this code has been reported in
+ // https://github.com/QMatrixClient/Quaternion/issues/566 - basically,
+ // when cleaning up childrent of a deleted Connection, there's a chance
+ // of pending jobs being abandoned, calling setStatus(Abandoned).
+ // There's nothing wrong with this; however, the safety check for
+ // cleartext access tokens below uses d->connection - which is a dangling
+ // pointer.
+ // To alleviate that, a stricter condition is applied, that for Abandoned
+ // and to-be-Abandoned jobs the status message will be disregarded entirely.
+ // For 0.6 we might rectify the situation by making d->connection
+ // a QPointer<> (and derive ConnectionData from QObject, respectively).
+ if (d->status.code == Abandoned || s.code == Abandoned)
+ s.message.clear();
+
if (d->status == s)
return;
- if (d->connection && !d->connection->accessToken().isEmpty())
+ if (!s.message.isEmpty()
+ && d->connection && !d->connection->accessToken().isEmpty())
s.message.replace(d->connection->accessToken(), "(REDACTED)");
if (!s.good())
qCWarning(d->logCat) << this << "status" << s;
diff --git a/lib/jobs/downloadfilejob.cpp b/lib/jobs/downloadfilejob.cpp
index 2bf9dd8f..672a7b2d 100644
--- a/lib/jobs/downloadfilejob.cpp
+++ b/lib/jobs/downloadfilejob.cpp
@@ -22,7 +22,8 @@ class DownloadFileJob::Private
QUrl DownloadFileJob::makeRequestUrl(QUrl baseUrl, const QUrl& mxcUri)
{
- return makeRequestUrl(baseUrl, mxcUri.authority(), mxcUri.path().mid(1));
+ return makeRequestUrl(
+ std::move(baseUrl), mxcUri.authority(), mxcUri.path().mid(1));
}
DownloadFileJob::DownloadFileJob(const QString& serverName,
@@ -31,7 +32,7 @@ DownloadFileJob::DownloadFileJob(const QString& serverName,
: GetContentJob(serverName, mediaId)
, d(localFilename.isEmpty() ? new Private : new Private(localFilename))
{
- setObjectName("DownloadFileJob");
+ setObjectName(QStringLiteral("DownloadFileJob"));
}
QString DownloadFileJob::targetFileName() const
diff --git a/lib/jobs/mediathumbnailjob.cpp b/lib/jobs/mediathumbnailjob.cpp
index aeb49839..edb9b156 100644
--- a/lib/jobs/mediathumbnailjob.cpp
+++ b/lib/jobs/mediathumbnailjob.cpp
@@ -59,5 +59,5 @@ BaseJob::Status MediaThumbnailJob::parseReply(QNetworkReply* reply)
if( _thumbnail.loadFromData(data()->readAll()) )
return Success;
- return { IncorrectResponseError, "Could not read image data" };
+ return { IncorrectResponseError, QStringLiteral("Could not read image data") };
}
diff --git a/lib/networkaccessmanager.cpp b/lib/networkaccessmanager.cpp
index 89967a8a..7d9cb360 100644
--- a/lib/networkaccessmanager.cpp
+++ b/lib/networkaccessmanager.cpp
@@ -29,7 +29,8 @@ class NetworkAccessManager::Private
QList<QSslError> ignoredSslErrors;
};
-NetworkAccessManager::NetworkAccessManager(QObject* parent) : d(std::make_unique<Private>())
+NetworkAccessManager::NetworkAccessManager(QObject* parent)
+ : QNetworkAccessManager(parent), d(std::make_unique<Private>())
{ }
QList<QSslError> NetworkAccessManager::ignoredSslErrors() const
diff --git a/lib/networksettings.cpp b/lib/networksettings.cpp
index 48bd09f3..6ff2bc1f 100644
--- a/lib/networksettings.cpp
+++ b/lib/networksettings.cpp
@@ -27,5 +27,5 @@ void NetworkSettings::setupApplicationProxy() const
}
QMC_DEFINE_SETTING(NetworkSettings, QNetworkProxy::ProxyType, proxyType, "proxy_type", QNetworkProxy::DefaultProxy, setProxyType)
-QMC_DEFINE_SETTING(NetworkSettings, QString, proxyHostName, "proxy_hostname", "", setProxyHostName)
+QMC_DEFINE_SETTING(NetworkSettings, QString, proxyHostName, "proxy_hostname", {}, setProxyHostName)
QMC_DEFINE_SETTING(NetworkSettings, quint16, proxyPort, "proxy_port", -1, setProxyPort)
diff --git a/lib/room.cpp b/lib/room.cpp
index 5da9373e..2ce37acc 100644
--- a/lib/room.cpp
+++ b/lib/room.cpp
@@ -105,6 +105,7 @@ class Room::Private
members_map_t membersMap;
QList<User*> usersTyping;
QMultiHash<QString, User*> eventIdReadUsers;
+ QList<User*> usersInvited;
QList<User*> membersLeft;
int unreadMessages = 0;
bool displayed = false;
@@ -167,7 +168,7 @@ class Room::Private
//void inviteUser(User* u); // We might get it at some point in time.
void insertMemberIntoMap(User* u);
- void renameMember(User* u, QString oldName);
+ void renameMember(User* u, const QString& oldName);
void removeMemberFromMap(const QString& username, User* u);
// This updates the room displayname field (which is the way a room
@@ -184,7 +185,7 @@ class Room::Private
void getPreviousContent(int limit = 10);
template <typename EventT>
- const EventT* getCurrentState(QString stateKey = {}) const
+ const EventT* getCurrentState(const QString& stateKey = {}) const
{
static const EventT empty;
const auto* evt =
@@ -235,8 +236,8 @@ class Room::Private
* @param placement - position and direction of insertion: Older for
* historical messages, Newer for new ones
*/
- Timeline::difference_type moveEventsToTimeline(RoomEventsRange events,
- EventsPlacement placement);
+ Timeline::size_type moveEventsToTimeline(RoomEventsRange events,
+ EventsPlacement placement);
/**
* Remove events from the passed container that are already in the timeline
@@ -340,7 +341,7 @@ const QString& Room::id() const
QString Room::version() const
{
const auto v = d->getCurrentState<RoomCreateEvent>()->version();
- return v.isEmpty() ? "1" : v;
+ return v.isEmpty() ? QStringLiteral("1") : v;
}
bool Room::isUnstable() const
@@ -369,6 +370,11 @@ const Room::PendingEvents& Room::pendingEvents() const
return d->unsyncedEvents;
}
+bool Room::allHistoryLoaded() const
+{
+ return !d->timeline.empty() && is<RoomCreateEvent>(*d->timeline.front());
+}
+
QString Room::name() const
{
return d->getCurrentState<RoomNameEvent>()->name();
@@ -389,6 +395,11 @@ QString Room::displayName() const
return d->displayname;
}
+void Room::refreshDisplayName()
+{
+ d->updateDisplayname();
+}
+
QString Room::topic() const
{
return d->getCurrentState<RoomTopicEvent>()->topic();
@@ -540,8 +551,8 @@ Room::Changes Room::Private::promoteReadMarker(User* u, rev_iter_t newMarker,
{
const auto oldUnreadCount = unreadMessages;
QElapsedTimer et; et.start();
- unreadMessages = count_if(eagerMarker, timeline.cend(),
- std::bind(&Room::Private::isEventNotable, this, _1));
+ unreadMessages = int(count_if(eagerMarker, timeline.cend(),
+ std::bind(&Room::Private::isEventNotable, this, _1)));
if (et.nsecsElapsed() > profilerMinNsecs() / 10)
qCDebug(PROFILER) << "Recounting unread messages took" << et;
@@ -579,8 +590,8 @@ Room::Changes Room::Private::markMessagesAsRead(rev_iter_t upToMarker)
{
if ((*upToMarker)->senderId() != q->localUser()->id())
{
- connection->callApi<PostReceiptJob>(id, "m.read",
- (*upToMarker)->id());
+ connection->callApi<PostReceiptJob>(id, QStringLiteral("m.read"),
+ QUrl::toPercentEncoding((*upToMarker)->id()));
break;
}
}
@@ -600,9 +611,12 @@ void Room::markAllMessagesAsRead()
bool Room::canSwitchVersions() const
{
+ if (!successorId().isEmpty())
+ return false; // Noone can upgrade a room that's already upgraded
+
// TODO, #276: m.room.power_levels
const auto* plEvt =
- d->currentState.value({"m.room.power_levels", ""});
+ d->currentState.value({QStringLiteral("m.room.power_levels"), {}});
if (!plEvt)
return true;
@@ -612,7 +626,7 @@ bool Room::canSwitchVersions() const
.value(localUser()->id()).toInt(
plJson.value("users_default"_ls).toInt());
const auto tombstonePowerLevel =
- plJson.value("events").toObject()
+ plJson.value("events"_ls).toObject()
.value("m.room.tombstone"_ls).toInt(
plJson.value("state_default"_ls).toInt());
return currentUserLevel >= tombstonePowerLevel;
@@ -815,7 +829,7 @@ void Room::resetNotificationCount()
if( d->notificationCount == 0 )
return;
d->notificationCount = 0;
- emit notificationCountChanged(this);
+ emit notificationCountChanged();
}
int Room::highlightCount() const
@@ -828,15 +842,22 @@ void Room::resetHighlightCount()
if( d->highlightCount == 0 )
return;
d->highlightCount = 0;
- emit highlightCountChanged(this);
+ emit highlightCountChanged();
}
void Room::switchVersion(QString newVersion)
{
- auto* job = connection()->callApi<UpgradeRoomJob>(id(), newVersion);
- connect(job, &BaseJob::failure, this, [this,job] {
- emit upgradeFailed(job->errorString());
- });
+ if (!successorId().isEmpty())
+ {
+ Q_ASSERT(!successorId().isEmpty());
+ emit upgradeFailed(tr("The room is already upgraded"));
+ }
+ if (auto* job = connection()->callApi<UpgradeRoomJob>(id(), newVersion))
+ connect(job, &BaseJob::failure, this, [this,job] {
+ emit upgradeFailed(job->errorString());
+ });
+ else
+ emit upgradeFailed(tr("Couldn't initiate upgrade"));
}
bool Room::hasAccountData(const QString& type) const
@@ -938,7 +959,7 @@ void Room::Private::setTags(TagsMap newTags)
}
tags = move(newTags);
qCDebug(MAIN) << "Room" << q->objectName() << "is tagged with"
- << q->tagNames().join(", ");
+ << q->tagNames().join(QStringLiteral(", "));
emit q->tagsChanged();
}
@@ -1174,7 +1195,11 @@ void Room::Private::insertMemberIntoMap(User *u)
const auto userName = u->name(q);
// If there is exactly one namesake of the added user, signal member renaming
// for that other one because the two should be disambiguated now.
- auto namesakes = membersMap.values(userName);
+ const auto namesakes = membersMap.values(userName);
+
+ // Callers should check they are not adding an existing user once more.
+ Q_ASSERT(!namesakes.contains(u));
+
if (namesakes.size() == 1)
emit q->memberAboutToRename(namesakes.front(),
namesakes.front()->fullName(q));
@@ -1183,7 +1208,7 @@ void Room::Private::insertMemberIntoMap(User *u)
emit q->memberRenamed(namesakes.front());
}
-void Room::Private::renameMember(User* u, QString oldName)
+void Room::Private::renameMember(User* u, const QString& oldName)
{
if (u->name(q) == oldName)
{
@@ -1196,7 +1221,6 @@ void Room::Private::renameMember(User* u, QString oldName)
removeMemberFromMap(oldName, u);
insertMemberIntoMap(u);
}
- emit q->memberRenamed(u);
}
void Room::Private::removeMemberFromMap(const QString& username, User* u)
@@ -1212,7 +1236,6 @@ void Room::Private::removeMemberFromMap(const QString& username, User* u)
membersMap.remove(username, u);
// If there was one namesake besides the removed user, signal member renaming
// for it because it doesn't need to be disambiguated anymore.
- // TODO: Think about left users.
if (namesake)
emit q->memberRenamed(namesake);
}
@@ -1222,7 +1245,7 @@ inline auto makeErrorStr(const Event& e, QByteArray msg)
return msg.append("; event dump follows:\n").append(e.originalJson());
}
-Room::Timeline::difference_type Room::Private::moveEventsToTimeline(
+Room::Timeline::size_type Room::Private::moveEventsToTimeline(
RoomEventsRange events, EventsPlacement placement)
{
Q_ASSERT(!events.empty());
@@ -1327,7 +1350,6 @@ void Room::updateData(SyncRoomData&& data, bool fromCache)
emit memberListChanged();
roomChanges |= d->setSummary(move(data.summary));
- d->updateDisplayname();
for( auto&& ephemeralEvent: data.ephemeral )
roomChanges |= processEphemeralEvent(move(ephemeralEvent));
@@ -1343,15 +1365,16 @@ void Room::updateData(SyncRoomData&& data, bool fromCache)
if( data.highlightCount != d->highlightCount )
{
d->highlightCount = data.highlightCount;
- emit highlightCountChanged(this);
+ emit highlightCountChanged();
}
if( data.notificationCount != d->notificationCount )
{
d->notificationCount = data.notificationCount;
- emit notificationCountChanged(this);
+ emit notificationCountChanged();
}
if (roomChanges != Change::NoChange)
{
+ d->updateDisplayname();
emit changed(roomChanges);
if (!fromCache)
connection()->saveRoomState(this);
@@ -1395,7 +1418,7 @@ QString Room::Private::doSendEvent(const RoomEvent* pEvent)
return;
}
it->setDeparted();
- emit q->pendingEventChanged(it - unsyncedEvents.begin());
+ emit q->pendingEventChanged(int(it - unsyncedEvents.begin()));
});
Room::connect(call, &BaseJob::failure, q,
std::bind(&Room::Private::onEventSendingFailure, this, txnId, call));
@@ -1411,7 +1434,7 @@ QString Room::Private::doSendEvent(const RoomEvent* pEvent)
}
it->setReachedServer(call->eventId());
- emit q->pendingEventChanged(it - unsyncedEvents.begin());
+ emit q->pendingEventChanged(int(it - unsyncedEvents.begin()));
});
} else
onEventSendingFailure(txnId);
@@ -1430,7 +1453,7 @@ void Room::Private::onEventSendingFailure(const QString& txnId, BaseJob* call)
it->setSendingFailed(call
? call->statusCaption() % ": " % call->errorString()
: tr("The call could not be started"));
- emit q->pendingEventChanged(it - unsyncedEvents.begin());
+ emit q->pendingEventChanged(int(it - unsyncedEvents.begin()));
}
QString Room::retryMessage(const QString& txnId)
@@ -1521,12 +1544,17 @@ QString Room::postFile(const QString& plainText, const QUrl& localPath,
{
QFileInfo localFile { localPath.toLocalFile() };
Q_ASSERT(localFile.isFile());
+
+ const auto txnId = connection()->generateTxnId();
// Remote URL will only be known after upload; fill in the local path
// to enable the preview while the event is pending.
- const auto txnId = d->addAsPending(makeEvent<RoomMessageEvent>(
- plainText, localFile, asGenericFile)
- )->transactionId();
uploadFile(txnId, localPath);
+ {
+ auto&& event =
+ makeEvent<RoomMessageEvent>(plainText, localFile, asGenericFile);
+ event->setTransactionId(txnId);
+ d->addAsPending(std::move(event));
+ }
auto* context = new QObject(this);
connect(this, &Room::fileTransferCompleted, context,
[context,this,txnId] (const QString& id, QUrl, const QUrl& mxcUri) {
@@ -1634,7 +1662,7 @@ void Room::checkVersion()
{
const auto defaultVersion = connection()->defaultRoomVersion();
const auto stableVersions = connection()->stableRoomVersions();
- Q_ASSERT(!defaultVersion.isEmpty() && successorId().isEmpty());
+ Q_ASSERT(!defaultVersion.isEmpty());
// This method is only called after the base state has been loaded
// or the server capabilities have been loaded.
emit stabilityUpdated(defaultVersion, stableVersions);
@@ -1734,8 +1762,8 @@ void Room::unban(const QString& userId)
void Room::redactEvent(const QString& eventId, const QString& reason)
{
- connection()->callApi<RedactEventJob>(
- id(), eventId, connection()->generateTxnId(), reason);
+ connection()->callApi<RedactEventJob>(id(),
+ QUrl::toPercentEncoding(eventId), connection()->generateTxnId(), reason);
}
void Room::uploadFile(const QString& id, const QUrl& localFilename,
@@ -1785,7 +1813,14 @@ void Room::downloadFile(const QString& eventId, const QUrl& localFilename)
Q_ASSERT(false);
return;
}
- const auto fileUrl = event->content()->fileInfo()->url;
+ const auto* const fileInfo = event->content()->fileInfo();
+ if (!fileInfo->isValid())
+ {
+ qCWarning(MAIN) << "Event" << eventId
+ << "has an empty or malformed mxc URL; won't download";
+ return;
+ }
+ const auto fileUrl = fileInfo->url;
auto filePath = localFilename.toLocalFile();
if (filePath.isEmpty())
{
@@ -1949,10 +1984,10 @@ bool Room::Private::processRedaction(const RedactionEvent& redaction)
{
const StateEventKey evtKey { oldEvent->matrixType(), oldEvent->stateKey() };
Q_ASSERT(currentState.contains(evtKey));
- if (currentState[evtKey] == oldEvent.get())
+ if (currentState.value(evtKey) == oldEvent.get())
{
Q_ASSERT(ti.index() >= 0); // Historical states can't be in currentState
- qCDebug(MAIN).nospace() << "Reverting state "
+ qCDebug(MAIN).nospace() << "Redacting state "
<< oldEvent->matrixType() << "/" << oldEvent->stateKey();
// Retarget the current state to the newly made event.
if (q->processStateEvent(*ti))
@@ -2021,7 +2056,7 @@ Room::Changes Room::Private::addNewMessageEvents(RoomEvents&& events)
roomChanges |= q->processStateEvent(*eptr);
auto timelineSize = timeline.size();
- auto totalInserted = 0;
+ size_t totalInserted = 0;
for (auto it = events.begin(); it != events.end();)
{
auto nextPendingPair = findFirstOf(it, events.end(),
@@ -2153,7 +2188,7 @@ Room::Changes Room::processStateEvent(const RoomEvent& e)
Q_ASSERT(!oldStateEvent ||
(oldStateEvent->matrixType() == e.matrixType() &&
oldStateEvent->stateKey() == e.stateKey()));
- if (!is<RoomMemberEvent>(e))
+ if (!is<RoomMemberEvent>(e)) // Room member events are too numerous
qCDebug(EVENTS) << "Room state event:" << e;
return visit(e
@@ -2179,16 +2214,52 @@ Room::Changes Room::processStateEvent(const RoomEvent& e)
emit avatarChanged();
return AvatarChange;
}
- , [this] (const RoomMemberEvent& evt) {
+ , [this,oldStateEvent] (const RoomMemberEvent& evt) {
auto* u = user(evt.userId());
- u->processEvent(evt, this);
- if (u == localUser() && memberJoinState(u) == JoinState::Invite
+ const auto* oldMemberEvent =
+ static_cast<const RoomMemberEvent*>(oldStateEvent);
+ u->processEvent(evt, this, oldMemberEvent == nullptr);
+ const auto prevMembership = oldMemberEvent
+ ? oldMemberEvent->membership() : MembershipType::Leave;
+ if (u == localUser() && evt.membership() == MembershipType::Invite
&& evt.isDirect())
connection()->addToDirectChats(this, user(evt.senderId()));
- if( evt.membership() == MembershipType::Join )
+ switch (prevMembership)
+ {
+ case MembershipType::Invite:
+ if (evt.membership() != prevMembership)
+ {
+ d->usersInvited.removeOne(u);
+ Q_ASSERT(!d->usersInvited.contains(u));
+ }
+ break;
+ case MembershipType::Join:
+ if (evt.membership() == MembershipType::Invite)
+ qCWarning(MAIN)
+ << "Invalid membership change from Join to Invite:"
+ << evt;
+ if (evt.membership() != prevMembership)
+ {
+ disconnect(u, &User::nameAboutToChange, this, nullptr);
+ disconnect(u, &User::nameChanged, this, nullptr);
+ d->removeMemberFromMap(u->name(this), u);
+ emit userRemoved(u);
+ }
+ break;
+ default:
+ if (evt.membership() == MembershipType::Invite
+ || evt.membership() == MembershipType::Join)
+ {
+ d->membersLeft.removeOne(u);
+ Q_ASSERT(!d->membersLeft.contains(u));
+ }
+ }
+
+ switch(evt.membership())
{
- if (memberJoinState(u) != JoinState::Join)
+ case MembershipType::Join:
+ if (prevMembership != MembershipType::Join)
{
d->insertMemberIntoMap(u);
connect(u, &User::nameAboutToChange, this,
@@ -2199,22 +2270,21 @@ Room::Changes Room::processStateEvent(const RoomEvent& e)
connect(u, &User::nameChanged, this,
[=] (QString, QString oldName, const Room* context) {
if (context == this)
+ {
d->renameMember(u, oldName);
+ emit memberRenamed(u);
+ }
});
emit userAdded(u);
}
- }
- else if( evt.membership() != MembershipType::Join )
- {
- if (memberJoinState(u) == JoinState::Join)
- {
- if (evt.membership() == MembershipType::Invite)
- qCWarning(MAIN) << "Invalid membership change:" << evt;
- if (!d->membersLeft.contains(u))
- d->membersLeft.append(u);
- d->removeMemberFromMap(u->name(this), u);
- emit userRemoved(u);
- }
+ break;
+ case MembershipType::Invite:
+ if (!d->usersInvited.contains(u))
+ d->usersInvited.push_back(u);
+ break;
+ default:
+ if (!d->membersLeft.contains(u))
+ d->membersLeft.append(u);
}
return MembersChange;
}
@@ -2395,42 +2465,59 @@ QString Room::Private::calculateDisplayname() const
return dispName;
// Using m.room.aliases in naming is explicitly discouraged by the spec
- //if (!q->aliases().empty() && !q->aliases().at(0).isEmpty())
- // return q->aliases().at(0);
// Supplementary code for 3 and 4: build the shortlist of users whose names
// will be used to construct the room name. Takes into account MSC688's
// "heroes" if available.
+ const bool localUserIsIn = joinState == JoinState::Join;
const bool emptyRoom = membersMap.isEmpty() ||
(membersMap.size() == 1 && isLocalUser(*membersMap.begin()));
- const auto shortlist =
- !summary.heroes.omitted() ? buildShortlist(summary.heroes.value()) :
- !emptyRoom ? buildShortlist(membersMap) :
- buildShortlist(membersLeft);
+ const bool nonEmptySummary =
+ !summary.heroes.omitted() && !summary.heroes->empty();
+ auto shortlist = nonEmptySummary ? buildShortlist(summary.heroes.value()) :
+ !emptyRoom ? buildShortlist(membersMap) :
+ users_shortlist_t { };
+
+ // When lazy-loading is on, we can rely on the heroes list.
+ // If it's off, the below code gathers invited and left members.
+ // NB: including invitations, if any, into naming is a spec extension.
+ // This kicks in when there's no lazy loading and it's a room with
+ // the local user as the only member, with more users invited.
+ if (!shortlist.front() && localUserIsIn)
+ shortlist = buildShortlist(usersInvited);
+
+ if (!shortlist.front()) // Still empty shortlist; use left members
+ shortlist = buildShortlist(membersLeft);
QStringList names;
for (auto u: shortlist)
{
if (u == nullptr || isLocalUser(u))
break;
- names.push_back(q->roomMembername(u));
+ // Only disambiguate if the room is not empty
+ names.push_back(u->displayname(emptyRoom ? nullptr : q));
}
- auto usersCountExceptLocal = emptyRoom
- ? membersLeft.size() - int(joinState == JoinState::Leave)
- : q->joinedCount() - int(joinState == JoinState::Join);
+ const auto usersCountExceptLocal =
+ !emptyRoom ? q->joinedCount() - int(joinState == JoinState::Join) :
+ !usersInvited.empty() ? usersInvited.count() :
+ membersLeft.size() - int(joinState == JoinState::Leave);
if (usersCountExceptLocal > int(shortlist.size()))
names <<
tr("%Ln other(s)",
"Used to make a room name from user names: A, B and _N others_",
- usersCountExceptLocal);
- auto namesList = QLocale().createSeparatedList(names);
+ usersCountExceptLocal - int(shortlist.size()));
+ const auto namesList = QLocale().createSeparatedList(names);
// 3. Room members
if (!emptyRoom)
return namesList;
+ // (Spec extension) Invited users
+ if (!usersInvited.empty())
+ return tr("Empty room (invited: %1)").arg(namesList);
+
// 4. Users that previously left the room
if (membersLeft.size() > 0)
return tr("Empty room (was: %1)").arg(namesList);
diff --git a/lib/room.h b/lib/room.h
index f4ecef42..055da3da 100644
--- a/lib/room.h
+++ b/lib/room.h
@@ -88,7 +88,7 @@ namespace QMatrixClient
Q_PROPERTY(QString name READ name NOTIFY namesChanged)
Q_PROPERTY(QStringList aliases READ aliases NOTIFY namesChanged)
Q_PROPERTY(QString canonicalAlias READ canonicalAlias NOTIFY namesChanged)
- Q_PROPERTY(QString displayName READ displayName NOTIFY namesChanged)
+ Q_PROPERTY(QString displayName READ displayName NOTIFY displaynameChanged)
Q_PROPERTY(QString topic READ topic NOTIFY topicChanged)
Q_PROPERTY(QString avatarMediaId READ avatarMediaId NOTIFY avatarChanged STORED false)
Q_PROPERTY(QUrl avatarUrl READ avatarUrl NOTIFY avatarChanged)
@@ -108,6 +108,9 @@ namespace QMatrixClient
Q_PROPERTY(QString readMarkerEventId READ readMarkerEventId WRITE markMessagesAsRead NOTIFY readMarkerMoved)
Q_PROPERTY(bool hasUnreadMessages READ hasUnreadMessages NOTIFY unreadMessagesChanged)
Q_PROPERTY(int unreadCount READ unreadCount NOTIFY unreadMessagesChanged)
+ Q_PROPERTY(int highlightCount READ highlightCount NOTIFY highlightCountChanged RESET resetHighlightCount)
+ Q_PROPERTY(int notificationCount READ notificationCount NOTIFY notificationCountChanged RESET resetNotificationCount)
+ Q_PROPERTY(bool allHistoryLoaded READ allHistoryLoaded NOTIFY addedMessages STORED false)
Q_PROPERTY(QStringList tagNames READ tagNames NOTIFY tagsChanged)
Q_PROPERTY(bool isFavourite READ isFavourite NOTIFY tagsChanged)
Q_PROPERTY(bool isLowPriority READ isLowPriority NOTIFY tagsChanged)
@@ -226,6 +229,14 @@ namespace QMatrixClient
const Timeline& messageEvents() const;
const PendingEvents& pendingEvents() const;
+
+ /// Check whether all historical messages are already loaded
+ /**
+ * \return true if the "oldest" event in the timeline is
+ * a room creation event and there's no further history
+ * to load; false otherwise
+ */
+ bool allHistoryLoaded() const;
/**
* A convenience method returning the read marker to the position
* before the "oldest" event; same as messageEvents().crend()
@@ -356,8 +367,28 @@ namespace QMatrixClient
Q_INVOKABLE QUrl urlToThumbnail(const QString& eventId) const;
Q_INVOKABLE QUrl urlToDownload(const QString& eventId) const;
+
+ /// Get a file name for downloading for a given event id
+ /*!
+ * The event MUST be RoomMessageEvent and have content
+ * for downloading. \sa RoomMessageEvent::hasContent
+ */
Q_INVOKABLE QString fileNameToDownload(const QString& eventId) const;
+
+ /// Get information on file upload/download
+ /*!
+ * \param id uploads are identified by the corresponding event's
+ * transactionId (because uploads are done before
+ * the event is even sent), while downloads are using
+ * the normal event id for identifier.
+ */
Q_INVOKABLE FileTransferInfo fileTransferInfo(const QString& id) const;
+
+ /// Get the URL to the actual file source in a unified way
+ /*!
+ * For uploads it will return a URL to a local file; for downloads
+ * the URL will be taken from the corresponding room event.
+ */
Q_INVOKABLE QUrl fileSource(const QString& id) const;
/** Pretty-prints plain text into HTML
@@ -365,7 +396,7 @@ namespace QMatrixClient
* in the future, it will also linkify room aliases, mxids etc.
* using the room context.
*/
- QString prettyPrint(const QString& plainText) const;
+ Q_INVOKABLE QString prettyPrint(const QString& plainText) const;
MemberSorter memberSorter() const;
@@ -408,6 +439,9 @@ namespace QMatrixClient
void setAliases(const QStringList& aliases);
void setTopic(const QString& newTopic);
+ /// You shouldn't normally call this method; it's here for debugging
+ void refreshDisplayName();
+
void getPreviousContent(int limit = 10);
void inviteToRoom(const QString& memberId);
@@ -517,8 +551,8 @@ namespace QMatrixClient
void joinStateChanged(JoinState oldState, JoinState newState);
void typingChanged();
- void highlightCountChanged(Room* room);
- void notificationCountChanged(Room* room);
+ void highlightCountChanged();
+ void notificationCountChanged();
void displayedChanged(bool displayed);
void firstDisplayedEventChanged();
diff --git a/lib/settings.cpp b/lib/settings.cpp
index 852e19cb..124d7042 100644
--- a/lib/settings.cpp
+++ b/lib/settings.cpp
@@ -84,18 +84,21 @@ void SettingsGroup::remove(const QString& key)
Settings::remove(fullKey);
}
-QMC_DEFINE_SETTING(AccountSettings, QString, deviceId, "device_id", "", setDeviceId)
-QMC_DEFINE_SETTING(AccountSettings, QString, deviceName, "device_name", "", setDeviceName)
+QMC_DEFINE_SETTING(AccountSettings, QString, deviceId, "device_id", {}, setDeviceId)
+QMC_DEFINE_SETTING(AccountSettings, QString, deviceName, "device_name", {}, setDeviceName)
QMC_DEFINE_SETTING(AccountSettings, bool, keepLoggedIn, "keep_logged_in", false, setKeepLoggedIn)
+static const auto HomeserverKey = QStringLiteral("homeserver");
+static const auto AccessTokenKey = QStringLiteral("access_token");
+
QUrl AccountSettings::homeserver() const
{
- return QUrl::fromUserInput(value("homeserver").toString());
+ return QUrl::fromUserInput(value(HomeserverKey).toString());
}
void AccountSettings::setHomeserver(const QUrl& url)
{
- setValue("homeserver", url.toString());
+ setValue(HomeserverKey, url.toString());
}
QString AccountSettings::userId() const
@@ -105,19 +108,19 @@ QString AccountSettings::userId() const
QString AccountSettings::accessToken() const
{
- return value("access_token").toString();
+ return value(AccessTokenKey).toString();
}
void AccountSettings::setAccessToken(const QString& accessToken)
{
qCWarning(MAIN) << "Saving access_token to QSettings is insecure."
" Developers, please save access_token separately.";
- setValue("access_token", accessToken);
+ setValue(AccessTokenKey, accessToken);
}
void AccountSettings::clearAccessToken()
{
- legacySettings.remove("access_token");
- legacySettings.remove("device_id"); // Force the server to re-issue it
- remove("access_token");
+ legacySettings.remove(AccessTokenKey);
+ legacySettings.remove(QStringLiteral("device_id")); // Force the server to re-issue it
+ remove(AccessTokenKey);
}
diff --git a/lib/settings.h b/lib/settings.h
index 0b3ecaff..759bda35 100644
--- a/lib/settings.h
+++ b/lib/settings.h
@@ -119,7 +119,7 @@ type classname::propname() const \
\
void classname::setter(type newValue) \
{ \
- setValue(QStringLiteral(qsettingname), newValue); \
+ setValue(QStringLiteral(qsettingname), std::move(newValue)); \
} \
class AccountSettings: public SettingsGroup
diff --git a/lib/syncdata.cpp b/lib/syncdata.cpp
index f55d4396..21517884 100644
--- a/lib/syncdata.cpp
+++ b/lib/syncdata.cpp
@@ -72,7 +72,7 @@ void JsonObjectConverter<RoomSummary>::fillFrom(const QJsonObject& jo,
{
fromJson(jo["m.joined_member_count"_ls], rs.joinedMemberCount);
fromJson(jo["m.invited_member_count"_ls], rs.invitedMemberCount);
- fromJson(jo["m.heroes"], rs.heroes);
+ fromJson(jo["m.heroes"_ls], rs.heroes);
}
template <typename EventsArrayT, typename StrT>
@@ -85,7 +85,7 @@ SyncRoomData::SyncRoomData(const QString& roomId_, JoinState joinState_,
const QJsonObject& room_)
: roomId(roomId_)
, joinState(joinState_)
- , summary(fromJson<RoomSummary>(room_["summary"]))
+ , summary(fromJson<RoomSummary>(room_["summary"_ls]))
, state(load<StateEvents>(room_, joinState == JoinState::Invite
? "invite_state"_ls : "state"_ls))
{
@@ -121,8 +121,8 @@ SyncData::SyncData(const QString& cacheFileName)
QFileInfo cacheFileInfo { cacheFileName };
auto json = loadJson(cacheFileName);
auto requiredVersion = std::get<0>(cacheVersion());
- auto actualVersion = json.value("cache_version").toObject()
- .value("major").toInt();
+ auto actualVersion = json.value("cache_version"_ls).toObject()
+ .value("major"_ls).toInt();
if (actualVersion == requiredVersion)
parseJson(json, cacheFileInfo.absolutePath() + '/');
else
diff --git a/lib/user.cpp b/lib/user.cpp
index b13f98b4..fdb82a38 100644
--- a/lib/user.cpp
+++ b/lib/user.cpp
@@ -76,7 +76,7 @@ class User::Private
qreal hueF;
Avatar mostUsedAvatar { makeAvatar({}) };
std::vector<Avatar> otherAvatars;
- auto otherAvatar(QUrl url)
+ auto otherAvatar(const QUrl& url)
{
return std::find_if(otherAvatars.begin(), otherAvatars.end(),
[&url] (const auto& av) { return av.url() == url; });
@@ -86,7 +86,7 @@ class User::Private
mutable int totalRooms = 0;
QString nameForRoom(const Room* r, const QString& hint = {}) const;
- void setNameForRoom(const Room* r, QString newName, QString oldName);
+ void setNameForRoom(const Room* r, QString newName, const QString& oldName);
QUrl avatarUrlForRoom(const Room* r, const QUrl& hint = {}) const;
void setAvatarForRoom(const Room* r, const QUrl& newUrl,
const QUrl& oldUrl);
@@ -99,7 +99,8 @@ class User::Private
QString User::Private::nameForRoom(const Room* r, const QString& hint) const
{
// If the hint is accurate, this function is O(1) instead of O(n)
- if (hint == mostUsedName || otherNames.contains(hint, r))
+ if (!hint.isNull()
+ && (hint == mostUsedName || otherNames.contains(hint, r)))
return hint;
return otherNames.key(r, mostUsedName);
}
@@ -107,7 +108,7 @@ QString User::Private::nameForRoom(const Room* r, const QString& hint) const
static constexpr int MIN_JOINED_ROOMS_TO_LOG = 20;
void User::Private::setNameForRoom(const Room* r, QString newName,
- QString oldName)
+ const QString& oldName)
{
Q_ASSERT(oldName != newName);
Q_ASSERT(oldName == mostUsedName || otherNames.contains(oldName, r));
@@ -134,7 +135,8 @@ void User::Private::setNameForRoom(const Room* r, QString newName,
et.start();
}
- for (auto* r1: connection->roomMap())
+ const auto& roomMap = connection->roomMap();
+ for (auto* r1: roomMap)
if (nameForRoom(r1) == mostUsedName)
otherNames.insert(mostUsedName, r1);
@@ -194,7 +196,8 @@ void User::Private::setAvatarForRoom(const Room* r, const QUrl& newUrl,
auto nextMostUsedIt = otherAvatar(newUrl);
Q_ASSERT(nextMostUsedIt != otherAvatars.end());
std::swap(mostUsedAvatar, *nextMostUsedIt);
- for (const auto* r1: connection->roomMap())
+ const auto& roomMap = connection->roomMap();
+ for (const auto* r1: roomMap)
if (avatarUrlForRoom(r1) == nextMostUsedIt->url())
avatarsToRooms.insert(nextMostUsedIt->url(), r1);
@@ -286,8 +289,9 @@ void User::updateAvatarUrl(const QUrl& newUrl, const QUrl& oldUrl,
void User::rename(const QString& newName)
{
- auto job = connection()->callApi<SetDisplayNameJob>(id(), newName);
- connect(job, &BaseJob::success, this, [=] { updateName(newName); });
+ const auto actualNewName = sanitized(newName);
+ connect(connection()->callApi<SetDisplayNameJob>(id(), actualNewName),
+ &BaseJob::success, this, [=] { updateName(actualNewName); });
}
void User::rename(const QString& newName, const Room* r)
@@ -301,10 +305,11 @@ void User::rename(const QString& newName, const Room* r)
}
Q_ASSERT_X(r->memberJoinState(this) == JoinState::Join, __FUNCTION__,
"Attempt to rename a user that's not a room member");
+ const auto actualNewName = sanitized(newName);
MemberEventContent evtC;
- evtC.displayName = newName;
- auto job = r->setMemberState(id(), RoomMemberEvent(move(evtC)));
- connect(job, &BaseJob::success, this, [=] { updateName(newName, r); });
+ evtC.displayName = actualNewName;
+ connect(r->setMemberState(id(), RoomMemberEvent(move(evtC))),
+ &BaseJob::success, this, [=] { updateName(actualNewName, r); });
}
bool User::setAvatar(const QString& fileName)
@@ -399,19 +404,18 @@ QUrl User::avatarUrl(const Room* room) const
return avatarObject(room).url();
}
-void User::processEvent(const RoomMemberEvent& event, const Room* room)
+void User::processEvent(const RoomMemberEvent& event, const Room* room,
+ bool firstMention)
{
Q_ASSERT(room);
+
+ if (firstMention)
+ ++d->totalRooms;
+
if (event.membership() != MembershipType::Invite &&
event.membership() != MembershipType::Join)
return;
- auto aboutToEnter = room->memberJoinState(this) == JoinState::Leave &&
- (event.membership() == MembershipType::Join ||
- event.membership() == MembershipType::Invite);
- if (aboutToEnter)
- ++d->totalRooms;
-
auto newName = event.displayName();
// `bridged` value uses the same notification signal as the name;
// it is assumed that first setting of the bridge occurs together with
@@ -419,7 +423,7 @@ void User::processEvent(const RoomMemberEvent& event, const Room* room)
// exceptionally rare (the only reasonable case being that the bridge
// changes the naming convention). For the same reason room-specific
// bridge tags are not supported at all.
- QRegularExpression reSuffix(" \\((IRC|Gitter|Telegram)\\)$");
+ QRegularExpression reSuffix(QStringLiteral(" \\((IRC|Gitter|Telegram)\\)$"));
auto match = reSuffix.match(newName);
if (match.hasMatch())
{
diff --git a/lib/user.h b/lib/user.h
index af1abfa2..80e6ad66 100644
--- a/lib/user.h
+++ b/lib/user.h
@@ -116,7 +116,11 @@ namespace QMatrixClient
QString avatarMediaId(const Room* room = nullptr) const;
QUrl avatarUrl(const Room* room = nullptr) const;
- void processEvent(const RoomMemberEvent& event, const Room* r);
+ /// This method is for internal use and should not be called
+ /// from client code
+ // FIXME: Move it away to private in lib 0.6
+ void processEvent(const RoomMemberEvent& event, const Room* r,
+ bool firstMention);
public slots:
/** Set a new name in the global user profile */
diff --git a/lib/util.cpp b/lib/util.cpp
index d042aa34..4e17d2f9 100644
--- a/lib/util.cpp
+++ b/lib/util.cpp
@@ -29,48 +29,57 @@ static const auto RegExpOptions =
| QRegularExpression::UseUnicodePropertiesOption;
// Converts all that looks like a URL into HTML links
-static void linkifyUrls(QString& htmlEscapedText)
+void QMatrixClient::linkifyUrls(QString& htmlEscapedText)
{
+ // Note: outer parentheses are a part of C++ raw string delimiters, not of
+ // the regex (see http://en.cppreference.com/w/cpp/language/string_literal).
+ // Note2: the next-outer parentheses are \N in the replacement.
+
+ // generic url:
// regexp is originally taken from Konsole (https://github.com/KDE/konsole)
- // full url:
// protocolname:// or www. followed by anything other than whitespaces,
// <, >, ' or ", and ends before whitespaces, <, >, ', ", ], !, ), :,
// comma or dot
- // Note: outer parentheses are a part of C++ raw string delimiters, not of
- // the regex (see http://en.cppreference.com/w/cpp/language/string_literal).
- // Note2: yet another pair of outer parentheses are \1 in the replacement.
static const QRegularExpression FullUrlRegExp(QStringLiteral(
- R"(((www\.(?!\.)|(https?|ftp|magnet)://)(&(?![lg]t;)|[^&\s<>'"])+(&(?![lg]t;)|[^&!,.\s<>'"\]):])))"
+ R"(\b((www\.(?!\.)(?!(\w|\.|-)+@)|(https?|ftp|magnet)://)(&(?![lg]t;)|[^&\s<>'"])+(&(?![lg]t;)|[^&!,.\s<>'"\]):])))"
), RegExpOptions);
// email address:
// [word chars, dots or dashes]@[word chars, dots or dashes].[word chars]
static const QRegularExpression EmailAddressRegExp(QStringLiteral(
- R"((mailto:)?(\b(\w|\.|-)+@(\w|\.|-)+\.\w+\b))"
+ R"(\b(mailto:)?((\w|\.|-)+@(\w|\.|-)+\.\w+\b))"
), RegExpOptions);
// An interim liberal implementation of
// https://matrix.org/docs/spec/appendices.html#identifier-grammar
static const QRegularExpression MxIdRegExp(QStringLiteral(
- R"((^|[^<>/])([!#@][-a-z0-9_=/.]{1,252}:[-.a-z0-9]+))"
+ R"((^|[^<>/])([!#@][-a-z0-9_=/.]{1,252}:(?:\w|\.|-)+\.\w+(?::\d{1,5})?))"
), RegExpOptions);
- // NOTE: htmlEscapedText is already HTML-escaped! No literal <,>,&
+ // NOTE: htmlEscapedText is already HTML-escaped! No literal <,>,&,"
htmlEscapedText.replace(EmailAddressRegExp,
- QStringLiteral(R"(<a href="mailto:\2">\1\2</a>)"));
+ QStringLiteral(R"(<a href="mailto:\2">\1\2</a>)"));
htmlEscapedText.replace(FullUrlRegExp,
- QStringLiteral(R"(<a href="\1">\1</a>)"));
+ QStringLiteral(R"(<a href="\1">\1</a>)"));
htmlEscapedText.replace(MxIdRegExp,
- QStringLiteral(R"(\1<a href="https://matrix.to/#/\2">\2</a>)"));
+ QStringLiteral(R"(\1<a href="https://matrix.to/#/\2">\2</a>)"));
}
-QString QMatrixClient::prettyPrint(const QString& plainText)
+QString QMatrixClient::sanitized(const QString& plainText)
{
- auto pt = QStringLiteral("<span style='white-space:pre-wrap'>") +
- plainText.toHtmlEscaped() + QStringLiteral("</span>");
- pt.replace('\n', QStringLiteral("<br/>"));
+ auto text = plainText;
+ text.remove(QChar(0x202e)); // RLO
+ text.remove(QChar(0x202d)); // LRO
+ text.remove(QChar(0xfffc)); // Object replacement character
+ return text;
+}
+QString QMatrixClient::prettyPrint(const QString& plainText)
+{
+ auto pt = plainText.toHtmlEscaped();
linkifyUrls(pt);
- return pt;
+ pt.replace('\n', QStringLiteral("<br/>"));
+ return QStringLiteral("<span style='white-space:pre-wrap'>") + pt
+ + QStringLiteral("</span>");
}
QString QMatrixClient::cacheLocation(const QString& dirName)
@@ -148,7 +157,7 @@ static_assert(!is_callable_v<fn_object<int>>, "Test non-function object");
// "Test returns<> with static member function");
template <typename T>
-QString ft(T&&);
+QString ft(T&&) { return {}; }
static_assert(std::is_same<fn_arg_t<decltype(ft<QString>)>, QString&&>(),
"Test function templates");
diff --git a/lib/util.h b/lib/util.h
index f7f646da..f08c1c95 100644
--- a/lib/util.h
+++ b/lib/util.h
@@ -296,8 +296,16 @@ namespace QMatrixClient
return std::make_pair(last, sLast);
}
- /** Pretty-prints plain text into HTML
- * This includes HTML escaping of <,>,",& and URLs linkification.
+ /** Convert what looks like a URL or a Matrix ID to an HTML hyperlink */
+ void linkifyUrls(QString& htmlEscapedText);
+
+ /** Sanitize the text before showing in HTML
+ * This does toHtmlEscaped() and removes Unicode BiDi marks.
+ */
+ QString sanitized(const QString& plainText);
+
+ /** Pretty-print plain text into HTML
+ * This includes HTML escaping of <,>,",& and calling linkifyUrls()
*/
QString prettyPrint(const QString& plainText);