aboutsummaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
Diffstat (limited to 'lib')
-rw-r--r--lib/connection.cpp132
-rw-r--r--lib/connection.h82
-rw-r--r--lib/connectiondata.cpp24
-rw-r--r--lib/connectiondata.h2
-rw-r--r--lib/converters.h32
-rw-r--r--lib/csapi/gtad.yaml4
-rw-r--r--lib/csapi/search.cpp191
-rw-r--r--lib/csapi/search.h203
-rw-r--r--lib/events/accountdataevents.h10
-rw-r--r--lib/events/roomevent.cpp6
-rw-r--r--lib/events/roomevent.h5
-rw-r--r--lib/events/roommemberevent.cpp14
-rw-r--r--lib/events/roommemberevent.h7
-rw-r--r--lib/events/roommessageevent.cpp23
-rw-r--r--lib/events/roommessageevent.h2
-rw-r--r--lib/jobs/basejob.cpp90
-rw-r--r--lib/jobs/basejob.h32
-rw-r--r--lib/jobs/downloadfilejob.h2
-rw-r--r--lib/jobs/requestdata.cpp5
-rw-r--r--lib/room.cpp209
-rw-r--r--lib/room.h114
-rw-r--r--lib/syncdata.cpp15
-rw-r--r--lib/user.h13
-rw-r--r--lib/util.cpp2
-rw-r--r--lib/util.h148
25 files changed, 565 insertions, 802 deletions
diff --git a/lib/connection.cpp b/lib/connection.cpp
index 2c5bf574..5bddbb83 100644
--- a/lib/connection.cpp
+++ b/lib/connection.cpp
@@ -99,7 +99,8 @@ public:
DirectChatsMap dcLocalAdditions;
DirectChatsMap dcLocalRemovals;
UnorderedMap<QString, EventPtr> accountData;
- int syncLoopTimeout = -1;
+ QMetaObject::Connection syncLoopConnection {};
+ int syncTimeout = -1;
GetCapabilitiesJob* capabilitiesJob = nullptr;
GetCapabilitiesJob::Capabilities capabilities;
@@ -259,8 +260,6 @@ void Connection::doConnectToServer(const QString& user, const QString& password,
});
}
-void Connection::syncLoopIteration() { sync(d->syncLoopTimeout); }
-
void Connection::connectWithToken(const QString& userId,
const QString& accessToken,
const QString& deviceId)
@@ -278,16 +277,16 @@ void Connection::reloadCapabilities()
else if (d->capabilitiesJob->error() == BaseJob::IncorrectRequestError)
qCDebug(MAIN) << "Server doesn't support /capabilities";
- if (d->capabilities.roomVersions.omitted()) {
+ if (!d->capabilities.roomVersions) {
qCWarning(MAIN) << "Pinning supported room version to 1";
d->capabilities.roomVersions = { "1", { { "1", "stable" } } };
} else {
qCDebug(MAIN) << "Room versions:" << defaultRoomVersion()
<< "is default, full list:" << availableRoomVersions();
}
- Q_ASSERT(!d->capabilities.roomVersions.omitted());
+ Q_ASSERT(d->capabilities.roomVersions.has_value());
emit capabilitiesLoaded();
- for (auto* r : d->roomMap)
+ for (auto* r : qAsConst(d->roomMap))
r->checkVersion();
});
}
@@ -296,7 +295,7 @@ bool Connection::loadingCapabilities() const
{
// (Ab)use the fact that room versions cannot be omitted after
// the capabilities have been loaded (see reloadCapabilities() above).
- return d->capabilities.roomVersions.omitted();
+ return !d->capabilities.roomVersions;
}
void Connection::Private::connectWithToken(const QString& userId,
@@ -335,25 +334,38 @@ void Connection::checkAndConnect(const QString& userId,
void Connection::logout()
{
- auto job = callApi<LogoutJob>();
- connect(job, &LogoutJob::finished, this, [job, this] {
- if (job->status().good() || job->error() == BaseJob::ContentAccessError) {
- stopSync();
+ // If there's an ongoing sync job, stop it but don't break the sync loop yet
+ const auto syncWasRunning = bool(d->syncJob);
+ if (syncWasRunning)
+ {
+ d->syncJob->abandon();
+ d->syncJob = nullptr;
+ }
+ const auto* job = callApi<LogoutJob>();
+ connect(job, &LogoutJob::finished, this, [this, job, syncWasRunning] {
+ if (job->status().good() || job->error() == BaseJob::Unauthorised
+ || job->error() == BaseJob::ContentAccessError) {
+ if (d->syncLoopConnection)
+ disconnect(d->syncLoopConnection);
d->data->setToken({});
emit stateChanged();
emit loggedOut();
- }
+ } else if (syncWasRunning)
+ syncLoopIteration(); // Resume sync loop (or a single sync)
});
}
void Connection::sync(int timeout)
{
- if (d->syncJob)
+ if (d->syncJob) {
+ qCInfo(MAIN) << d->syncJob << "is already running";
return;
+ }
+ d->syncTimeout = timeout;
Filter filter;
- filter.room->timeline->limit = 100;
- filter.room->state->lazyLoadMembers = d->lazyLoading;
+ filter.room.edit().timeline.edit().limit.emplace(100);
+ filter.room.edit().state.edit().lazyLoadMembers.emplace(d->lazyLoading);
auto job = d->syncJob =
callApi<SyncJob>(BackgroundRequest, d->data->lastEvent(), filter,
timeout);
@@ -369,9 +381,9 @@ void Connection::sync(int timeout)
});
connect(job, &SyncJob::failure, this, [this, job] {
d->syncJob = nullptr;
- if (job->error() == BaseJob::ContentAccessError) {
- qCWarning(SYNCJOB) << "Sync job failed with ContentAccessError - "
- "login expired?";
+ if (job->error() == BaseJob::Unauthorised) {
+ qCWarning(SYNCJOB)
+ << "Sync job failed with Unauthorised - login expired?";
emit loginError(job->errorString(), job->rawDataSample());
} else
emit syncError(job->errorString(), job->rawDataSample());
@@ -380,12 +392,26 @@ void Connection::sync(int timeout)
void Connection::syncLoop(int timeout)
{
- d->syncLoopTimeout = timeout;
- connect(this, &Connection::syncDone, this, &Connection::syncLoopIteration);
- syncLoopIteration(); // initial sync to start the loop
+ if (d->syncLoopConnection && d->syncTimeout == timeout) {
+ qCInfo(MAIN) << "Attempt to run sync loop but there's one already "
+ "running; nothing will be done";
+ return;
+ }
+ std::swap(d->syncTimeout, timeout);
+ if (d->syncLoopConnection) {
+ qCInfo(MAIN) << "Timeout for next syncs changed from"
+ << timeout << "to" << d->syncTimeout;
+ } else {
+ d->syncLoopConnection = connect(this, &Connection::syncDone,
+ this, &Connection::syncLoopIteration,
+ Qt::QueuedConnection);
+ syncLoopIteration(); // initial sync to start the loop
+ }
}
-QJsonObject toJson(const Connection::DirectChatsMap& directChats)
+void Connection::syncLoopIteration() { sync(d->syncTimeout); }
+
+QJsonObject toJson(const DirectChatsMap& directChats)
{
QJsonObject json;
for (auto it = directChats.begin(); it != directChats.end();) {
@@ -421,7 +447,7 @@ void Connection::onSyncSuccess(SyncData&& data, bool fromCache)
r->updateData(std::move(roomData), fromCache);
if (d->firstTimeRooms.removeOne(r)) {
emit loadedRoomState(r);
- if (!d->capabilities.roomVersions.omitted())
+ if (d->capabilities.roomVersions)
r->checkVersion();
// Otherwise, the version will be checked in reloadCapabilities()
}
@@ -514,8 +540,7 @@ void Connection::onSyncSuccess(SyncData&& data, bool fromCache)
void Connection::stopSync()
{
// If there's a sync loop, break it
- disconnect(this, &Connection::syncDone, this,
- &Connection::syncLoopIteration);
+ disconnect(d->syncLoopConnection);
if (d->syncJob) // If there's an ongoing sync job, stop it too
{
d->syncJob->abandon();
@@ -534,10 +559,14 @@ JoinRoomJob* Connection::joinRoom(const QString& roomAlias,
const QStringList& serverNames)
{
auto job = callApi<JoinRoomJob>(roomAlias, serverNames);
- // Upon completion, ensure a room object in Join state is created but only
- // if it's not already there due to a sync completing earlier.
- connect(job, &JoinRoomJob::success, this,
- [this, job] { provideRoom(job->roomId()); });
+ // Upon completion, ensure a room object in Join state is created
+ // (or it might already be there due to a sync completing earlier).
+ // finished() is used here instead of success() to overtake clients
+ // that may add their own slots to finished().
+ connect(job, &BaseJob::finished, this, [this, job] {
+ if (job->status().good())
+ provideRoom(job->roomId());
+ });
return job;
}
@@ -595,12 +624,17 @@ UploadContentJob*
Connection::uploadContent(QIODevice* contentSource, const QString& filename,
const QString& overrideContentType) const
{
+ Q_ASSERT(contentSource != nullptr);
auto contentType = overrideContentType;
if (contentType.isEmpty()) {
contentType = QMimeDatabase()
.mimeTypeForFileNameAndData(filename, contentSource)
.name();
- contentSource->open(QIODevice::ReadOnly);
+ if (!contentSource->open(QIODevice::ReadOnly)) {
+ qCWarning(MAIN) << "Couldn't open content source" << filename
+ << "for reading:" << contentSource->errorString();
+ return nullptr;
+ }
}
return callApi<UploadContentJob>(contentSource, filename, contentType);
}
@@ -609,11 +643,6 @@ UploadContentJob* Connection::uploadFile(const QString& fileName,
const QString& overrideContentType)
{
auto sourceFile = new QFile(fileName);
- if (!sourceFile->open(QIODevice::ReadOnly)) {
- qCWarning(MAIN) << "Couldn't open" << sourceFile->fileName()
- << "for reading";
- return nullptr;
- }
return uploadContent(sourceFile, QFileInfo(*sourceFile).fileName(),
overrideContentType);
}
@@ -775,7 +804,7 @@ ForgetRoomJob* Connection::forgetRoom(const QString& id)
if (!room)
room = d->roomMap.value({ id, true });
if (room && room->joinState() != JoinState::Leave) {
- auto leaveJob = room->leaveRoom();
+ auto leaveJob = leaveRoom(room);
connect(leaveJob, &BaseJob::result, this,
[this, leaveJob, forgetJob, room] {
if (leaveJob->error() == BaseJob::Success
@@ -1046,7 +1075,7 @@ QVector<Room*> Connection::roomsWithTag(const QString& tagName) const
return rooms;
}
-Connection::DirectChatsMap Connection::directChats() const
+DirectChatsMap Connection::directChats() const
{
return d->directChats;
}
@@ -1113,7 +1142,7 @@ bool Connection::isIgnored(const User* user) const
return ignoredUsers().contains(user->id());
}
-Connection::IgnoredUsersList Connection::ignoredUsers() const
+IgnoredUsersList Connection::ignoredUsers() const
{
const auto* event = d->unpackAccountData<IgnoredUsersEvent>();
return event ? event->ignored_users() : IgnoredUsersList();
@@ -1154,7 +1183,7 @@ Room* Connection::provideRoom(const QString& id, Omittable<JoinState> joinState)
// TODO: This whole function is a strong case for a RoomManager class.
Q_ASSERT_X(!id.isEmpty(), __FUNCTION__, "Empty room id");
- // If joinState.omitted(), all joinState == comparisons below are false.
+ // If joinState is empty, all joinState == comparisons below are false.
const auto roomKey = qMakePair(id, joinState == JoinState::Invite);
auto* room = d->roomMap.value(roomKey, nullptr);
if (room) {
@@ -1163,7 +1192,7 @@ Room* Connection::provideRoom(const QString& id, Omittable<JoinState> joinState)
// and emit a signal. For Invite and Join, there's no such problem.
if (room->joinState() == joinState && joinState != JoinState::Leave)
return room;
- } else if (joinState.omitted()) {
+ } else if (!joinState) {
// No Join and Leave, maybe Invite?
room = d->roomMap.value({ id, true }, nullptr);
if (room)
@@ -1172,9 +1201,7 @@ Room* Connection::provideRoom(const QString& id, Omittable<JoinState> joinState)
}
if (!room) {
- room = roomFactory()(this, id,
- joinState.omitted() ? JoinState::Join
- : joinState.value());
+ room = roomFactory()(this, id, joinState.value_or(JoinState::Join));
if (!room) {
qCCritical(MAIN) << "Failed to create a room" << id;
return nullptr;
@@ -1185,20 +1212,20 @@ Room* Connection::provideRoom(const QString& id, Omittable<JoinState> joinState)
&Connection::aboutToDeleteRoom);
emit newRoom(room);
}
- if (joinState.omitted())
+ if (!joinState)
return room;
- if (joinState == JoinState::Invite) {
+ if (*joinState == JoinState::Invite) {
// prev is either Leave or nullptr
auto* prev = d->roomMap.value({ id, false }, nullptr);
emit invitedRoom(room, prev);
} else {
- room->setJoinState(joinState.value());
+ room->setJoinState(*joinState);
// Preempt the Invite room (if any) with a room in Join/Leave state.
auto* prevInvite = d->roomMap.take({ id, true });
- if (joinState == JoinState::Join)
+ if (*joinState == JoinState::Join)
emit joinedRoom(room, prevInvite);
- else if (joinState == JoinState::Leave)
+ else if (*joinState == JoinState::Leave)
emit leftRoom(room, prevInvite);
if (prevInvite) {
const auto dcUsers = prevInvite->directChatUsers();
@@ -1387,8 +1414,7 @@ void Connection::setLazyLoading(bool newValue)
void Connection::run(BaseJob* job, RunningPolicy runningPolicy) const
{
connect(job, &BaseJob::failure, this, &Connection::requestFailed);
- job->prepare(d->data.get(), runningPolicy & BackgroundRequest);
- d->data->submit(job);
+ job->initiate(d->data.get(), runningPolicy & BackgroundRequest);
}
void Connection::getTurnServers()
@@ -1403,13 +1429,13 @@ const QString Connection::SupportedRoomVersion::StableTag =
QString Connection::defaultRoomVersion() const
{
- Q_ASSERT(!d->capabilities.roomVersions.omitted());
+ Q_ASSERT(d->capabilities.roomVersions.has_value());
return d->capabilities.roomVersions->defaultVersion;
}
QStringList Connection::stableRoomVersions() const
{
- Q_ASSERT(!d->capabilities.roomVersions.omitted());
+ Q_ASSERT(d->capabilities.roomVersions.has_value());
QStringList l;
const auto& allVersions = d->capabilities.roomVersions->available;
for (auto it = allVersions.begin(); it != allVersions.end(); ++it)
@@ -1429,7 +1455,7 @@ inline bool roomVersionLess(const Connection::SupportedRoomVersion& v1,
QVector<Connection::SupportedRoomVersion> Connection::availableRoomVersions() const
{
- Q_ASSERT(!d->capabilities.roomVersions.omitted());
+ Q_ASSERT(d->capabilities.roomVersions.has_value());
QVector<SupportedRoomVersion> result;
result.reserve(d->capabilities.roomVersions->available.size());
for (auto it = d->capabilities.roomVersions->available.begin();
diff --git a/lib/connection.h b/lib/connection.h
index 1f1d4cd5..e4109fd4 100644
--- a/lib/connection.h
+++ b/lib/connection.h
@@ -97,6 +97,14 @@ enum RunningPolicy { ForegroundRequest = 0x0, BackgroundRequest = 0x1 };
Q_ENUM_NS(RunningPolicy)
+// Room ids, rather than room pointers, are used in the direct chat
+// map types because the library keeps Invite rooms separate from
+// rooms in Join and Leave state; and direct chats in account data
+// are stored with no regard to their state.
+using DirectChatsMap = QMultiHash<const User*, QString>;
+using DirectChatUsersMap = QMultiHash<QString, User*>;
+using IgnoredUsersList = IgnoredUsersEvent::content_type;
+
class Connection : public QObject {
Q_OBJECT
@@ -115,14 +123,6 @@ class Connection : public QObject {
lazyLoadingChanged)
public:
- // Room ids, rather than room pointers, are used in the direct chat
- // map types because the library keeps Invite rooms separate from
- // rooms in Join and Leave state; and direct chats in account data
- // are stored with no regard to their state.
- using DirectChatsMap = QMultiHash<const User*, QString>;
- using DirectChatUsersMap = QMultiHash<QString, User*>;
- using IgnoredUsersList = IgnoredUsersEvent::content_type;
-
using UsersToDevicesToEvents =
UnorderedMap<QString, UnorderedMap<QString, const Event&>>;
@@ -153,7 +153,7 @@ public:
* from the server.
* \sa rooms, room, roomsWithTag
*/
- Q_INVOKABLE QVector<Room*> allRooms() const;
+ Q_INVOKABLE QVector<Quotient::Room*> allRooms() const;
/// Get rooms that have either of the given join state(s)
/*!
@@ -163,10 +163,11 @@ public:
* Leave rooms from the server.
* \sa allRooms, room, roomsWithTag
*/
- Q_INVOKABLE QVector<Room*> rooms(JoinStates joinStates) const;
+ Q_INVOKABLE QVector<Quotient::Room*>
+ rooms(Quotient::JoinStates joinStates) const;
/// Get the total number of rooms in the given join state(s)
- Q_INVOKABLE int roomsCount(JoinStates joinStates) const;
+ Q_INVOKABLE int roomsCount(Quotient::JoinStates joinStates) const;
/** Check whether the account has data of the given type
* Direct chats map is not supported by this method _yet_.
@@ -253,10 +254,10 @@ public:
QList<User*> directChatUsers(const Room* room) const;
/** Check whether a particular user is in the ignore list */
- Q_INVOKABLE bool isIgnored(const User* user) const;
+ Q_INVOKABLE bool isIgnored(const Quotient::User* user) const;
/** Get the whole list of ignored users */
- Q_INVOKABLE IgnoredUsersList ignoredUsers() const;
+ Q_INVOKABLE Quotient::IgnoredUsersList ignoredUsers() const;
/** Add the user to the ignore list
* The change signal is emitted synchronously, without waiting
@@ -264,14 +265,14 @@ public:
*
* \sa ignoredUsersListChanged
*/
- Q_INVOKABLE void addToIgnoredUsers(const User* user);
+ Q_INVOKABLE void addToIgnoredUsers(const Quotient::User* user);
/** Remove the user from the ignore list */
/** Similar to adding, the change signal is emitted synchronously.
*
* \sa ignoredUsersListChanged
*/
- Q_INVOKABLE void removeFromIgnoredUsers(const User* user);
+ Q_INVOKABLE void removeFromIgnoredUsers(const Quotient::User* user);
/** Get the full list of users known to this account */
QMap<QString, User*> users() const;
@@ -281,13 +282,14 @@ public:
/** 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;
+ Q_INVOKABLE Quotient::Room*
+ room(const QString& roomId,
+ Quotient::JoinStates states = JoinState::Invite | JoinState::Join) const;
/** Find a room by its alias and a mask of applicable states */
- Q_INVOKABLE Room* roomByAlias(const QString& roomAlias,
- JoinStates states = JoinState::Invite
- | JoinState::Join) const;
+ Q_INVOKABLE Quotient::Room*
+ roomByAlias(const QString& roomAlias,
+ Quotient::JoinStates states = JoinState::Invite
+ | JoinState::Join) const;
/** Update the internal map of room aliases to IDs */
/// This is used to maintain the internal index of room aliases.
/// It does NOT change aliases on the server,
@@ -295,15 +297,15 @@ public:
void updateRoomAliases(const QString& roomId, const QString& aliasServer,
const QStringList& previousRoomAliases,
const QStringList& roomAliases);
- Q_INVOKABLE Room* invitation(const QString& roomId) const;
- Q_INVOKABLE User* user(const QString& userId);
+ Q_INVOKABLE Quotient::Room* invitation(const QString& roomId) const;
+ Q_INVOKABLE Quotient::User* user(const QString& userId);
const User* user() const;
User* user();
QString userId() const;
QString deviceId() const;
QByteArray accessToken() const;
QtOlm::Account* olmAccount() const;
- Q_INVOKABLE SyncJob* syncJob() const;
+ Q_INVOKABLE Quotient::SyncJob* syncJob() const;
Q_INVOKABLE int millisToReconnect() const;
Q_INVOKABLE void getTurnServers();
@@ -589,6 +591,7 @@ public slots:
/** @deprecated Use callApi<PostReceiptJob>() or Room::postReceipt() instead
*/
virtual PostReceiptJob* postReceipt(Room* room, RoomEvent* event) const;
+
signals:
/**
* @deprecated
@@ -622,7 +625,7 @@ signals:
*
* @param request - the pointer to the failed job
*/
- void requestFailed(BaseJob* request);
+ void requestFailed(Quotient::BaseJob* request);
/** A network request (job) failed due to network problems
*
@@ -640,7 +643,7 @@ signals:
void syncDone();
void syncError(QString message, QString details);
- void newUser(User* user);
+ void newUser(Quotient::User* user);
/**
* \group Signals emitted on room transitions
@@ -672,7 +675,7 @@ signals:
*/
/** A new room object has been created */
- void newRoom(Room* room);
+ void newRoom(Quotient::Room* room);
/** A room invitation is seen for the first time
*
@@ -680,7 +683,7 @@ signals:
* that initial sync will trigger this signal for all rooms in
* Invite state.
*/
- void invitedRoom(Room* room, Room* prev);
+ void invitedRoom(Quotient::Room* room, Quotient::Room* prev);
/** A joined room is seen for the first time
*
@@ -691,7 +694,7 @@ signals:
* this room was in Invite state before, the respective object is
* passed in prev (and it will be deleted shortly afterwards).
*/
- void joinedRoom(Room* room, Room* prev);
+ void joinedRoom(Quotient::Room* room, Quotient::Room* prev);
/** A room has just been left
*
@@ -702,10 +705,10 @@ signals:
* Left rooms upon initial sync (not only those that were left
* right before the sync).
*/
- void leftRoom(Room* room, Room* prev);
+ void leftRoom(Quotient::Room* room, Quotient::Room* prev);
/** The room object is about to be deleted */
- void aboutToDeleteRoom(Room* room);
+ void aboutToDeleteRoom(Quotient::Room* room);
/** The room has just been created by createRoom or requestDirectChat
*
@@ -716,7 +719,7 @@ signals:
* use directChatAvailable signal if you just need to obtain
* a direct chat room.
*/
- void createdRoom(Room* room);
+ void createdRoom(Quotient::Room* room);
/** The first sync for the room has been completed
*
@@ -726,7 +729,7 @@ signals:
* signals (newRoom, joinedRoom etc.) come earlier, when the room
* has just been created.
*/
- void loadedRoomState(Room* room);
+ void loadedRoomState(Quotient::Room* room);
/** Account data (except direct chats) have changed */
void accountDataChanged(QString type);
@@ -735,18 +738,18 @@ signals:
* This signal is emitted upon any successful outcome from
* requestDirectChat.
*/
- void directChatAvailable(Room* directChat);
+ void directChatAvailable(Quotient::Room* directChat);
/** The list of direct chats has changed
* This signal is emitted every time when the mapping of users
* to direct chat rooms is changed (because of either local updates
* or a different list arrived from the server).
*/
- void directChatsListChanged(DirectChatsMap additions,
- DirectChatsMap removals);
+ void directChatsListChanged(Quotient::DirectChatsMap additions,
+ Quotient::DirectChatsMap removals);
- void ignoredUsersListChanged(IgnoredUsersList additions,
- IgnoredUsersList removals);
+ void ignoredUsersListChanged(Quotient::IgnoredUsersList additions,
+ Quotient::IgnoredUsersList removals);
void cacheStateChanged();
void lazyLoadingChanged();
@@ -812,4 +815,5 @@ private:
static user_factory_t _userFactory;
};
} // namespace Quotient
-Q_DECLARE_METATYPE(Quotient::Connection*)
+Q_DECLARE_METATYPE(Quotient::DirectChatsMap)
+Q_DECLARE_METATYPE(Quotient::IgnoredUsersList)
diff --git a/lib/connectiondata.cpp b/lib/connectiondata.cpp
index a3807fc4..e806f952 100644
--- a/lib/connectiondata.cpp
+++ b/lib/connectiondata.cpp
@@ -42,6 +42,7 @@ public:
QString lastEvent;
QString userId;
QString deviceId;
+ std::vector<QString> needToken;
mutable unsigned int txnCounter = 0;
const qint64 txnBase = QDateTime::currentMSecsSinceEpoch();
@@ -67,7 +68,6 @@ ConnectionData::ConnectionData(QUrl baseUrl)
for (auto& q : d->jobs)
while (!q.empty()) {
auto& job = q.front();
- q.pop();
if (!job || job->error() == BaseJob::Abandoned)
continue;
if (job->error() != BaseJob::Pending) {
@@ -79,19 +79,24 @@ ConnectionData::ConnectionData(QUrl baseUrl)
}
job->sendRequest();
d->rateLimiter.start();
+ q.pop();
return;
}
qCDebug(MAIN) << d->id() << "job queues are empty";
});
}
-ConnectionData::~ConnectionData() = default;
+ConnectionData::~ConnectionData()
+{
+ d->rateLimiter.disconnect();
+ d->rateLimiter.stop();
+}
void ConnectionData::submit(BaseJob* job)
{
- Q_ASSERT(job->error() == BaseJob::Pending);
+ job->setStatus(BaseJob::Pending);
if (!d->rateLimiter.isActive()) {
- job->sendRequest();
+ QTimer::singleShot(0, job, &BaseJob::sendRequest);
return;
}
d->jobs[size_t(job->isBackground())].emplace(job);
@@ -143,6 +148,12 @@ const QString& ConnectionData::deviceId() const { return d->deviceId; }
const QString& ConnectionData::userId() const { return d->userId; }
+bool ConnectionData::needsToken(const QString& requestName) const
+{
+ return std::find(d->needToken.cbegin(), d->needToken.cend(), requestName)
+ != d->needToken.cend();
+}
+
void ConnectionData::setDeviceId(const QString& deviceId)
{
d->deviceId = deviceId;
@@ -150,6 +161,11 @@ void ConnectionData::setDeviceId(const QString& deviceId)
void ConnectionData::setUserId(const QString& userId) { d->userId = userId; }
+void ConnectionData::setNeedsToken(const QString& requestName)
+{
+ d->needToken.push_back(requestName);
+}
+
QString ConnectionData::lastEvent() const { return d->lastEvent; }
void ConnectionData::setLastEvent(QString identifier)
diff --git a/lib/connectiondata.h b/lib/connectiondata.h
index b367c977..000099d1 100644
--- a/lib/connectiondata.h
+++ b/lib/connectiondata.h
@@ -40,6 +40,7 @@ public:
QUrl baseUrl() const;
const QString& deviceId() const;
const QString& userId() const;
+ bool needsToken(const QString& requestName) const;
QNetworkAccessManager* nam() const;
void setBaseUrl(QUrl baseUrl);
@@ -50,6 +51,7 @@ public:
void setPort(int port);
void setDeviceId(const QString& deviceId);
void setUserId(const QString& userId);
+ void setNeedsToken(const QString& requestName);
QString lastEvent() const;
void setLastEvent(QString identifier);
diff --git a/lib/converters.h b/lib/converters.h
index b753a80b..075af7ef 100644
--- a/lib/converters.h
+++ b/lib/converters.h
@@ -30,6 +30,7 @@
#include <vector>
+#if QT_VERSION < QT_VERSION_CHECK(5,14,0)
// Enable std::unordered_map<QString, T>
// REMOVEME in favor of UnorderedMap, once we regenerate API files
namespace std {
@@ -37,15 +38,12 @@ template <>
struct hash<QString> {
size_t operator()(const QString& s) const Q_DECL_NOEXCEPT
{
- return qHash(s
-#if (QT_VERSION >= QT_VERSION_CHECK(5, 6, 0))
- ,
- uint(qGlobalQHashSeed())
-#endif
+ return qHash(s, uint(qGlobalQHashSeed())
);
}
};
} // namespace std
+#endif
class QVariant;
@@ -206,7 +204,7 @@ template <typename T>
struct JsonConverter<Omittable<T>> {
static QJsonValue dump(const Omittable<T>& from)
{
- return from.omitted() ? QJsonValue() : toJson(from.value());
+ return from.has_value() ? toJson(from.value()) : QJsonValue();
}
static Omittable<T> load(const QJsonValue& jv)
{
@@ -378,28 +376,10 @@ namespace _impl {
static void impl(ContT& container, const QString& key,
const OmittableT& value)
{
- if (!value.omitted())
- addTo(container, key, value.value());
+ if (value)
+ addTo(container, key, *value);
}
};
-
-#if 0
- // This is a special one that unfolds optional<>
- template <typename ValT, bool Force>
- struct AddNode<optional<ValT>, Force>
- {
- template <typename ContT, typename OptionalT>
- static void impl(ContT& container,
- const QString& key, const OptionalT& value)
- {
- if (value)
- AddNode<ValT>::impl(container, key, value.value());
- else if (Force) // Edge case, no value but must put something
- AddNode<ValT>::impl(container, key, QString{});
- }
- };
-#endif
-
} // namespace _impl
static constexpr bool IfNotEmpty = false;
diff --git a/lib/csapi/gtad.yaml b/lib/csapi/gtad.yaml
index 301ee0b6..6d4e080f 100644
--- a/lib/csapi/gtad.yaml
+++ b/lib/csapi/gtad.yaml
@@ -95,9 +95,9 @@ analyzer:
- //: { type: "QVector<{{1}}>", imports: <QtCore/QVector> }
- map: # `additionalProperties` in OpenAPI
- RoomState:
- type: "std::unordered_map<QString, {{1}}>"
+ type: "UnorderedMap<QString, {{1}}>"
moveOnly:
- imports: <unordered_map>
+ imports: '"util.h"'
- /.+/:
type: "QHash<QString, {{1}}>"
imports: <QtCore/QHash>
diff --git a/lib/csapi/search.cpp b/lib/csapi/search.cpp
deleted file mode 100644
index 9619f340..00000000
--- a/lib/csapi/search.cpp
+++ /dev/null
@@ -1,191 +0,0 @@
-/******************************************************************************
- * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN
- */
-
-#include "search.h"
-
-#include "converters.h"
-
-#include <QtCore/QStringBuilder>
-
-using namespace Quotient;
-
-static const auto basePath = QStringLiteral("/_matrix/client/r0");
-
-// Converters
-namespace Quotient
-{
-
-template <>
-struct JsonObjectConverter<SearchJob::IncludeEventContext>
-{
- static void dumpTo(QJsonObject& jo,
- const SearchJob::IncludeEventContext& pod)
- {
- addParam<IfNotEmpty>(jo, QStringLiteral("before_limit"),
- pod.beforeLimit);
- addParam<IfNotEmpty>(jo, QStringLiteral("after_limit"), pod.afterLimit);
- addParam<IfNotEmpty>(jo, QStringLiteral("include_profile"),
- pod.includeProfile);
- }
-};
-
-template <>
-struct JsonObjectConverter<SearchJob::Group>
-{
- static void dumpTo(QJsonObject& jo, const SearchJob::Group& pod)
- {
- addParam<IfNotEmpty>(jo, QStringLiteral("key"), pod.key);
- }
-};
-
-template <>
-struct JsonObjectConverter<SearchJob::Groupings>
-{
- static void dumpTo(QJsonObject& jo, const SearchJob::Groupings& pod)
- {
- addParam<IfNotEmpty>(jo, QStringLiteral("group_by"), pod.groupBy);
- }
-};
-
-template <>
-struct JsonObjectConverter<SearchJob::RoomEventsCriteria>
-{
- static void dumpTo(QJsonObject& jo, const SearchJob::RoomEventsCriteria& pod)
- {
- addParam<>(jo, QStringLiteral("search_term"), pod.searchTerm);
- addParam<IfNotEmpty>(jo, QStringLiteral("keys"), pod.keys);
- addParam<IfNotEmpty>(jo, QStringLiteral("filter"), pod.filter);
- addParam<IfNotEmpty>(jo, QStringLiteral("order_by"), pod.orderBy);
- addParam<IfNotEmpty>(jo, QStringLiteral("event_context"),
- pod.eventContext);
- addParam<IfNotEmpty>(jo, QStringLiteral("include_state"),
- pod.includeState);
- addParam<IfNotEmpty>(jo, QStringLiteral("groupings"), pod.groupings);
- }
-};
-
-template <>
-struct JsonObjectConverter<SearchJob::Categories>
-{
- static void dumpTo(QJsonObject& jo, const SearchJob::Categories& pod)
- {
- addParam<IfNotEmpty>(jo, QStringLiteral("room_events"), pod.roomEvents);
- }
-};
-
-template <>
-struct JsonObjectConverter<SearchJob::UserProfile>
-{
- static void fillFrom(const QJsonObject& jo, SearchJob::UserProfile& result)
- {
- fromJson(jo.value("displayname"_ls), result.displayname);
- fromJson(jo.value("avatar_url"_ls), result.avatarUrl);
- }
-};
-
-template <>
-struct JsonObjectConverter<SearchJob::EventContext>
-{
- static void fillFrom(const QJsonObject& jo, SearchJob::EventContext& result)
- {
- fromJson(jo.value("start"_ls), result.begin);
- fromJson(jo.value("end"_ls), result.end);
- fromJson(jo.value("profile_info"_ls), result.profileInfo);
- fromJson(jo.value("events_before"_ls), result.eventsBefore);
- fromJson(jo.value("events_after"_ls), result.eventsAfter);
- }
-};
-
-template <>
-struct JsonObjectConverter<SearchJob::Result>
-{
- static void fillFrom(const QJsonObject& jo, SearchJob::Result& result)
- {
- fromJson(jo.value("rank"_ls), result.rank);
- fromJson(jo.value("result"_ls), result.result);
- fromJson(jo.value("context"_ls), result.context);
- }
-};
-
-template <>
-struct JsonObjectConverter<SearchJob::GroupValue>
-{
- static void fillFrom(const QJsonObject& jo, SearchJob::GroupValue& result)
- {
- fromJson(jo.value("next_batch"_ls), result.nextBatch);
- fromJson(jo.value("order"_ls), result.order);
- fromJson(jo.value("results"_ls), result.results);
- }
-};
-
-template <>
-struct JsonObjectConverter<SearchJob::ResultRoomEvents>
-{
- static void fillFrom(const QJsonObject& jo,
- SearchJob::ResultRoomEvents& result)
- {
- fromJson(jo.value("count"_ls), result.count);
- fromJson(jo.value("highlights"_ls), result.highlights);
- fromJson(jo.value("results"_ls), result.results);
- fromJson(jo.value("state"_ls), result.state);
- fromJson(jo.value("groups"_ls), result.groups);
- fromJson(jo.value("next_batch"_ls), result.nextBatch);
- }
-};
-
-template <>
-struct JsonObjectConverter<SearchJob::ResultCategories>
-{
- static void fillFrom(const QJsonObject& jo,
- SearchJob::ResultCategories& result)
- {
- fromJson(jo.value("room_events"_ls), result.roomEvents);
- }
-};
-
-} // namespace Quotient
-
-class SearchJob::Private
-{
-public:
- ResultCategories searchCategories;
-};
-
-BaseJob::Query queryToSearch(const QString& nextBatch)
-{
- BaseJob::Query _q;
- addParam<IfNotEmpty>(_q, QStringLiteral("next_batch"), nextBatch);
- return _q;
-}
-
-static const auto SearchJobName = QStringLiteral("SearchJob");
-
-SearchJob::SearchJob(const Categories& searchCategories,
- const QString& nextBatch)
- : BaseJob(HttpVerb::Post, SearchJobName, basePath % "/search",
- queryToSearch(nextBatch))
- , d(new Private)
-{
- QJsonObject _data;
- addParam<>(_data, QStringLiteral("search_categories"), searchCategories);
- setRequestData(_data);
-}
-
-SearchJob::~SearchJob() = default;
-
-const SearchJob::ResultCategories& SearchJob::searchCategories() const
-{
- return d->searchCategories;
-}
-
-BaseJob::Status SearchJob::parseJson(const QJsonDocument& data)
-{
- auto json = data.object();
- if (!json.contains("search_categories"_ls))
- return { IncorrectResponse,
- "The key 'search_categories' not found in the response" };
- fromJson(json.value("search_categories"_ls), d->searchCategories);
-
- return Success;
-}
diff --git a/lib/csapi/search.h b/lib/csapi/search.h
deleted file mode 100644
index 079ac8e9..00000000
--- a/lib/csapi/search.h
+++ /dev/null
@@ -1,203 +0,0 @@
-/******************************************************************************
- * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN
- */
-
-#pragma once
-
-#include "converters.h"
-
-#include "csapi/definitions/room_event_filter.h"
-
-#include "events/eventloader.h"
-#include "jobs/basejob.h"
-
-#include <QtCore/QHash>
-#include <QtCore/QVector>
-
-#include <unordered_map>
-
-namespace Quotient
-{
-
-// Operations
-
-/// Perform a server-side search.
-/*!
- * Performs a full text search across different categories.
- */
-class SearchJob : public BaseJob
-{
-public:
- // Inner data structures
-
- /// Configures whether any context for the eventsreturned are included in
- /// the response.
- struct IncludeEventContext
- {
- /// How many events before the result arereturned. By default, this is
- /// ``5``.
- Omittable<int> beforeLimit;
- /// How many events after the result arereturned. By default, this is
- /// ``5``.
- Omittable<int> afterLimit;
- /// Requests that the server returns thehistoric profile information for
- /// the usersthat sent the events that were returned.By default, this is
- /// ``false``.
- Omittable<bool> includeProfile;
- };
-
- /// Configuration for group.
- struct Group
- {
- /// Key that defines the group.
- QString key;
- };
-
- /// Requests that the server partitions the result setbased on the provided
- /// list of keys.
- struct Groupings
- {
- /// List of groups to request.
- QVector<Group> groupBy;
- };
-
- /// Mapping of category name to search criteria.
- struct RoomEventsCriteria
- {
- /// The string to search events for
- QString searchTerm;
- /// The keys to search. Defaults to all.
- QStringList keys;
- /// This takes a `filter`_.
- Omittable<RoomEventFilter> filter;
- /// The order in which to search for results.By default, this is
- /// ``"rank"``.
- QString orderBy;
- /// Configures whether any context for the eventsreturned are included
- /// in the response.
- Omittable<IncludeEventContext> eventContext;
- /// Requests the server return the current state foreach room returned.
- Omittable<bool> includeState;
- /// Requests that the server partitions the result setbased on the
- /// provided list of keys.
- Omittable<Groupings> groupings;
- };
-
- /// Describes which categories to search in and their criteria.
- struct Categories
- {
- /// Mapping of category name to search criteria.
- Omittable<RoomEventsCriteria> roomEvents;
- };
-
- /// Performs a full text search across different categories.
- struct UserProfile
- {
- /// Performs a full text search across different categories.
- QString displayname;
- /// Performs a full text search across different categories.
- QString avatarUrl;
- };
-
- /// Context for result, if requested.
- struct EventContext
- {
- /// Pagination token for the start of the chunk
- QString begin;
- /// Pagination token for the end of the chunk
- QString end;
- /// The historic profile information of theusers that sent the events
- /// returned.The ``string`` key is the user ID for whichthe profile
- /// belongs to.
- QHash<QString, UserProfile> profileInfo;
- /// Events just before the result.
- RoomEvents eventsBefore;
- /// Events just after the result.
- RoomEvents eventsAfter;
- };
-
- /// The result object.
- struct Result
- {
- /// A number that describes how closely this result matches the search.
- /// Higher is closer.
- Omittable<double> rank;
- /// The event that matched.
- RoomEventPtr result;
- /// Context for result, if requested.
- Omittable<EventContext> context;
- };
-
- /// The results for a particular group value.
- struct GroupValue
- {
- /// Token that can be used to get the next batchof results in the group,
- /// by passing as the`next_batch` parameter to the next call. Ifthis
- /// field is absent, there are no moreresults in this group.
- QString nextBatch;
- /// Key that can be used to order differentgroups.
- Omittable<int> order;
- /// Which results are in this group.
- QStringList results;
- };
-
- /// Mapping of category name to search criteria.
- struct ResultRoomEvents
- {
- /// An approximate count of the total number of results found.
- Omittable<int> count;
- /// List of words which should be highlighted, useful for stemming which
- /// may change the query terms.
- QStringList highlights;
- /// List of results in the requested order.
- std::vector<Result> results;
- /// The current state for every room in the results.This is included if
- /// the request had the``include_state`` key set with a value of
- /// ``true``.The ``string`` key is the room ID for which the
- /// ``StateEvent`` array belongs to.
- std::unordered_map<QString, StateEvents> state;
- /// Any groups that were requested.The outer ``string`` key is the group
- /// key requested (eg: ``room_id``or ``sender``). The inner ``string``
- /// key is the grouped value (eg: a room's ID or a user's ID).
- QHash<QString, QHash<QString, GroupValue>> groups;
- /// Token that can be used to get the next batch ofresults, by passing
- /// as the `next_batch` parameter tothe next call. If this field is
- /// absent, there are nomore results.
- QString nextBatch;
- };
-
- /// Describes which categories to search in and their criteria.
- struct ResultCategories
- {
- /// Mapping of category name to search criteria.
- Omittable<ResultRoomEvents> roomEvents;
- };
-
- // Construction/destruction
-
- /*! Perform a server-side search.
- * \param searchCategories
- * Describes which categories to search in and their criteria.
- * \param nextBatch
- * The point to return events from. If given, this should be a
- * ``next_batch`` result from a previous call to this endpoint.
- */
- explicit SearchJob(const Categories& searchCategories,
- const QString& nextBatch = {});
-
- ~SearchJob() override;
-
- // Result properties
-
- /// Describes which categories to search in and their criteria.
- const ResultCategories& searchCategories() const;
-
-protected:
- Status parseJson(const QJsonDocument& data) override;
-
-private:
- class Private;
- QScopedPointer<Private> d;
-};
-
-} // namespace Quotient
diff --git a/lib/events/accountdataevents.h b/lib/events/accountdataevents.h
index 31176766..a55016d9 100644
--- a/lib/events/accountdataevents.h
+++ b/lib/events/accountdataevents.h
@@ -1,5 +1,3 @@
-#include <utility>
-
/******************************************************************************
* Copyright (C) 2018 Kitsune Ral <kitsune-ral@users.sf.net>
*
@@ -34,13 +32,13 @@ struct TagRecord {
order_type order;
- TagRecord(order_type order = none) : order(order) {}
+ TagRecord(order_type order = none) : order(std::move(order)) {}
bool operator<(const TagRecord& other) const
{
- // Per The Spec, rooms with no order should be after those with order
- return !order.omitted()
- && (other.order.omitted() || order.value() < other.order.value());
+ // Per The Spec, rooms with no order should be after those with order,
+ // against optional<>::operator<() convention.
+ return order && (!other.order || *order < *other.order);
}
};
diff --git a/lib/events/roomevent.cpp b/lib/events/roomevent.cpp
index 971d8597..a59cd6e0 100644
--- a/lib/events/roomevent.cpp
+++ b/lib/events/roomevent.cpp
@@ -36,17 +36,15 @@ RoomEvent::RoomEvent(Type type, const QJsonObject& json) : Event(type, json)
{
const auto unsignedData = json[UnsignedKeyL].toObject();
const auto redaction = unsignedData[RedactedCauseKeyL];
- if (redaction.isObject()) {
+ if (redaction.isObject())
_redactedBecause = makeEvent<RedactionEvent>(redaction.toObject());
- return;
- }
}
RoomEvent::~RoomEvent() = default; // Let the smart pointer do its job
QString RoomEvent::id() const { return fullJson()[EventIdKeyL].toString(); }
-QDateTime RoomEvent::timestamp() const
+QDateTime RoomEvent::originTimestamp() const
{
return Quotient::fromJson<QDateTime>(fullJson()["origin_server_ts"_ls]);
}
diff --git a/lib/events/roomevent.h b/lib/events/roomevent.h
index f943bce4..621652cb 100644
--- a/lib/events/roomevent.h
+++ b/lib/events/roomevent.h
@@ -46,7 +46,10 @@ public:
~RoomEvent() override;
QString id() const;
- QDateTime timestamp() const;
+ QDateTime originTimestamp() const;
+ [[deprecated("Use originTimestamp()")]] QDateTime timestamp() const {
+ return originTimestamp();
+ }
QString roomId() const;
QString senderId() const;
bool isReplaced() const;
diff --git a/lib/events/roommemberevent.cpp b/lib/events/roommemberevent.cpp
index d0787170..d4b2be45 100644
--- a/lib/events/roommemberevent.cpp
+++ b/lib/events/roommemberevent.cpp
@@ -52,6 +52,7 @@ MemberEventContent::MemberEventContent(const QJsonObject& json)
, isDirect(json["is_direct"_ls].toBool())
, displayName(sanitized(json["displayname"_ls].toString()))
, avatarUrl(json["avatar_url"_ls].toString())
+ , reason(json["reason"_ls].toString())
{}
void MemberEventContent::fillJson(QJsonObject* o) const
@@ -64,18 +65,23 @@ void MemberEventContent::fillJson(QJsonObject* o) const
o->insert(QStringLiteral("displayname"), displayName);
if (avatarUrl.isValid())
o->insert(QStringLiteral("avatar_url"), avatarUrl.toString());
+ if (!reason.isEmpty())
+ o->insert(QStringLiteral("reason"), reason);
+}
+
+bool RoomMemberEvent::changesMembership() const
+{
+ return !prevContent() || prevContent()->membership != membership();
}
bool RoomMemberEvent::isInvite() const
{
- return membership() == MembershipType::Invite
- && (!prevContent() || prevContent()->membership != membership());
+ return membership() == MembershipType::Invite && changesMembership();
}
bool RoomMemberEvent::isJoin() const
{
- return membership() == MembershipType::Join
- && (!prevContent() || prevContent()->membership != membership());
+ return membership() == MembershipType::Join && changesMembership();
}
bool RoomMemberEvent::isLeave() const
diff --git a/lib/events/roommemberevent.h b/lib/events/roommemberevent.h
index 6a34fd7f..0ca439e1 100644
--- a/lib/events/roommemberevent.h
+++ b/lib/events/roommemberevent.h
@@ -40,6 +40,7 @@ public:
bool isDirect = false;
QString displayName;
QUrl avatarUrl;
+ QString reason;
protected:
void fillJson(QJsonObject* o) const override;
@@ -56,8 +57,8 @@ public:
explicit RoomMemberEvent(const QJsonObject& obj) : StateEvent(typeId(), obj)
{}
- [[deprecated("Use RoomMemberEvent(userId, contentArgs) "
- "instead")]] RoomMemberEvent(MemberEventContent&& c)
+ [[deprecated("Use RoomMemberEvent(userId, contentArgs) instead")]]
+ RoomMemberEvent(MemberEventContent&& c)
: StateEvent(typeId(), matrixTypeId(), QString(), c)
{}
template <typename... ArgTs>
@@ -85,6 +86,8 @@ public:
bool isDirect() const { return content().isDirect; }
QString displayName() const { return content().displayName; }
QUrl avatarUrl() const { return content().avatarUrl; }
+ QString reason() const { return content().reason; }
+ bool changesMembership() const;
bool isInvite() const;
bool isJoin() const;
bool isLeave() const;
diff --git a/lib/events/roommessageevent.cpp b/lib/events/roommessageevent.cpp
index 09562d65..078ae70a 100644
--- a/lib/events/roommessageevent.cpp
+++ b/lib/events/roommessageevent.cpp
@@ -95,6 +95,11 @@ MsgType jsonToMsgType(const QString& matrixType)
return MsgType::Unknown;
}
+inline bool isReplacement(const Omittable<RelatesTo>& rel)
+{
+ return rel && rel->type == RelatesTo::ReplacementTypeId();
+}
+
QJsonObject RoomMessageEvent::assembleContentJson(const QString& plainBody,
const QString& jsonMsgType,
TypedBase* content)
@@ -111,6 +116,7 @@ QJsonObject RoomMessageEvent::assembleContentJson(const QString& plainBody,
// After the above, we know for sure that the content is TextContent
// and that its RelatesTo structure is not omitted
auto* textContent = static_cast<const TextContent*>(content);
+ Q_ASSERT(textContent && textContent->relatesTo.has_value());
if (textContent->relatesTo->type == RelatesTo::ReplacementTypeId()) {
auto newContentJson = json.take("m.new_content"_ls).toObject();
newContentJson.insert(BodyKey, plainBody);
@@ -243,9 +249,7 @@ QString RoomMessageEvent::replacedEvent() const
return {};
const auto& rel = static_cast<const TextContent*>(content())->relatesTo;
- return !rel.omitted() && rel->type == RelatesTo::ReplacementTypeId()
- ? rel->eventId
- : QString();
+ return isReplacement(rel) ? rel->eventId : QString();
}
QString rawMsgTypeForMimeType(const QMimeType& mimeType)
@@ -269,10 +273,10 @@ QString RoomMessageEvent::rawMsgTypeForFile(const QFileInfo& fi)
return rawMsgTypeForMimeType(QMimeDatabase().mimeTypeForFile(fi));
}
-TextContent::TextContent(const QString& text, const QString& contentType,
+TextContent::TextContent(QString text, const QString& contentType,
Omittable<RelatesTo> relatesTo)
: mimeType(QMimeDatabase().mimeTypeForName(contentType))
- , body(text)
+ , body(std::move(text))
, relatesTo(std::move(relatesTo))
{
if (contentType == HtmlContentTypeId)
@@ -304,10 +308,9 @@ TextContent::TextContent(const QJsonObject& json)
static const auto PlainTextMimeType = db.mimeTypeForName("text/plain");
static const auto HtmlMimeType = db.mimeTypeForName("text/html");
- const auto actualJson =
- relatesTo.omitted() || relatesTo->type != RelatesTo::ReplacementTypeId()
- ? json
- : json.value("m.new_content"_ls).toObject();
+ const auto actualJson = isReplacement(relatesTo)
+ ? json.value("m.new_content"_ls).toObject()
+ : json;
// Special-casing the custom matrix.org's (actually, Riot's) way
// of sending HTML messages.
if (actualJson["format"_ls].toString() == HtmlContentTypeId) {
@@ -331,7 +334,7 @@ void TextContent::fillJson(QJsonObject* json) const
json->insert(FormatKey, HtmlContentTypeId);
json->insert(FormattedBodyKey, body);
}
- if (!relatesTo.omitted()) {
+ if (relatesTo) {
json->insert(QStringLiteral("m.relates_to"),
QJsonObject { { relatesTo->type, relatesTo->eventId } });
if (relatesTo->type == RelatesTo::ReplacementTypeId()) {
diff --git a/lib/events/roommessageevent.h b/lib/events/roommessageevent.h
index e95aabfc..ded5e572 100644
--- a/lib/events/roommessageevent.h
+++ b/lib/events/roommessageevent.h
@@ -114,7 +114,7 @@ namespace EventContent {
*/
class TextContent : public TypedBase {
public:
- TextContent(const QString& text, const QString& contentType,
+ TextContent(QString text, const QString& contentType,
Omittable<RelatesTo> relatesTo = none);
explicit TextContent(const QJsonObject& json);
diff --git a/lib/jobs/basejob.cpp b/lib/jobs/basejob.cpp
index 13e65188..68adeaf6 100644
--- a/lib/jobs/basejob.cpp
+++ b/lib/jobs/basejob.cpp
@@ -138,8 +138,7 @@ BaseJob::BaseJob(HttpVerb verb, const QString& name, const QString& endpoint,
setObjectName(name);
connect(&d->timer, &QTimer::timeout, this, &BaseJob::timeout);
connect(&d->retryTimer, &QTimer::timeout, this, [this] {
- setStatus(Pending);
- sendRequest();
+ d->connection->submit(this);
});
}
@@ -226,8 +225,9 @@ void BaseJob::Private::sendRequest()
requestQuery) };
if (!requestHeaders.contains("Content-Type"))
req.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
- req.setRawHeader("Authorization",
- QByteArray("Bearer ") + connection->accessToken());
+ if (needsToken)
+ req.setRawHeader("Authorization",
+ QByteArray("Bearer ") + connection->accessToken());
req.setAttribute(QNetworkRequest::BackgroundRequestAttribute, inBackground);
req.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true);
req.setMaximumRedirectsAllowed(10);
@@ -258,14 +258,27 @@ void BaseJob::onSentRequest(QNetworkReply*) {}
void BaseJob::beforeAbandon(QNetworkReply*) {}
-void BaseJob::prepare(ConnectionData* connData, bool inBackground)
+void BaseJob::initiate(ConnectionData* connData, bool inBackground)
{
+ Q_ASSERT(connData != nullptr);
+
d->inBackground = inBackground;
d->connection = connData;
doPrepare();
- if (status().code != Unprepared && status().code != Pending)
+
+ if ((d->verb == HttpVerb::Post || d->verb == HttpVerb::Put)
+ && d->requestData.source() && !d->requestData.source()->isReadable()) {
+ setStatus(FileError, "Request data not ready");
+ }
+ Q_ASSERT(status().code != Pending); // doPrepare() must NOT set this
+ if (status().code == Unprepared) {
+ d->connection->submit(this);
+ } else {
+ qDebug(d->logCat).noquote()
+ << "Request failed preparation and won't be sent:"
+ << d->dumpRequest();
QTimer::singleShot(0, this, &BaseJob::finishJob);
- setStatus(Pending);
+ }
}
void BaseJob::sendRequest()
@@ -274,6 +287,7 @@ void BaseJob::sendRequest()
return;
Q_ASSERT(d->connection && status().code == Pending);
qCDebug(d->logCat).noquote() << "Making" << d->dumpRequest();
+ d->needsToken |= d->connection->needsToken(objectName());
emit aboutToSendRequest();
d->sendRequest();
Q_ASSERT(d->reply);
@@ -290,7 +304,7 @@ void BaseJob::sendRequest()
onSentRequest(d->reply.data());
emit sentRequest();
} else
- qCWarning(d->logCat).noquote()
+ qCCritical(d->logCat).noquote()
<< "Request could not start:" << d->dumpRequest();
}
@@ -341,32 +355,32 @@ bool checkContentType(const QByteArray& type, const QByteArrayList& patterns)
return false;
}
-BaseJob::Status BaseJob::Status::fromHttpCode(int httpCode, QString msg)
+BaseJob::StatusCode BaseJob::Status::fromHttpCode(int httpCode)
{
+ if (httpCode / 10 == 41) // 41x errors
+ return httpCode == 410 ? IncorrectRequestError : NotFoundError;
+ switch (httpCode) {
+ case 401:
+ return Unauthorised;
+ // clang-format off
+ case 403: case 407: // clang-format on
+ return ContentAccessError;
+ case 404:
+ return NotFoundError;
// clang-format off
- return { [httpCode]() -> StatusCode {
- if (httpCode / 10 == 41) // 41x errors
- return httpCode == 410 ? IncorrectRequestError : NotFoundError;
- switch (httpCode) {
- case 401: case 403: case 407:
- return ContentAccessError;
- case 404:
- return NotFoundError;
- case 400: case 405: case 406: case 426: case 428: case 505:
- case 494: // Unofficial nginx "Request header too large"
- case 497: // Unofficial nginx "HTTP request sent to HTTPS port"
- return IncorrectRequestError;
- case 429:
- return TooManyRequestsError;
- case 501: case 510:
- return RequestNotImplementedError;
- case 511:
- return NetworkAuthRequiredError;
- default:
- return NetworkError;
- }
- }(), std::move(msg) };
- // clang-format on
+ case 400: case 405: case 406: case 426: case 428: case 505: // clang-format on
+ case 494: // Unofficial nginx "Request header too large"
+ case 497: // Unofficial nginx "HTTP request sent to HTTPS port"
+ return IncorrectRequestError;
+ case 429:
+ return TooManyRequestsError;
+ case 501: case 510:
+ return RequestNotImplementedError;
+ case 511:
+ return NetworkAuthRequiredError;
+ default:
+ return NetworkError;
+ }
}
QDebug BaseJob::Status::dumpToLog(QDebug dbg) const
@@ -492,10 +506,18 @@ void BaseJob::finishJob()
stop();
if (error() == TooManyRequests) {
emit rateLimited();
- setStatus(Pending);
d->connection->submit(this);
return;
}
+ if (error() == Unauthorised && !d->needsToken
+ && !d->connection->accessToken().isEmpty()) {
+ // Rerun with access token (extension of the spec while
+ // https://github.com/matrix-org/matrix-doc/issues/701 is pending)
+ d->connection->setNeedsToken(objectName());
+ qCWarning(d->logCat) << this << "re-running with authentication";
+ emit retryScheduled(d->retriesTaken, 0);
+ d->connection->submit(this);
+ }
if ((error() == NetworkError || error() == Timeout)
&& d->retriesTaken < d->maxRetries) {
// TODO: The whole retrying thing should be put to Connection(Manager)
@@ -596,6 +618,8 @@ QString BaseJob::statusCaption() const
return tr("Network problems");
case TimeoutError:
return tr("Request timed out");
+ case Unauthorised:
+ return tr("Unauthorised request");
case ContentAccessError:
return tr("Access error");
case NotFoundError:
diff --git a/lib/jobs/basejob.h b/lib/jobs/basejob.h
index 6c1b802c..c8046e9e 100644
--- a/lib/jobs/basejob.h
+++ b/lib/jobs/basejob.h
@@ -57,9 +57,10 @@ public:
Unprepared = 25, //< Initial job state is incomplete, hence warning level
Abandoned = 50, //< A tiny period between abandoning and object deletion
ErrorLevel = 100, //< Errors have codes starting from this
- NetworkError = 100,
+ NetworkError = 101,
Timeout,
TimeoutError = Timeout,
+ Unauthorised,
ContentAccessError,
NotFoundError,
IncorrectRequest,
@@ -81,6 +82,7 @@ public:
UserConsentRequiredError = UserConsentRequired,
CannotLeaveRoom,
UserDeactivated,
+ FileError,
UserDefinedError = 256
};
Q_ENUM(StatusCode)
@@ -113,7 +115,12 @@ public:
struct Status {
Status(StatusCode c) : code(c) {}
Status(int c, QString m) : code(c), message(std::move(m)) {}
- static Status fromHttpCode(int httpCode, QString msg = {});
+
+ static StatusCode fromHttpCode(int httpCode);
+ static Status fromHttpCode(int httpCode, QString msg)
+ {
+ return { fromHttpCode(httpCode), std::move(msg) };
+ }
bool good() const { return code < ErrorLevel; }
QDebug dumpToLog(QDebug dbg) const;
@@ -184,11 +191,11 @@ public:
using duration_ms_t = std::chrono::milliseconds::rep; // normally int64_t
std::chrono::seconds getCurrentTimeout() const;
- Q_INVOKABLE duration_ms_t getCurrentTimeoutMs() const;
+ Q_INVOKABLE Quotient::BaseJob::duration_ms_t getCurrentTimeoutMs() const;
std::chrono::seconds getNextRetryInterval() const;
- Q_INVOKABLE duration_ms_t getNextRetryMs() const;
+ Q_INVOKABLE Quotient::BaseJob::duration_ms_t getNextRetryMs() const;
std::chrono::milliseconds timeToRetry() const;
- Q_INVOKABLE duration_ms_t millisToRetry() const;
+ Q_INVOKABLE Quotient::BaseJob::duration_ms_t millisToRetry() const;
friend QDebug operator<<(QDebug dbg, const BaseJob* j)
{
@@ -196,7 +203,7 @@ public:
}
public slots:
- void prepare(ConnectionData* connData, bool inBackground);
+ void initiate(ConnectionData* connData, bool inBackground);
/**
* Abandons the result of this job, arrived or unarrived.
@@ -215,7 +222,7 @@ signals:
void sentRequest();
/** The job has changed its status */
- void statusChanged(Status newStatus);
+ void statusChanged(Quotient::BaseJob::Status newStatus);
/**
* The previous network request has failed; the next attempt will
@@ -225,7 +232,8 @@ signals:
* @param inMilliseconds the interval after which the next attempt will be
* taken
*/
- void retryScheduled(int nextAttempt, duration_ms_t inMilliseconds);
+ void retryScheduled(int nextAttempt,
+ Quotient::BaseJob::duration_ms_t inMilliseconds);
/**
* The previous network request has been rate-limited; the next attempt
@@ -251,7 +259,7 @@ signals:
*
* @see result, success, failure
*/
- void finished(BaseJob* job);
+ void finished(Quotient::BaseJob* job);
/**
* Emitted when the job is finished (except when abandoned).
@@ -262,14 +270,14 @@ signals:
*
* @see success, failure
*/
- void result(BaseJob* job);
+ void result(Quotient::BaseJob* job);
/**
* Emitted together with result() in case there's no error.
*
* @see result, failure
*/
- void success(BaseJob*);
+ void success(Quotient::BaseJob*);
/**
* Emitted together with result() if there's an error.
@@ -277,7 +285,7 @@ signals:
*
* @see result, success
*/
- void failure(BaseJob*);
+ void failure(Quotient::BaseJob*);
void downloadProgress(qint64 bytesReceived, qint64 bytesTotal);
void uploadProgress(qint64 bytesSent, qint64 bytesTotal);
diff --git a/lib/jobs/downloadfilejob.h b/lib/jobs/downloadfilejob.h
index b7d2d75b..06dc145c 100644
--- a/lib/jobs/downloadfilejob.h
+++ b/lib/jobs/downloadfilejob.h
@@ -5,8 +5,6 @@
namespace Quotient {
class DownloadFileJob : public GetContentJob {
public:
- enum { FileError = BaseJob::UserDefinedError + 1 };
-
using GetContentJob::makeRequestUrl;
static QUrl makeRequestUrl(QUrl baseUrl, const QUrl& mxcUri);
diff --git a/lib/jobs/requestdata.cpp b/lib/jobs/requestdata.cpp
index 0c70f085..cec15954 100644
--- a/lib/jobs/requestdata.cpp
+++ b/lib/jobs/requestdata.cpp
@@ -11,9 +11,8 @@ using namespace Quotient;
auto fromData(const QByteArray& data)
{
auto source = std::make_unique<QBuffer>();
- source->open(QIODevice::WriteOnly);
- source->write(data);
- source->close();
+ source->setData(data);
+ source->open(QIODevice::ReadOnly);
return source;
}
diff --git a/lib/room.cpp b/lib/room.cpp
index c4cdac04..696a5f1b 100644
--- a/lib/room.cpp
+++ b/lib/room.cpp
@@ -387,11 +387,31 @@ QString Room::predecessorId() const
return d->getCurrentState<RoomCreateEvent>()->predecessor().roomId;
}
+Room* Room::predecessor(JoinStates statesFilter) const
+{
+ if (const auto& predId = predecessorId(); !predId.isEmpty())
+ if (auto* r = connection()->room(predId, statesFilter);
+ r && r->successorId() == id())
+ return r;
+
+ return nullptr;
+}
+
QString Room::successorId() const
{
return d->getCurrentState<RoomTombstoneEvent>()->successorRoomId();
}
+Room* Room::successor(JoinStates statesFilter) const
+{
+ if (const auto& succId = successorId(); !succId.isEmpty())
+ if (auto* r = connection()->room(succId, statesFilter);
+ r && r->predecessorId() == id())
+ return r;
+
+ return nullptr;
+}
+
const Room::Timeline& Room::messageEvents() const { return d->timeline; }
const Room::PendingEvents& Room::pendingEvents() const
@@ -515,7 +535,9 @@ void Room::Private::updateUnreadCount(rev_iter_t from, rev_iter_t to)
// that has just arrived. In this case we should recalculate
// unreadMessages and might need to promote the read marker further
// over local-origin messages.
- const auto readMarker = q->readMarker();
+ auto readMarker = q->readMarker();
+ if (readMarker == timeline.crend() && q->allHistoryLoaded())
+ --readMarker; // Read marker not found in the timeline, initialise it
if (readMarker >= from && readMarker < to) {
promoteReadMarker(q->localUser(), readMarker, true);
return;
@@ -926,12 +948,27 @@ void Room::removeTag(const QString& name)
<< "not found, nothing to remove";
}
-void Room::setTags(TagsMap newTags)
+void Room::setTags(TagsMap newTags, ActionScope applyOn)
{
+ bool propagate = applyOn != ActionScope::ThisRoomOnly;
+ auto joinStates =
+ applyOn == ActionScope::WithinSameState ? joinState() :
+ applyOn == ActionScope::OmitLeftState ? JoinState::Join|JoinState::Invite :
+ JoinState::Join|JoinState::Invite|JoinState::Leave;
+ if (propagate) {
+ for (auto* r = this; (r = r->predecessor(joinStates));)
+ r->setTags(newTags, ActionScope::ThisRoomOnly);
+ }
+
d->setTags(move(newTags));
connection()->callApi<SetAccountDataPerRoomJob>(
localUser()->id(), id(), TagEvent::matrixTypeId(),
TagEvent(d->tags).contentJson());
+
+ if (propagate) {
+ for (auto* r = this; (r = r->successor(joinStates));)
+ r->setTags(newTags, ActionScope::ThisRoomOnly);
+ }
}
void Room::Private::setTags(TagsMap newTags)
@@ -968,6 +1005,11 @@ QList<User*> Room::directChatUsers() const
return connection()->directChatUsers(this);
}
+QString safeFileName(QString rawName)
+{
+ return rawName.replace(QRegularExpression("[/\\<>|\"*?:]"), "_");
+}
+
const RoomMessageEvent*
Room::Private::getEventWithFile(const QString& eventId) const
{
@@ -983,24 +1025,24 @@ Room::Private::getEventWithFile(const QString& eventId) const
QString Room::Private::fileNameToDownload(const RoomMessageEvent* event) const
{
- Q_ASSERT(event->hasFileContent());
+ Q_ASSERT(event && event->hasFileContent());
const auto* fileInfo = event->content()->fileInfo();
QString fileName;
if (!fileInfo->originalName.isEmpty())
- fileName = QFileInfo(fileInfo->originalName).fileName();
- else if (!event->plainBody().isEmpty()) {
- // Having no better options, assume that the body has
- // the original file URL or at least the file name.
- if (QUrl u { event->plainBody() }; u.isValid())
- fileName = QFileInfo(u.path()).fileName();
+ fileName = QFileInfo(safeFileName(fileInfo->originalName)).fileName();
+ else if (QUrl u { event->plainBody() }; u.isValid()) {
+ qDebug(MAIN) << event->id()
+ << "has no file name supplied but the event body "
+ "looks like a URL - using the file name from it";
+ fileName = u.fileName();
}
- // Check the file name for sanity
- if (fileName.isEmpty() || !QTemporaryFile(fileName).open())
- return "file." % fileInfo->mimeType.preferredSuffix();
+ if (fileName.isEmpty())
+ return safeFileName(fileInfo->mediaId()).replace('.', '-') % '.'
+ % fileInfo->mimeType.preferredSuffix();
if (QSysInfo::productType() == "windows") {
if (const auto& suffixes = fileInfo->mimeType.suffixes();
- suffixes.isEmpty()
+ !suffixes.isEmpty()
&& std::none_of(suffixes.begin(), suffixes.end(),
[&fileName](const QString& s) {
return fileName.endsWith(s);
@@ -1201,16 +1243,14 @@ QString Room::decryptMessage(QByteArray cipher, const QString& senderKey,
int Room::joinedCount() const
{
- return d->summary.joinedMemberCount.omitted()
- ? d->membersMap.size()
- : d->summary.joinedMemberCount.value();
+ return d->summary.joinedMemberCount.value_or(d->membersMap.size());
}
int Room::invitedCount() const
{
// TODO: Store invited users in Room too
- Q_ASSERT(!d->summary.invitedMemberCount.omitted());
- return d->summary.invitedMemberCount.value();
+ Q_ASSERT(d->summary.invitedMemberCount.has_value());
+ return d->summary.invitedMemberCount.value_or(0);
}
int Room::totalMemberCount() const { return joinedCount() + invitedCount(); }
@@ -1593,19 +1633,16 @@ QString Room::postFile(const QString& plainText, const QUrl& localPath,
QFileInfo localFile { localPath.toLocalFile() };
Q_ASSERT(localFile.isFile());
- const auto txnId = connection()->generateTxnId();
+ const auto txnId =
+ d->addAsPending(
+ makeEvent<RoomMessageEvent>(plainText, localFile, asGenericFile))
+ ->transactionId();
// Remote URL will only be known after upload; fill in the local path
// to enable the preview while the event is pending.
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) {
+ // Below, the upload job is used as a context object to clean up connections
+ connect(this, &Room::fileTransferCompleted, d->fileTransfers[txnId].job,
+ [this, txnId](const QString& id, QUrl, const QUrl& mxcUri) {
if (id == txnId) {
auto it = findPendingEvent(txnId);
if (it != d->unsyncedEvents.end()) {
@@ -1621,11 +1658,10 @@ QString Room::postFile(const QString& plainText, const QUrl& localPath,
<< "but the event referring to it was "
"cancelled";
}
- context->deleteLater();
}
});
- connect(this, &Room::fileTransferCancelled, this,
- [context, this, txnId](const QString& id) {
+ connect(this, &Room::fileTransferCancelled, d->fileTransfers[txnId].job,
+ [this, txnId](const QString& id) {
if (id == txnId) {
auto it = findPendingEvent(txnId);
if (it != d->unsyncedEvents.end()) {
@@ -1635,7 +1671,6 @@ QString Room::postFile(const QString& plainText, const QUrl& localPath,
d->unsyncedEvents.erase(d->unsyncedEvents.begin() + idx);
emit pendingEventDiscarded();
}
- context->deleteLater();
}
});
@@ -1814,7 +1849,7 @@ void Room::uploadFile(const QString& id, const QUrl& localFilename,
auto fileName = localFilename.toLocalFile();
auto job = connection()->uploadFile(fileName, overrideContentType);
if (isJobRunning(job)) {
- d->fileTransfers.insert(id, { job, fileName, true });
+ d->fileTransfers[id] = { job, fileName, true };
connect(job, &BaseJob::uploadProgress, this,
[this, id](qint64 sent, qint64 total) {
d->fileTransfers[id].update(sent, total);
@@ -1858,18 +1893,20 @@ void Room::downloadFile(const QString& eventId, const QUrl& localFilename)
}
const auto fileUrl = fileInfo->url;
auto filePath = localFilename.toLocalFile();
- if (filePath.isEmpty()) {
- // Build our own file path, starting with temp directory and eventId.
- filePath = eventId;
- filePath = QDir::tempPath() % '/'
- % filePath.replace(QRegularExpression("[/\\<>|\"*?:]"), "_")
- % '#' % d->fileNameToDownload(event);
+ if (filePath.isEmpty()) { // Setup default file path
+ filePath =
+ fileInfo->url.path().mid(1) % '_' % d->fileNameToDownload(event);
+
+ if (filePath.size() > 200) // If too long, elide in the middle
+ filePath.replace(128, filePath.size() - 192, "---");
+
+ filePath = QDir::tempPath() % '/' % filePath;
+ qDebug(MAIN) << "File path:" << filePath;
}
auto job = connection()->downloadFile(fileUrl, filePath);
if (isJobRunning(job)) {
- // If there was a previous transfer (completed or failed), remove it.
- d->fileTransfers.remove(eventId);
- d->fileTransfers.insert(eventId, { job, job->targetFileName() });
+ // If there was a previous transfer (completed or failed), overwrite it.
+ d->fileTransfers[eventId] = { job, job->targetFileName() };
connect(job, &BaseJob::downloadProgress, this,
[this, eventId](qint64 received, qint64 total) {
d->fileTransfers[eventId].update(received, total);
@@ -1936,22 +1973,15 @@ RoomEventPtr makeRedacted(const RoomEvent& target,
const RedactionEvent& redaction)
{
auto originalJson = target.originalJsonObject();
- static const QStringList keepKeys { EventIdKey,
- TypeKey,
- QStringLiteral("room_id"),
- QStringLiteral("sender"),
- StateKeyKey,
- QStringLiteral("prev_content"),
- ContentKey,
- QStringLiteral("hashes"),
- QStringLiteral("signatures"),
- QStringLiteral("depth"),
- QStringLiteral("prev_events"),
- QStringLiteral("prev_state"),
- QStringLiteral("auth_events"),
- QStringLiteral("origin"),
- QStringLiteral("origin_server_ts"),
- QStringLiteral("membership") };
+ // clang-format off
+ static const QStringList keepKeys { EventIdKey, TypeKey,
+ QStringLiteral("room_id"), QStringLiteral("sender"), StateKeyKey,
+ QStringLiteral("hashes"), QStringLiteral("signatures"),
+ QStringLiteral("depth"), QStringLiteral("prev_events"),
+ QStringLiteral("prev_state"), QStringLiteral("auth_events"),
+ QStringLiteral("origin"), QStringLiteral("origin_server_ts"),
+ QStringLiteral("membership") };
+ // clang-format on
std::vector<std::pair<Event::Type, QStringList>> keepContentKeysMap {
{ RoomMemberEvent::typeId(), { QStringLiteral("membership") } },
@@ -2107,7 +2137,7 @@ inline bool isEditing(const RoomEventPtr& ep)
if (is<RedactionEvent>(*ep))
return true;
if (auto* msgEvent = eventCast<RoomMessageEvent>(ep))
- return msgEvent->replacedEvent().isEmpty();
+ return !msgEvent->replacedEvent().isEmpty();
return false;
}
@@ -2130,12 +2160,10 @@ Room::Changes Room::Private::addNewMessageEvents(RoomEvents&& events)
// Try to find the target in the timeline, then in the batch.
if (processRedaction(*r))
continue;
- auto targetIt = std::find_if(events.begin(), it,
- [id = r->redactedEvent()](
- const RoomEventPtr& ep) {
- return ep->id() == id;
- });
- if (targetIt != it)
+ if (auto targetIt = std::find_if(events.begin(), events.end(),
+ [id = r->redactedEvent()](const RoomEventPtr& ep) {
+ return ep->id() == id;
+ }); targetIt != events.end())
*targetIt = makeRedacted(**targetIt, *r);
else
qCDebug(EVENTS)
@@ -2143,25 +2171,23 @@ Room::Changes Room::Private::addNewMessageEvents(RoomEvents&& events)
<< r->redactedEvent() << "is not found";
// If the target event comes later, it comes already redacted.
}
- if (auto* msg = eventCast<RoomMessageEvent>(eptr)) {
- if (!msg->replacedEvent().isEmpty()) {
- if (processReplacement(*msg))
- continue;
- auto targetIt = std::find_if(events.begin(), it,
- [id = msg->replacedEvent()](
- const RoomEventPtr& ep) {
- return ep->id() == id;
- });
- if (targetIt != it)
- *targetIt = makeReplaced(**targetIt, *msg);
- else // FIXME: don't ignore, just show it wherever it arrived
- qCDebug(EVENTS)
- << "Replacing event" << msg->id()
- << "ignored: replaced event" << msg->replacedEvent()
- << "is not found";
- // Same as with redactions above, the replaced event coming
- // later will come already with the new content.
- }
+ if (auto* msg = eventCast<RoomMessageEvent>(eptr);
+ msg && !msg->replacedEvent().isEmpty()) {
+ if (processReplacement(*msg))
+ continue;
+ auto targetIt = std::find_if(events.begin(), it,
+ [id = msg->replacedEvent()](const RoomEventPtr& ep) {
+ return ep->id() == id;
+ });
+ if (targetIt != it)
+ *targetIt = makeReplaced(**targetIt, *msg);
+ else // FIXME: don't ignore, just show it wherever it arrived
+ qCDebug(EVENTS)
+ << "Replacing event" << msg->id()
+ << "ignored: replaced event" << msg->replacedEvent()
+ << "is not found";
+ // Same as with redactions above, the replaced event coming
+ // later will come already with the new content.
}
}
}
@@ -2326,7 +2352,7 @@ Room::Changes Room::processStateEvent(const RoomEvent& e)
|| (oldStateEvent->matrixType() == e.matrixType()
&& oldStateEvent->stateKey() == e.stateKey()));
if (!is<RoomMemberEvent>(e)) // Room member events are too numerous
- qCDebug(EVENTS) << "Room state event:" << e;
+ qCDebug(STATE) << "Room state event:" << e;
// clang-format off
return visit(e
@@ -2336,10 +2362,10 @@ Room::Changes Room::processStateEvent(const RoomEvent& e)
, [this,oldStateEvent] (const RoomAliasesEvent& ae) {
// clang-format on
if (ae.aliases().isEmpty()) {
- qCDebug(STATE).noquote()
- << ae.stateKey() << "no more has aliases for room"
- << objectName();
- d->aliasServers.remove(ae.stateKey());
+ if (d->aliasServers.remove(ae.stateKey()))
+ qCDebug(STATE).noquote()
+ << ae.stateKey() << "no more has aliases for room"
+ << objectName();
} else {
d->aliasServers.insert(ae.stateKey());
qCDebug(STATE).nospace().noquote()
@@ -2620,9 +2646,8 @@ QString Room::Private::calculateDisplayname() const
const bool emptyRoom =
membersMap.isEmpty()
|| (membersMap.size() == 1 && isLocalUser(*membersMap.begin()));
- const bool nonEmptySummary =
- !summary.heroes.omitted() && !summary.heroes->empty();
- auto shortlist = nonEmptySummary ? buildShortlist(summary.heroes.value())
+ const bool nonEmptySummary = summary.heroes && !summary.heroes->empty();
+ auto shortlist = nonEmptySummary ? buildShortlist(*summary.heroes)
: !emptyRoom ? buildShortlist(membersMap)
: users_shortlist_t {};
diff --git a/lib/room.h b/lib/room.h
index cded7eb9..ad19792e 100644
--- a/lib/room.h
+++ b/lib/room.h
@@ -27,6 +27,8 @@
#include "events/accountdataevents.h"
#include "events/encryptedevent.h"
#include "events/roommessageevent.h"
+#include "events/roomcreateevent.h"
+#include "events/roomtombstoneevent.h"
#include <QtCore/QJsonObject>
#include <QtGui/QImage>
@@ -167,7 +169,22 @@ public:
QString version() const;
bool isUnstable() const;
QString predecessorId() const;
+ /// Room predecessor
+ /** This function validates that the predecessor has a tombstone and
+ * the tombstone refers to the current room. If that's not the case,
+ * or if the predecessor is in a join state not matching \p stateFilter,
+ * the function returns nullptr.
+ */
+ Room* predecessor(JoinStates statesFilter = JoinState::Invite
+ | JoinState::Join) const;
QString successorId() const;
+ /// Room successor
+ /** This function validates that the successor room's creation event
+ * refers to the current room. If that's not the case, or if the successor
+ * is in a join state not matching \p stateFilter, it returns nullptr.
+ */
+ Room* successor(JoinStates statesFilter = JoinState::Invite
+ | JoinState::Join) const;
QString name() const;
/// Room aliases defined on the current user's server
/// \sa remoteAliases, setLocalAliases
@@ -182,10 +199,10 @@ public:
QUrl avatarUrl() const;
const Avatar& avatarObject() const;
Q_INVOKABLE JoinState joinState() const;
- Q_INVOKABLE QList<User*> usersTyping() const;
+ Q_INVOKABLE QList<Quotient::User*> usersTyping() const;
QList<User*> membersLeft() const;
- Q_INVOKABLE QList<User*> users() const;
+ Q_INVOKABLE QList<Quotient::User*> users() const;
QStringList memberNames() const;
[[deprecated("Use joinedCount(), invitedCount(), totalMemberCount()")]]
int memberCount() const;
@@ -228,7 +245,7 @@ public:
* \note The method will return a valid user regardless of
* the membership.
*/
- Q_INVOKABLE User* user(const QString& userId) const;
+ Q_INVOKABLE Quotient::User* user(const QString& userId) const;
/**
* \brief Check the join state of a given user in this room
@@ -236,16 +253,15 @@ public:
* \note Banned and invited users are not tracked for now (Leave
* will be returned for them).
*
- * \return either of Join, Leave, depending on the given
- * user's state in the room
+ * \return Join if the user is a room member; Leave otherwise
*/
- Q_INVOKABLE JoinState memberJoinState(User* user) const;
+ Q_INVOKABLE Quotient::JoinState memberJoinState(Quotient::User* user) const;
/**
* Get a disambiguated name for a given user in
* the context of the room
*/
- Q_INVOKABLE QString roomMembername(const User* u) const;
+ Q_INVOKABLE QString roomMembername(const Quotient::User* u) const;
/**
* Get a disambiguated name for a user with this id in
* the context of the room
@@ -274,9 +290,10 @@ public:
Timeline::const_iterator syncEdge() const;
/// \deprecated Use historyEdge instead
rev_iter_t timelineEdge() const;
- Q_INVOKABLE TimelineItem::index_t minTimelineIndex() const;
- Q_INVOKABLE TimelineItem::index_t maxTimelineIndex() const;
- Q_INVOKABLE bool isValidIndex(TimelineItem::index_t timelineIndex) const;
+ Q_INVOKABLE Quotient::TimelineItem::index_t minTimelineIndex() const;
+ Q_INVOKABLE Quotient::TimelineItem::index_t maxTimelineIndex() const;
+ Q_INVOKABLE bool
+ isValidIndex(Quotient::TimelineItem::index_t timelineIndex) const;
rev_iter_t findInTimeline(TimelineItem::index_t index) const;
rev_iter_t findInTimeline(const QString& evtId) const;
@@ -288,6 +305,11 @@ public:
const RelatedEvents relatedEvents(const RoomEvent& evt,
const char* relType) const;
+ const RoomCreateEvent* creation() const
+ { return getCurrentState<RoomCreateEvent>(); }
+ const RoomTombstoneEvent* tombstone() const
+ { return getCurrentState<RoomTombstoneEvent>(); }
+
bool displayed() const;
/// Mark the room as currently displayed to the user
/**
@@ -374,6 +396,19 @@ public:
/// Remove a tag from the room
Q_INVOKABLE void removeTag(const QString& name);
+ /// The scope to apply an action on
+ /*! This enumeration is used to pick a strategy to propagate certain
+ * actions on the room to its predecessors and successors.
+ */
+ enum ActionScope {
+ ThisRoomOnly, //< Do not apply to predecessors and successors
+ WithinSameState, //< Apply to predecessors and successors in the same
+ //< state as the current one
+ OmitLeftState, //< Apply to all reachable predecessors and successors
+ //< except those in Leave state
+ WholeSequence //< Apply to all reachable predecessors and successors
+ };
+
/** Overwrite the room's tags
* This completely replaces the existing room's tags with a set
* of new ones and updates the new set on the server. Unlike
@@ -381,8 +416,11 @@ public:
* immediately, not waiting for confirmation from the server
* (because tags are saved in account data rather than in shared
* room state).
+ * \param applyOn setting this to Room::OnAllConversations will set tags
+ * on this and all _known_ predecessors and successors;
+ * by default only the current room is changed
*/
- void setTags(TagsMap newTags);
+ void setTags(TagsMap newTags, ActionScope applyOn = ThisRoomOnly);
/// Check whether the list of tags has m.favourite
bool isFavourite() const;
@@ -414,7 +452,8 @@ public:
* the event is even sent), while downloads are using
* the normal event id for identifier.
*/
- Q_INVOKABLE FileTransferInfo fileTransferInfo(const QString& id) const;
+ Q_INVOKABLE Quotient::FileTransferInfo
+ fileTransferInfo(const QString& id) const;
/// Get the URL to the actual file source in a unified way
/*!
@@ -438,9 +477,13 @@ public:
/*! This method returns a (potentially empty) state event corresponding
* to the pair of event type \p evtType and state key \p stateKey.
*/
- Q_INVOKABLE const StateEventBase*
+ Q_INVOKABLE const Quotient::StateEventBase*
getCurrentState(const QString& evtType, const QString& stateKey = {}) const;
+ /// Get a state event with the given event type and state key
+ /*! This is a typesafe overload that accepts a C++ event type instead of
+ * its Matrix name.
+ */
template <typename EvT>
const EvT* getCurrentState(const QString& stateKey = {}) const
{
@@ -452,6 +495,14 @@ public:
return evt;
}
+ /// Set a state event of the given type with the given arguments
+ /*! This typesafe overload attempts to send a state event with the type
+ * \p EvT and the content defined by \p args. Specifically, the function
+ * creates a temporary object of type \p EvT passing \p args to
+ * the constructor, and sends a request to the homeserver using
+ * the Matrix event type defined by \p EvT and the event content produced
+ * via EvT::contentJson().
+ */
template <typename EvT, typename... ArgTs>
auto setState(ArgTs&&... args) const
{
@@ -549,7 +600,8 @@ signals:
/// The remote echo has arrived with the sync and will be merged
/// with its local counterpart
/** NB: Requires a sync loop to be emitted */
- void pendingEventAboutToMerge(RoomEvent* serverEvent, int pendingEventIndex);
+ void pendingEventAboutToMerge(Quotient::RoomEvent* serverEvent,
+ int pendingEventIndex);
/// The remote and local copies of the event have been merged
/** NB: Requires a sync loop to be emitted */
void pendingEventMerged();
@@ -577,21 +629,21 @@ signals:
* upon the last sync
* \sa Changes
*/
- void changed(Changes changes);
+ void changed(Quotient::Room::Changes changes);
/**
* \brief The room name, the canonical alias or other aliases changed
*
* Not triggered when display name changes.
*/
- void namesChanged(Room* room);
- void displaynameAboutToChange(Room* room);
- void displaynameChanged(Room* room, QString oldName);
+ void namesChanged(Quotient::Room* room);
+ void displaynameAboutToChange(Quotient::Room* room);
+ void displaynameChanged(Quotient::Room* room, QString oldName);
void topicChanged();
void avatarChanged();
- void userAdded(User* user);
- void userRemoved(User* user);
- void memberAboutToRename(User* user, QString newName);
- void memberRenamed(User* user);
+ void userAdded(Quotient::User* user);
+ void userRemoved(Quotient::User* user);
+ void memberAboutToRename(Quotient::User* user, QString newName);
+ void memberRenamed(Quotient::User* user);
/// The list of members has changed
/** Emitted no more than once per sync, this is a good signal to
* for cases when some action should be done upon any change in
@@ -605,7 +657,8 @@ signals:
void allMembersLoaded();
void encryption();
- void joinStateChanged(JoinState oldState, JoinState newState);
+ void joinStateChanged(Quotient::JoinState oldState,
+ Quotient::JoinState newState);
void typingChanged();
void highlightCountChanged();
@@ -614,11 +667,11 @@ signals:
void displayedChanged(bool displayed);
void firstDisplayedEventChanged();
void lastDisplayedEventChanged();
- void lastReadEventChanged(User* user);
+ void lastReadEventChanged(Quotient::User* user);
void readMarkerMoved(QString fromEventId, QString toEventId);
- void readMarkerForUserMoved(User* user, QString fromEventId,
+ void readMarkerForUserMoved(Quotient::User* user, QString fromEventId,
QString toEventId);
- void unreadMessagesChanged(Room* room);
+ void unreadMessagesChanged(Quotient::Room* room);
void accountDataAboutToChange(QString type);
void accountDataChanged(QString type);
@@ -626,7 +679,8 @@ signals:
void tagsChanged();
void updatedEvent(QString eventId);
- void replacedEvent(const RoomEvent* newEvent, const RoomEvent* oldEvent);
+ void replacedEvent(const Quotient::RoomEvent* newEvent,
+ const Quotient::RoomEvent* oldEvent);
void newFileTransfer(QString id, QUrl localFile);
void fileTransferProgress(QString id, qint64 progress, qint64 total);
@@ -634,18 +688,18 @@ signals:
void fileTransferFailed(QString id, QString errorMessage = {});
void fileTransferCancelled(QString id);
- void callEvent(Room* room, const RoomEvent* event);
+ void callEvent(Quotient::Room* room, const Quotient::RoomEvent* event);
/// The room's version stability may have changed
void stabilityUpdated(QString recommendedDefault,
QStringList stableVersions);
/// This room has been upgraded and won't receive updates any more
- void upgraded(QString serverMessage, Room* successor);
+ void upgraded(QString serverMessage, Quotient::Room* successor);
/// An attempted room upgrade has failed
void upgradeFailed(QString errorMessage);
/// The room is about to be deleted
- void beforeDestruction(Room*);
+ void beforeDestruction(Quotient::Room*);
protected:
virtual Changes processStateEvent(const RoomEvent& e);
diff --git a/lib/syncdata.cpp b/lib/syncdata.cpp
index 5b47b30f..89c512a2 100644
--- a/lib/syncdata.cpp
+++ b/lib/syncdata.cpp
@@ -30,8 +30,7 @@ const QString SyncRoomData::UnreadCountKey =
bool RoomSummary::isEmpty() const
{
- return joinedMemberCount.omitted() && invitedMemberCount.omitted()
- && heroes.omitted();
+ return !joinedMemberCount && !invitedMemberCount && !heroes;
}
bool RoomSummary::merge(const RoomSummary& other)
@@ -46,12 +45,12 @@ QDebug Quotient::operator<<(QDebug dbg, const RoomSummary& rs)
{
QDebugStateSaver _(dbg);
QStringList sl;
- if (!rs.joinedMemberCount.omitted())
- sl << QStringLiteral("joined: %1").arg(rs.joinedMemberCount.value());
- if (!rs.invitedMemberCount.omitted())
- sl << QStringLiteral("invited: %1").arg(rs.invitedMemberCount.value());
- if (!rs.heroes.omitted())
- sl << QStringLiteral("heroes: [%1]").arg(rs.heroes.value().join(','));
+ if (rs.joinedMemberCount)
+ sl << QStringLiteral("joined: %1").arg(*rs.joinedMemberCount);
+ if (rs.invitedMemberCount)
+ sl << QStringLiteral("invited: %1").arg(*rs.invitedMemberCount);
+ if (rs.heroes)
+ sl << QStringLiteral("heroes: [%1]").arg(rs.heroes->join(','));
dbg.nospace().noquote() << sl.join(QStringLiteral("; "));
return dbg;
}
diff --git a/lib/user.h b/lib/user.h
index c9e3dbc1..28ec841b 100644
--- a/lib/user.h
+++ b/lib/user.h
@@ -107,9 +107,10 @@ public:
qreal hueF() const;
const Avatar& avatarObject(const Room* room = nullptr) const;
- Q_INVOKABLE QImage avatar(int dimension, const Room* room = nullptr);
+ Q_INVOKABLE QImage avatar(int dimension,
+ const Quotient::Room* room = nullptr);
Q_INVOKABLE QImage avatar(int requestedWidth, int requestedHeight,
- const Room* room = nullptr);
+ const Quotient::Room* room = nullptr);
QImage avatar(int width, int height, const Room* room,
const Avatar::get_callback_t& callback);
@@ -145,9 +146,10 @@ public slots:
signals:
void nameAboutToChange(QString newName, QString oldName,
- const Room* roomContext);
- void nameChanged(QString newName, QString oldName, const Room* roomContext);
- void avatarChanged(User* user, const Room* roomContext);
+ const Quotient::Room* roomContext);
+ void nameChanged(QString newName, QString oldName,
+ const Quotient::Room* roomContext);
+ void avatarChanged(Quotient::User* user, const Quotient::Room* roomContext);
private slots:
void updateName(const QString& newName, const Room* room = nullptr);
@@ -161,4 +163,3 @@ private:
QScopedPointer<Private> d;
};
} // namespace Quotient
-Q_DECLARE_METATYPE(Quotient::User*)
diff --git a/lib/util.cpp b/lib/util.cpp
index 041a8aba..9f4ac85f 100644
--- a/lib/util.cpp
+++ b/lib/util.cpp
@@ -56,7 +56,7 @@ void Quotient::linkifyUrls(QString& htmlEscapedText)
// https://matrix.org/docs/spec/appendices.html#identifier-grammar
static const QRegularExpression MxIdRegExp(
QStringLiteral(
- R"((^|[^<>/])([!#@][-a-z0-9_=/.]{1,252}:(?:\w|\.|-)+\.\w+(?::\d{1,5})?))"),
+ R"((^|[^<>/])([!#@][-a-z0-9_=#/.]{1,252}:(?:\w|\.|-)+\.\w+(?::\d{1,5})?))"),
RegExpOptions);
// NOTE: htmlEscapedText is already HTML-escaped! No literal <,>,&,"
diff --git a/lib/util.h b/lib/util.h
index f7a81b2a..902b4bfc 100644
--- a/lib/util.h
+++ b/lib/util.h
@@ -24,6 +24,7 @@
#include <functional>
#include <memory>
#include <unordered_map>
+#include <optional>
// Along the lines of Q_DISABLE_COPY - the upstream version comes in Qt 5.13
#define DISABLE_MOVE(_ClassName) \
@@ -43,68 +44,83 @@ struct HashQ {
template <typename KeyT, typename ValT>
using UnorderedMap = std::unordered_map<KeyT, ValT, HashQ<KeyT>>;
-struct NoneTag {};
-constexpr NoneTag none {};
+constexpr auto none = std::nullopt;
-/** A crude substitute for `optional` while we're not C++17
+/** `std::optional` with tweaks
*
- * Only works with default-constructible types.
+ * The tweaks are:
+ * - streamlined assignment (operator=)/emplace()ment of values that can be
+ * used to implicitly construct the underlying type, including
+ * direct-list-initialisation, e.g.:
+ * \code
+ * struct S { int a; char b; }
+ * Omittable<S> o;
+ * o = { 1, 'a' }; // std::optional would require o = S { 1, 'a' }
+ * \endcode
+ * - entirely deleted value(). The technical reason is that Xcode 10 doesn't
+ * have it; but besides that, value_or() or (after explicit checking)
+ * `operator*()`/`operator->()` are better alternatives within Quotient
+ * that doesn't practice throwing exceptions (as doesn't most of Qt).
+ * - disabled non-const lvalue operator*() and operator->(), as it's too easy
+ * to inadvertently cause a value change through them.
+ * - edit() to provide a safe and explicit lvalue accessor instead of those
+ * above. Requires the underlying type to be default-constructible.
+ * Allows chained initialisation of nested Omittables:
+ * \code
+ * struct Inner { int member = 10; Omittable<int> innermost; };
+ * struct Outer { int anotherMember = 10; Omittable<Inner> inner; };
+ * Omittable<Outer> o; // = { 10, std::nullopt };
+ * o.edit().inner.edit().innermost.emplace(42);
+ * \endcode
+ * - merge() - a soft version of operator= that only overwrites its first
+ * operand with the second one if the second one is not empty.
*/
template <typename T>
-class Omittable {
- static_assert(!std::is_reference<T>::value,
- "You cannot make an Omittable<> with a reference type");
-
+class Omittable : public std::optional<T> {
public:
+ using base_type = std::optional<T>;
using value_type = std::decay_t<T>;
- explicit Omittable() : Omittable(none) {}
- Omittable(NoneTag) : _value(value_type()), _omitted(true) {}
- Omittable(const value_type& val) : _value(val) {}
- Omittable(value_type&& val) : _value(std::move(val)) {}
- Omittable<T>& operator=(const value_type& val)
+ using std::optional<T>::optional;
+
+ // Overload emplace() and operator=() to allow passing braced-init-lists
+ // (the standard emplace() does direct-initialisation but
+ // not direct-list-initialisation).
+ using base_type::operator=;
+ Omittable& operator=(const value_type& v)
{
- _value = val;
- _omitted = false;
+ base_type::operator=(v);
return *this;
}
- Omittable<T>& operator=(value_type&& val)
+ Omittable& operator=(value_type&& v)
{
- // For some reason GCC complains about -Wmaybe-uninitialized
- // in the context of using Omittable<bool> with converters.h;
- // though the logic looks very much benign (GCC bug???)
- _value = std::move(val);
- _omitted = false;
+ base_type::operator=(v);
return *this;
}
-
- bool operator==(const value_type& rhs) const
- {
- return !omitted() && value() == rhs;
- }
- friend bool operator==(const value_type& lhs,
- const Omittable<value_type>& rhs)
- {
- return rhs == lhs;
- }
- bool operator!=(const value_type& rhs) const { return !operator==(rhs); }
- friend bool operator!=(const value_type& lhs,
- const Omittable<value_type>& rhs)
+ using base_type::emplace;
+ T& emplace(const T& val) { return base_type::emplace(val); }
+ T& emplace(T&& val) { return base_type::emplace(std::move(val)); }
+
+ // use value_or() or check (with operator! or has_value) before accessing
+ // with operator-> or operator*
+ // The technical reason is that Xcode 10 has incomplete std::optional
+ // that has no value(); but using value() may also mean that you rely
+ // on the optional throwing an exception (which is not assumed practice
+ // throughout Quotient) or that you spend unnecessary CPU cycles on
+ // an extraneous has_value() check.
+ value_type& value() = delete;
+ const value_type& value() const = delete;
+ value_type& edit()
{
- return !(rhs == lhs);
+ return this->has_value() ? base_type::operator*() : this->emplace();
}
- bool omitted() const { return _omitted; }
- const value_type& value() const
+ [[deprecated("Use '!o' or '!o.has_value()' instead of 'o.omitted()'")]]
+ bool omitted() const
{
- Q_ASSERT(!_omitted);
- return _value;
- }
- value_type& editValue()
- {
- _omitted = false;
- return _value;
+ return !this->has_value();
}
+
/// Merge the value from another Omittable
/// \return true if \p other is not omitted and the value of
/// the current Omittable was different (or omitted);
@@ -114,26 +130,20 @@ public:
auto merge(const Omittable<T1>& other)
-> std::enable_if_t<std::is_convertible<T1, T>::value, bool>
{
- if (other.omitted() || (!_omitted && _value == other.value()))
+ if (!other || (this->has_value() && **this == *other))
return false;
- _omitted = false;
- _value = other.value();
+ *this = other;
return true;
}
- value_type&& release()
- {
- _omitted = true;
- return std::move(_value);
- }
- const value_type* operator->() const& { return &value(); }
- value_type* operator->() & { return &editValue(); }
- const value_type& operator*() const& { return value(); }
- value_type& operator*() & { return editValue(); }
+ // Hide non-const lvalue operator-> and operator* as these are
+ // a bit too surprising: value() & doesn't lazy-create an object;
+ // and it's too easy to inadvertently change the underlying value.
-private:
- T _value;
- bool _omitted = false;
+ const value_type* operator->() const& { return base_type::operator->(); }
+ value_type* operator->() && { return base_type::operator->(); }
+ const value_type& operator*() const& { return base_type::operator*(); }
+ value_type& operator*() && { return base_type::operator*(); }
};
namespace _impl {
@@ -213,19 +223,19 @@ class Range {
using size_type = typename ArrayT::size_type;
public:
- Range(ArrayT& arr) : from(std::begin(arr)), to(std::end(arr)) {}
- Range(iterator from, iterator to) : from(from), to(to) {}
+ constexpr Range(ArrayT& arr) : from(std::begin(arr)), to(std::end(arr)) {}
+ constexpr Range(iterator from, iterator to) : from(from), to(to) {}
- size_type size() const
+ constexpr size_type size() const
{
Q_ASSERT(std::distance(from, to) >= 0);
return size_type(std::distance(from, to));
}
- bool empty() const { return from == to; }
- const_iterator begin() const { return from; }
- const_iterator end() const { return to; }
- iterator begin() { return from; }
- iterator end() { return to; }
+ constexpr bool empty() const { return from == to; }
+ constexpr const_iterator begin() const { return from; }
+ constexpr const_iterator end() const { return to; }
+ constexpr iterator begin() { return from; }
+ constexpr iterator end() { return to; }
private:
iterator from;
@@ -239,8 +249,8 @@ private:
*/
template <typename InputIt, typename ForwardIt, typename Pred>
inline std::pair<InputIt, ForwardIt> findFirstOf(InputIt first, InputIt last,
- ForwardIt sFirst,
- ForwardIt sLast, Pred pred)
+ ForwardIt sFirst,
+ ForwardIt sLast, Pred pred)
{
for (; first != last; ++first)
for (auto it = sFirst; it != sLast; ++it)