diff --git a/lib/avatar.cpp b/lib/avatar.cpp index 7e6dc50b7..be9b6a785 100644 --- a/lib/avatar.cpp +++ b/lib/avatar.cpp @@ -126,6 +126,14 @@ QImage Avatar::Private::get(Connection* connection, QSize size, if (callback) callbacks.emplace_back(move(callback)); _thumbnailRequest = connection->getThumbnail(_url, size); + if (_originalImage.isNull() && !_defaultIcon.isNull()) + { + _originalImage = QImage(_defaultIcon.actualSize(size), + QImage::Format_ARGB32_Premultiplied); + _originalImage.fill(Qt::transparent); + QPainter p { &_originalImage }; + _defaultIcon.paint(&p, { QPoint(), _defaultIcon.actualSize(size) }); + } QObject::connect( _thumbnailRequest, &MediaThumbnailJob::success, _thumbnailRequest, [this] { _fetched = true; @@ -138,15 +146,6 @@ QImage Avatar::Private::get(Connection* connection, QSize size, }); } - if( _originalImage.isNull() ) - { - if (_defaultIcon.isNull()) - return _originalImage; - - QPainter p { &_originalImage }; - _defaultIcon.paint(&p, { QPoint(), _defaultIcon.actualSize(size) }); - } - for (const auto& p: _scaledImages) if (p.first == size) return p.second; diff --git a/lib/connection.cpp b/lib/connection.cpp index 8d55460d7..cf3446fff 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -649,14 +649,14 @@ ForgetRoomJob* Connection::forgetRoom(const QString& id) forgetJob->start(connectionData()); connect(forgetJob, &BaseJob::success, this, [this, id] { - // If the room is in the map (possibly in both forms), delete all forms. + // Delete whatever instances of the room are still in the map. for (auto f: {false, true}) if (auto r = d->roomMap.take({ id, f })) { - emit aboutToDeleteRoom(r); - qCDebug(MAIN) << "Room" << id - << "in join state" << toCString(r->joinState()) + qCDebug(MAIN) << "Room" << r->objectName() + << "in state" << toCString(r->joinState()) << "will be deleted"; + emit r->beforeDestruction(r); r->deleteLater(); } }); @@ -995,6 +995,8 @@ Room* Connection::provideRoom(const QString& id, JoinState joinState) } d->roomMap.insert(roomKey, room); d->firstTimeRooms.push_back(room); + connect(room, &Room::beforeDestruction, + this, &Connection::aboutToDeleteRoom); emit newRoom(room); } if (joinState == JoinState::Invite) @@ -1015,7 +1017,7 @@ Room* Connection::provideRoom(const QString& id, JoinState joinState) if (prevInvite) { qCDebug(MAIN) << "Deleting Invite state for room" << prevInvite->id(); - emit aboutToDeleteRoom(prevInvite); + emit prevInvite->beforeDestruction(prevInvite); prevInvite->deleteLater(); } } diff --git a/lib/converters.h b/lib/converters.h index 1e8283939..7f78effe2 100644 --- a/lib/converters.h +++ b/lib/converters.h @@ -163,6 +163,11 @@ namespace QMatrixClient auto operator()(const QJsonValue& jv) const { return jv.toDouble(); } }; + template <> struct FromJson + { + auto operator()(const QJsonValue& jv) const { return float(jv.toDouble()); } + }; + template <> struct FromJson { auto operator()(const QJsonValue& jv) const { return qint64(jv.toDouble()); } diff --git a/lib/events/accountdataevents.h b/lib/events/accountdataevents.h index 94fc510aa..27f6c77c1 100644 --- a/lib/events/accountdataevents.h +++ b/lib/events/accountdataevents.h @@ -31,22 +31,40 @@ namespace QMatrixClient struct TagRecord { - TagRecord (QString order = {}) : order(std::move(order)) { } - explicit TagRecord(const QJsonValue& jv) - : order(jv.toObject().value("order"_ls).toString()) - { } + using order_type = Omittable; + + order_type order; - QString order; + TagRecord (order_type order = none) : order(order) { } + explicit TagRecord(const QJsonValue& jv) + { + // Parse a float both from JSON double and JSON string because + // libqmatrixclient previously used to use strings to store order. + const auto orderJv = jv.toObject().value("order"_ls); + if (orderJv.isDouble()) + order = fromJson(orderJv); + else if (orderJv.isString()) + { + bool ok; + order = orderJv.toString().toFloat(&ok); + if (!ok) + order = none; + } + } - bool operator==(const TagRecord& other) const - { return order == other.order; } - bool operator!=(const TagRecord& other) const - { return !operator==(other); } + 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()); + } }; inline QJsonValue toJson(const TagRecord& rec) { - return QJsonObject {{ QStringLiteral("order"), rec.order }}; + QJsonObject o; + addParam(o, QStringLiteral("order"), rec.order); + return o; } using TagsMap = QHash; diff --git a/lib/jobs/syncjob.cpp b/lib/jobs/syncjob.cpp index 02690e6df..9cbac71b9 100644 --- a/lib/jobs/syncjob.cpp +++ b/lib/jobs/syncjob.cpp @@ -130,10 +130,10 @@ SyncRoomData::SyncRoomData(const QString& roomId_, JoinState joinState_, switch (joinState) { case JoinState::Join: ephemeral = load(room_, "ephemeral"_ls); - accountData = load(room_, "account_data"_ls); FALLTHROUGH; case JoinState::Leave: { + accountData = load(room_, "account_data"_ls); timeline = load(room_, "timeline"_ls); const auto timelineJson = room_.value("timeline"_ls).toObject(); timelineLimited = timelineJson.value("limited"_ls).toBool(); diff --git a/lib/room.cpp b/lib/room.cpp index a22f477be..869a81c8e 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -234,14 +234,13 @@ class Room::Private */ bool processRedaction(const RedactionEvent& redaction); - std::pair setTags(TagsMap newTags); - void broadcastTagUpdates(const TagsMap& additions, - const QStringList& removals) + void setTags(TagsMap newTags); + void sendTagUpdates() { connection->callApi( connection->userId(), id, TagEvent::matrixTypeId(), TagEvent(tags).contentJson()); - emit q->tagsChanged(additions, removals); + emit q->tagsChanged(); } QJsonObject toJson() const; @@ -703,7 +702,7 @@ std::pair validatedTag(QString name) return { false, name }; qWarning(MAIN) << "The tag" << name - << "doesn't follow the CS API conventions, check your client code"; + << "doesn't follow the CS API conventions"; name.prepend("u."); qWarning(MAIN) << "Using " << name << "instead"; @@ -717,8 +716,10 @@ void Room::addTag(const QString& name, const TagRecord& record) (checkRes.first && d->tags.contains(checkRes.second))) return; + emit tagsAboutToChange(); d->tags.insert(checkRes.second, record); - d->broadcastTagUpdates({{ checkRes.second, record }}, {}); + emit tagsChanged(); + d->sendTagUpdates(); } void Room::addTag(const QString& name, const QString& order) @@ -728,43 +729,32 @@ void Room::addTag(const QString& name, const QString& order) void Room::removeTag(const QString& name) { - if (!d->tags.contains(name)) - return; - - d->tags.remove(name); - d->broadcastTagUpdates({}, {{ name }}); + if (d->tags.contains(name)) + { + emit tagsAboutToChange(); + d->tags.remove(name); + emit tagsChanged(); + d->sendTagUpdates(); + } else if (!name.startsWith("u.")) + removeTag("u." + name); + else + qWarning(MAIN) << "Tag" << name << "on room" << objectName() + << "not found, nothing to remove"; } void Room::setTags(TagsMap newTags) { - const auto& changes = d->setTags(move(newTags)); - d->broadcastTagUpdates(changes.first, changes.second); + d->setTags(move(newTags)); + d->sendTagUpdates(); } -std::pair Room::Private::setTags(TagsMap newTags) +void Room::Private::setTags(TagsMap newTags) { - if (newTags == tags) - return {}; - - TagsMap additions; - const auto& tagNames = newTags.keys(); - for (const auto& t: tagNames) - { - const auto& checkRes = validatedTag(t); - const auto& value = checkRes.first ? - newTags.insert(checkRes.second, newTags.take(t)).value() : - newTags.value(checkRes.second); - if (!tags.contains(checkRes.second)) - additions.insert(checkRes.second, value); - } - - QStringList removals; - for (const auto& tag: tags.keys()) - if (!newTags.contains(tag)) - removals.push_back(tag); - - tags = newTags; - return { additions, removals }; + emit q->tagsAboutToChange(); + tags = move(newTags); + qCDebug(MAIN) << "Room" << id << "is tagged with:" + << q->tagNames().join(", "); + emit q->tagsChanged(); } bool Room::isFavourite() const @@ -1851,15 +1841,8 @@ void Room::processEphemeralEvent(EventPtr&& event) void Room::processAccountDataEvent(EventPtr&& event) { if (auto* evt = eventCast(event)) - { - const auto& changes = d->setTags(evt->tags()); - if (!(changes.first.empty() && changes.second.empty())) - { - qCDebug(MAIN) << "Room" << id() << "is tagged with:" - << tagNames().join(", "); - emit tagsChanged(changes.first, changes.second); - } - } + d->setTags(evt->tags()); + if (auto* evt = eventCast(event)) { auto readEventId = evt->event_id(); @@ -1877,6 +1860,7 @@ void Room::processAccountDataEvent(EventPtr&& event) // efficient; maaybe do it another day if (!currentData || currentData->contentJson() != event->contentJson()) { + emit accountDataAboutToChange(event->matrixType()); currentData = move(event); qCDebug(MAIN) << "Updated account data of type" << currentData->matrixType(); diff --git a/lib/room.h b/lib/room.h index ac29c33a2..f79fe86dc 100644 --- a/lib/room.h +++ b/lib/room.h @@ -129,20 +129,20 @@ namespace QMatrixClient /** * Returns a square room avatar with the given size and requests it * from the network if needed - * @return a pixmap with the avatar or a placeholder if there's none + * \return a pixmap with the avatar or a placeholder if there's none * available yet */ Q_INVOKABLE QImage avatar(int dimension); /** * Returns a room avatar with the given dimensions and requests it * from the network if needed - * @return a pixmap with the avatar or a placeholder if there's none + * \return a pixmap with the avatar or a placeholder if there's none * available yet */ Q_INVOKABLE QImage avatar(int width, int height); /** - * @brief Get a user object for a given user id + * \brief Get a user object for a given user id * This is the recommended way to get a user object in a room * context. The actual object type may be changed in further * versions to provide room-specific user information (display name, @@ -164,12 +164,12 @@ namespace QMatrixClient Q_INVOKABLE JoinState memberJoinState(User* user) const; /** - * @brief Produces a disambiguated name for a given user in + * Get a disambiguated name for a given user in * the context of the room */ Q_INVOKABLE QString roomMembername(const User* u) const; /** - * @brief Produces a disambiguated name for a user with this id in + * Get a disambiguated name for a user with this id in * the context of the room */ Q_INVOKABLE QString roomMembername(const QString& userId) const; @@ -177,8 +177,8 @@ namespace QMatrixClient const Timeline& messageEvents() const; const PendingEvents& pendingEvents() const; /** - * A convenience method returning the read marker to the before-oldest - * message + * A convenience method returning the read marker to + * the before-oldest message */ rev_iter_t timelineEdge() const; Q_INVOKABLE TimelineItem::index_t minTimelineIndex() const; @@ -204,7 +204,7 @@ namespace QMatrixClient QString readMarkerEventId() const; QList usersAtEventId(const QString& eventId); /** - * @brief Mark the event with uptoEventId as read + * \brief Mark the event with uptoEventId as read * * Finds in the timeline and marks as read the event with * the specified id; also posts a read receipt to the server either @@ -213,7 +213,7 @@ namespace QMatrixClient */ void markMessagesAsRead(QString uptoEventId); - /** Check whether there are unread messages in the room */ + /// Check whether there are unread messages in the room bool hasUnreadMessages() const; /** Get the number of unread messages in the room @@ -264,7 +264,7 @@ namespace QMatrixClient void addTag(const QString& name, const TagRecord& record = {}); Q_INVOKABLE void addTag(const QString& name, const QString& order); - /** Remove a tag from the room */ + /// Remove a tag from the room Q_INVOKABLE void removeTag(const QString& name); /** Overwrite the room's tags @@ -277,15 +277,15 @@ namespace QMatrixClient */ void setTags(TagsMap newTags); - /** Check whether the list of tags has m.favourite */ + /// Check whether the list of tags has m.favourite bool isFavourite() const; - /** Check whether the list of tags has m.lowpriority */ + /// Check whether the list of tags has m.lowpriority bool isLowPriority() const; - /** Check whether this room is a direct chat */ + /// Check whether this room is a direct chat Q_INVOKABLE bool isDirectChat() const; - /** Get the list of users this room is a direct chat with */ + /// Get the list of users this room is a direct chat with QList directChatUsers() const; Q_INVOKABLE QUrl urlToThumbnail(const QString& eventId); @@ -302,10 +302,6 @@ namespace QMatrixClient MemberSorter memberSorter() const; - QJsonObject toJson() const; - void updateData(SyncRoomData&& data ); - void setJoinState( JoinState state ); - public slots: QString postMessage(const QString& plainText, MessageEventType type); QString postPlainText(const QString& plainText); @@ -345,7 +341,7 @@ namespace QMatrixClient const QUrl& localFilename = {}); void cancelFileTransfer(const QString& id); - /** Mark all messages in the room as read */ + /// Mark all messages in the room as read void markAllMessagesAsRead(); signals: @@ -362,12 +358,12 @@ namespace QMatrixClient void pendingEventChanged(int pendingEventIndex); /** - * @brief The room name, the canonical alias or other aliases changed + * \brief The room name, the canonical alias or other aliases changed * * Not triggered when displayname changes. */ void namesChanged(Room* room); - /** @brief The room displayname changed */ + /// The room displayname changed void displaynameChanged(Room* room, QString oldName); void topicChanged(); void avatarChanged(); @@ -392,9 +388,10 @@ namespace QMatrixClient void readMarkerForUserMoved(User* user, QString fromEventId, QString toEventId); void unreadMessagesChanged(Room* room); + void accountDataAboutToChange(QString type); void accountDataChanged(QString type); - void tagsChanged(const TagsMap& additions, - const QStringList& removals); + void tagsAboutToChange(); + void tagsChanged(); void replacedEvent(const RoomEvent* newEvent, const RoomEvent* oldEvent); @@ -405,6 +402,17 @@ namespace QMatrixClient void fileTransferFailed(QString id, QString errorMessage = {}); void fileTransferCancelled(QString id); + /// The room is about to be deleted + void beforeDestruction(Room*); + + public: // Used by Connection - not a part of the client API + QJsonObject toJson() const; + void updateData(SyncRoomData&& data ); + + // Clients should use Connection::joinRoom() and Room::leaveRoom() + // to change the room state + void setJoinState( JoinState state ); + protected: /// Returns true if any of room names/aliases has changed virtual bool processStateEvent(const RoomEvent& e); diff --git a/lib/util.h b/lib/util.h index ce166e357..13eec143d 100644 --- a/lib/util.h +++ b/lib/util.h @@ -40,6 +40,15 @@ _ClassName(_ClassName&&) Q_DECL_EQ_DELETE; \ _ClassName& operator=(_ClassName&&) Q_DECL_EQ_DELETE; +#if QT_VERSION < QT_VERSION_CHECK(5, 7, 0) +// Copy-pasted from Qt 5.10 +template +Q_DECL_CONSTEXPR typename std::add_const::type &qAsConst(T &t) Q_DECL_NOTHROW { return t; } +// prevent rvalue arguments: +template +static void qAsConst(const T &&) Q_DECL_EQ_DELETE; +#endif + namespace QMatrixClient { // The below enables pretty-printing of enums in logs @@ -146,15 +155,6 @@ namespace QMatrixClient template using fn_arg_t = typename function_traits::arg_type; -#if QT_VERSION < QT_VERSION_CHECK(5, 7, 0) - // Copy-pasted from Qt 5.10 - template - Q_DECL_CONSTEXPR typename std::add_const::type &qAsConst(T &t) Q_DECL_NOTHROW { return t; } - // prevent rvalue arguments: - template - static void qAsConst(const T &&) Q_DECL_EQ_DELETE; -#endif - inline auto operator"" _ls(const char* s, std::size_t size) { return QLatin1String(s, int(size));