diff --git a/CMakeLists.txt b/CMakeLists.txt index fb104807ce7f64cded02f7e39ec67e3ca014e402..ae4ce2fa375a829ac3ffd6727354c2bd0a53164b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -356,7 +356,7 @@ if(USE_BUNDLED_MTXCLIENT) FetchContent_Declare( MatrixClient GIT_REPOSITORY https://github.com/Nheko-Reborn/mtxclient.git - GIT_TAG 5d2f055ea9403770039ddf66b1900f890cd5cde7 + GIT_TAG fdb2016eff4f2e91f17c343e9fcb0bfab5e78b63 ) set(BUILD_LIB_EXAMPLES OFF CACHE INTERNAL "") set(BUILD_LIB_TESTS OFF CACHE INTERNAL "") @@ -458,7 +458,7 @@ endif() # single instance functionality set(QAPPLICATION_CLASS QApplication CACHE STRING "Inheritance class for SingleApplication") -add_subdirectory(third_party/SingleApplication-3.2.0-dc8042b/) +add_subdirectory(third_party/SingleApplication-3.3.0/) feature_summary(WHAT ALL INCLUDE_QUIET_PACKAGES FATAL_ON_MISSING_REQUIRED_PACKAGES) diff --git a/io.github.NhekoReborn.Nheko.yaml b/io.github.NhekoReborn.Nheko.yaml index 2e2ec44655228fdbbea0f0e16e91cb8cb9df8cb9..564b08a991c986cdcb669cd44d9384286bda167d 100644 --- a/io.github.NhekoReborn.Nheko.yaml +++ b/io.github.NhekoReborn.Nheko.yaml @@ -150,7 +150,7 @@ modules: buildsystem: cmake-ninja name: mtxclient sources: - - commit: 5d2f055ea9403770039ddf66b1900f890cd5cde7 + - commit: fdb2016eff4f2e91f17c343e9fcb0bfab5e78b63 type: git url: https://github.com/Nheko-Reborn/mtxclient.git - config-opts: diff --git a/resources/qml/ChatPage.qml b/resources/qml/ChatPage.qml index df2bf41fc4505b83fcc848c4d33ff603bf768081..cd323a97b56961554ca54308cbfc2562b19bae13 100644 --- a/resources/qml/ChatPage.qml +++ b/resources/qml/ChatPage.qml @@ -71,7 +71,7 @@ Rectangle { AdaptiveLayoutElement { id: timlineViewC - minimumWidth: fontMetrics.averageCharacterWidth * 40 + Nheko.avatarSize + 2* Nheko.paddingMedium + minimumWidth: fontMetrics.averageCharacterWidth * 40 + Nheko.avatarSize + 2 * Nheko.paddingMedium TimelineView { id: timeline diff --git a/resources/qml/MatrixText.qml b/resources/qml/MatrixText.qml index 167899a549db77cb8c5e2851536a54d534346a3c..65d66315bb469f0d82c05d4c07f46748f6070e4b 100644 --- a/resources/qml/MatrixText.qml +++ b/resources/qml/MatrixText.qml @@ -12,7 +12,8 @@ TextEdit { focus: false wrapMode: Text.Wrap selectByMouse: !Settings.mobileMode - enabled: selectByMouse + // this always has to be enabled, otherwise you can't click links anymore! + //enabled: selectByMouse color: Nheko.colors.text onLinkActivated: Nheko.openLink(link) ToolTip.visible: hoveredLink diff --git a/resources/qml/RoomList.qml b/resources/qml/RoomList.qml index 5845eb8143d5b6924e944f11a24ef5acbae1d105..c4a8bcfb1c1347f441853d0e363bf53cf76a205a 100644 --- a/resources/qml/RoomList.qml +++ b/resources/qml/RoomList.qml @@ -255,6 +255,8 @@ Page { Label { id: timestamp + visible: !model.isInvite && !model.isSpace + width: visible ? 0 : undefined Layout.alignment: Qt.AlignRight | Qt.AlignBottom font.pixelSize: fontMetrics.font.pixelSize * 0.9 color: roomItem.unimportantText @@ -266,12 +268,11 @@ Page { RowLayout { Layout.fillWidth: true spacing: 0 - visible: !model.isInvite + visible: !model.isInvite && !model.isSpace height: visible ? 0 : undefined ElidedLabel { color: roomItem.unimportantText - font.weight: Font.Thin font.pixelSize: fontMetrics.font.pixelSize * 0.9 elideWidth: textContent.width - (notificationBubble.visible ? notificationBubble.width : 0) - Nheko.paddingSmall fullText: model.lastMessage @@ -485,7 +486,6 @@ Page { ElidedLabel { Layout.alignment: Qt.AlignTop color: Nheko.colors.buttonText - font.weight: Font.Thin font.pointSize: fontMetrics.font.pointSize * 0.9 elideWidth: col.width fullText: userInfoGrid.profile ? userInfoGrid.profile.userid : "" diff --git a/resources/qml/TimelineView.qml b/resources/qml/TimelineView.qml index 90e2816666f8b4148bd9fda7d3112c8da0092d19..2c7c943abc5cdad9e063a64569e9ea8886e0ee3a 100644 --- a/resources/qml/TimelineView.qml +++ b/resources/qml/TimelineView.qml @@ -41,7 +41,8 @@ Item { ColumnLayout { id: timelineLayout - visible: room != null + visible: room != null && !room.isSpace + enabled: visible anchors.fill: parent spacing: 0 @@ -127,6 +128,79 @@ Item { } + ColumnLayout { + visible: room != null && room.isSpace + enabled: visible + anchors.fill: parent + anchors.margins: Nheko.paddingLarge + spacing: Nheko.paddingLarge + + Avatar { + url: room ? room.roomAvatarUrl.replace("mxc://", "image://MxcImage/") : "" + displayName: room ? room.roomName : "" + height: 130 + width: 130 + Layout.alignment: Qt.AlignHCenter + enabled: false + } + + MatrixText { + text: room ? room.roomName : "" + font.pixelSize: 24 + Layout.alignment: Qt.AlignHCenter + } + + MatrixText { + text: qsTr("%1 member(s)").arg(room ? room.roomMemberCount : 0) + Layout.alignment: Qt.AlignHCenter + } + + ScrollView { + Layout.alignment: Qt.AlignHCenter + width: timelineView.width - Nheko.paddingLarge * 2 + + TextArea { + text: TimelineManager.escapeEmoji(room ? room.roomTopic : "") + wrapMode: TextEdit.WordWrap + textFormat: TextEdit.RichText + readOnly: true + background: null + selectByMouse: true + color: Nheko.colors.text + horizontalAlignment: TextEdit.AlignHCenter + onLinkActivated: Nheko.openLink(link) + + CursorShape { + anchors.fill: parent + cursorShape: parent.hoveredLink ? Qt.PointingHandCursor : Qt.ArrowCursor + } + + } + + } + + Item { + Layout.fillHeight: true + } + + } + + ImageButton { + id: backToRoomsButton + + anchors.top: parent.top + anchors.left: parent.left + anchors.margins: Nheko.paddingMedium + width: Nheko.avatarSize + height: Nheko.avatarSize + visible: room != null && room.isSpace && showBackButton + enabled: visible + image: ":/icons/icons/ui/angle-pointing-to-left.png" + ToolTip.visible: hovered + ToolTip.text: qsTr("Back to room list") + onClicked: Rooms.resetCurrentRoom() + } + NhekoDropArea { anchors.fill: parent roomid: room ? room.roomId() : "" diff --git a/src/Cache.cpp b/src/Cache.cpp index 2178bbfbb991a6cce57062244589e5a2a92f5ac8..144a2d9ab43a548801a751edeb758d9a61706515 100644 --- a/src/Cache.cpp +++ b/src/Cache.cpp @@ -55,6 +55,10 @@ constexpr auto BATCH_SIZE = 100; //! Format: room_id -> RoomInfo constexpr auto ROOMS_DB("rooms"); constexpr auto INVITES_DB("invites"); +//! maps each room to its parent space (id->id) +constexpr auto SPACES_PARENTS_DB("space_parents"); +//! maps each space to its current children (id->id) +constexpr auto SPACES_CHILDREN_DB("space_children"); //! Information that must be kept between sync requests. constexpr auto SYNC_STATE_DB("sync_state"); //! Read receipts per room/event. @@ -237,12 +241,14 @@ Cache::setup() env_.open(cacheDirectory_.toStdString().c_str()); } - auto txn = lmdb::txn::begin(env_); - syncStateDb_ = lmdb::dbi::open(txn, SYNC_STATE_DB, MDB_CREATE); - roomsDb_ = lmdb::dbi::open(txn, ROOMS_DB, MDB_CREATE); - invitesDb_ = lmdb::dbi::open(txn, INVITES_DB, MDB_CREATE); - readReceiptsDb_ = lmdb::dbi::open(txn, READ_RECEIPTS_DB, MDB_CREATE); - notificationsDb_ = lmdb::dbi::open(txn, NOTIFICATIONS_DB, MDB_CREATE); + auto txn = lmdb::txn::begin(env_); + syncStateDb_ = lmdb::dbi::open(txn, SYNC_STATE_DB, MDB_CREATE); + roomsDb_ = lmdb::dbi::open(txn, ROOMS_DB, MDB_CREATE); + spacesChildrenDb_ = lmdb::dbi::open(txn, SPACES_CHILDREN_DB, MDB_CREATE | MDB_DUPSORT); + spacesParentsDb_ = lmdb::dbi::open(txn, SPACES_PARENTS_DB, MDB_CREATE | MDB_DUPSORT); + invitesDb_ = lmdb::dbi::open(txn, INVITES_DB, MDB_CREATE); + readReceiptsDb_ = lmdb::dbi::open(txn, READ_RECEIPTS_DB, MDB_CREATE); + notificationsDb_ = lmdb::dbi::open(txn, NOTIFICATIONS_DB, MDB_CREATE); // Device management devicesDb_ = lmdb::dbi::open(txn, DEVICES_DB, MDB_CREATE); @@ -899,7 +905,9 @@ Cache::runMigrations() std::reverse(oldMessages.events.begin(), oldMessages.events.end()); // save messages using the new method - saveTimelineMessages(txn, room_id, oldMessages); + auto eventsDb = getEventsDb(txn, room_id); + saveTimelineMessages( + txn, eventsDb, room_id, oldMessages); } // delete old messages db @@ -1194,24 +1202,73 @@ Cache::saveState(const mtx::responses::Sync &res) auto userKeyCacheDb = getUserKeysDb(txn); + std::set<std::string> spaces_with_updates; + std::set<std::string> rooms_with_space_updates; + // Save joined rooms for (const auto &room : res.rooms.join) { auto statesdb = getStatesDb(txn, room.first); auto stateskeydb = getStatesKeyDb(txn, room.first); auto membersdb = getMembersDb(txn, room.first); - - saveStateEvents( - txn, statesdb, stateskeydb, membersdb, room.first, room.second.state.events); - saveStateEvents( - txn, statesdb, stateskeydb, membersdb, room.first, room.second.timeline.events); - - saveTimelineMessages(txn, room.first, room.second.timeline); + auto eventsDb = getEventsDb(txn, room.first); + + saveStateEvents(txn, + statesdb, + stateskeydb, + membersdb, + eventsDb, + room.first, + room.second.state.events); + saveStateEvents(txn, + statesdb, + stateskeydb, + membersdb, + eventsDb, + room.first, + room.second.timeline.events); + + saveTimelineMessages(txn, eventsDb, room.first, room.second.timeline); RoomInfo updatedInfo; updatedInfo.name = getRoomName(txn, statesdb, membersdb).toStdString(); updatedInfo.topic = getRoomTopic(txn, statesdb).toStdString(); updatedInfo.avatar_url = getRoomAvatarUrl(txn, statesdb, membersdb).toStdString(); updatedInfo.version = getRoomVersion(txn, statesdb).toStdString(); + updatedInfo.is_space = getRoomIsSpace(txn, statesdb); + + if (updatedInfo.is_space) { + bool space_updates = false; + for (const auto &e : room.second.state.events) + if (std::holds_alternative<StateEvent<state::space::Child>>(e) || + std::holds_alternative<StateEvent<state::PowerLevels>>(e)) + space_updates = true; + for (const auto &e : room.second.timeline.events) + if (std::holds_alternative<StateEvent<state::space::Child>>(e) || + std::holds_alternative<StateEvent<state::PowerLevels>>(e)) + space_updates = true; + + if (space_updates) + spaces_with_updates.insert(room.first); + } + + { + bool room_has_space_update = false; + for (const auto &e : room.second.state.events) { + if (auto se = std::get_if<StateEvent<state::space::Parent>>(&e)) { + spaces_with_updates.insert(se->state_key); + room_has_space_update = true; + } + } + for (const auto &e : room.second.timeline.events) { + if (auto se = std::get_if<StateEvent<state::space::Parent>>(&e)) { + spaces_with_updates.insert(se->state_key); + room_has_space_update = true; + } + } + + if (room_has_space_update) + rooms_with_space_updates.insert(room.first); + } bool has_new_tags = false; // Process the account_data associated with this room @@ -1291,6 +1348,8 @@ Cache::saveState(const mtx::responses::Sync &res) removeLeftRooms(txn, res.rooms.leave); + updateSpaces(txn, spaces_with_updates, std::move(rooms_with_space_updates)); + txn.commit(); std::map<QString, bool> readStatus; @@ -1339,6 +1398,7 @@ Cache::saveInvites(lmdb::txn &txn, const std::map<std::string, mtx::responses::I updatedInfo.topic = getInviteRoomTopic(txn, statesdb).toStdString(); updatedInfo.avatar_url = getInviteRoomAvatarUrl(txn, statesdb, membersdb).toStdString(); + updatedInfo.is_space = getInviteRoomIsSpace(txn, statesdb); updatedInfo.is_invite = true; invitesDb_.put(txn, room.first, json(updatedInfo).dump()); @@ -1427,27 +1487,6 @@ Cache::roomsWithStateUpdates(const mtx::responses::Sync &res) return rooms; } -std::vector<std::string> -Cache::roomsWithTagUpdates(const mtx::responses::Sync &res) -{ - using namespace mtx::events; - - std::vector<std::string> rooms; - for (const auto &room : res.rooms.join) { - bool hasUpdates = false; - for (const auto &evt : room.second.account_data.events) { - if (std::holds_alternative<AccountDataEvent<account_data::Tags>>(evt)) { - hasUpdates = true; - } - } - - if (hasUpdates) - rooms.emplace_back(room.first); - } - - return rooms; -} - RoomInfo Cache::singleRoomInfo(const std::string &room_id) { @@ -1733,6 +1772,13 @@ Cache::relatedEvents(const std::string &room_id, const std::string &event_id) return related_ids; } +size_t +Cache::memberCount(const std::string &room_id) +{ + auto txn = lmdb::txn::begin(env_, nullptr, MDB_RDONLY); + return getMembersDb(txn, room_id).size(txn); +} + QMap<QString, RoomInfo> Cache::roomInfo(bool withInvites) { @@ -2337,6 +2383,29 @@ Cache::getRoomVersion(lmdb::txn &txn, lmdb::dbi &statesdb) return QString("1"); } +bool +Cache::getRoomIsSpace(lmdb::txn &txn, lmdb::dbi &statesdb) +{ + using namespace mtx::events; + using namespace mtx::events::state; + + std::string_view event; + bool res = statesdb.get(txn, to_string(mtx::events::EventType::RoomCreate), event); + + if (res) { + try { + StateEvent<Create> msg = json::parse(event); + + return msg.content.type == mtx::events::state::room_type::space; + } catch (const json::exception &e) { + nhlog::db()->warn("failed to parse m.room.create event: {}", e.what()); + } + } + + nhlog::db()->warn("m.room.create event is missing room version, assuming version \"1\""); + return false; +} + std::optional<mtx::events::state::CanonicalAlias> Cache::getRoomAliases(const std::string &roomid) { @@ -2464,6 +2533,27 @@ Cache::getInviteRoomTopic(lmdb::txn &txn, lmdb::dbi &db) return QString(); } +bool +Cache::getInviteRoomIsSpace(lmdb::txn &txn, lmdb::dbi &db) +{ + using namespace mtx::events; + using namespace mtx::events::state; + + std::string_view event; + bool res = db.get(txn, to_string(mtx::events::EventType::RoomCreate), event); + + if (res) { + try { + StrippedEvent<Create> msg = json::parse(event); + return msg.content.type == mtx::events::state::room_type::space; + } catch (const json::exception &e) { + nhlog::db()->warn("failed to parse m.room.topic event: {}", e.what()); + } + } + + return false; +} + std::vector<std::string> Cache::joinedRooms() { @@ -2506,42 +2596,6 @@ Cache::getMember(const std::string &room_id, const std::string &user_id) return std::nullopt; } -std::vector<RoomSearchResult> -Cache::searchRooms(const std::string &query, std::uint8_t max_items) -{ - std::multimap<int, std::pair<std::string, RoomInfo>> items; - - auto txn = lmdb::txn::begin(env_, nullptr, MDB_RDONLY); - auto cursor = lmdb::cursor::open(txn, roomsDb_); - - std::string_view room_id, room_data; - while (cursor.get(room_id, room_data, MDB_NEXT)) { - RoomInfo tmp = json::parse(room_data); - - const int score = utils::levenshtein_distance( - query, QString::fromStdString(tmp.name).toLower().toStdString()); - items.emplace(score, std::make_pair(room_id, tmp)); - } - - cursor.close(); - - auto end = items.begin(); - - if (items.size() >= max_items) - std::advance(end, max_items); - else if (items.size() > 0) - std::advance(end, items.size()); - - std::vector<RoomSearchResult> results; - for (auto it = items.begin(); it != end; it++) { - results.push_back(RoomSearchResult{it->second.first, it->second.second}); - } - - txn.commit(); - - return results; -} - std::vector<RoomMember> Cache::getMembers(const std::string &room_id, std::size_t startIndex, std::size_t len) { @@ -2600,11 +2654,12 @@ void Cache::savePendingMessage(const std::string &room_id, const mtx::events::collections::TimelineEvent &message) { - auto txn = lmdb::txn::begin(env_); + auto txn = lmdb::txn::begin(env_); + auto eventsDb = getEventsDb(txn, room_id); mtx::responses::Timeline timeline; timeline.events.push_back(message.data); - saveTimelineMessages(txn, room_id, timeline); + saveTimelineMessages(txn, eventsDb, room_id, timeline); auto pending = getPendingMessagesDb(txn, room_id); @@ -2672,13 +2727,13 @@ Cache::removePendingStatus(const std::string &room_id, const std::string &txn_id void Cache::saveTimelineMessages(lmdb::txn &txn, + lmdb::dbi &eventsDb, const std::string &room_id, const mtx::responses::Timeline &res) { if (res.events.empty()) return; - auto eventsDb = getEventsDb(txn, room_id); auto relationsDb = getRelationsDb(txn, room_id); auto orderDb = getEventOrderDb(txn, room_id); @@ -3203,6 +3258,147 @@ Cache::deleteOldData() noexcept } } +void +Cache::updateSpaces(lmdb::txn &txn, + const std::set<std::string> &spaces_with_updates, + std::set<std::string> rooms_with_updates) +{ + if (spaces_with_updates.empty() && rooms_with_updates.empty()) + return; + + for (const auto &space : spaces_with_updates) { + // delete old entries + { + auto cursor = lmdb::cursor::open(txn, spacesChildrenDb_); + bool first = true; + std::string_view sp = space, space_child = ""; + + if (cursor.get(sp, space_child, MDB_SET)) { + while (cursor.get( + sp, space_child, first ? MDB_FIRST_DUP : MDB_NEXT_DUP)) { + first = false; + spacesParentsDb_.del(txn, space_child, space); + } + } + cursor.close(); + spacesChildrenDb_.del(txn, space); + } + + for (const auto &event : + getStateEventsWithType<mtx::events::state::space::Child>(txn, space)) { + if (event.content.via.has_value() && event.state_key.size() > 3 && + event.state_key.at(0) == '!') { + spacesChildrenDb_.put(txn, space, event.state_key); + spacesParentsDb_.put(txn, event.state_key, space); + } + } + } + + const auto space_event_type = to_string(mtx::events::EventType::RoomPowerLevels); + + for (const auto &room : rooms_with_updates) { + for (const auto &event : + getStateEventsWithType<mtx::events::state::space::Parent>(txn, room)) { + if (event.content.via.has_value() && event.state_key.size() > 3 && + event.state_key.at(0) == '!') { + const std::string &space = event.state_key; + + auto pls = + getStateEvent<mtx::events::state::PowerLevels>(txn, space); + + if (!pls) + continue; + + if (pls->content.user_level(event.sender) >= + pls->content.state_level(space_event_type)) { + spacesChildrenDb_.put(txn, space, room); + spacesParentsDb_.put(txn, room, space); + } + } + } + } +} + +QMap<QString, std::optional<RoomInfo>> +Cache::spaces() +{ + auto txn = lmdb::txn::begin(env_, nullptr, MDB_RDONLY); + + QMap<QString, std::optional<RoomInfo>> ret; + { + auto cursor = lmdb::cursor::open(txn, spacesChildrenDb_); + bool first = true; + std::string_view space_id, space_child; + while (cursor.get(space_id, space_child, first ? MDB_FIRST : MDB_NEXT)) { + first = false; + + if (!space_child.empty()) { + std::string_view room_data; + if (roomsDb_.get(txn, space_id, room_data)) { + RoomInfo tmp = json::parse(std::move(room_data)); + ret.insert( + QString::fromUtf8(space_id.data(), space_id.size()), tmp); + } else { + ret.insert( + QString::fromUtf8(space_id.data(), space_id.size()), + std::nullopt); + } + } + } + cursor.close(); + } + + return ret; +} + +std::vector<std::string> +Cache::getParentRoomIds(const std::string &room_id) +{ + auto txn = lmdb::txn::begin(env_, nullptr, MDB_RDONLY); + + std::vector<std::string> roomids; + { + auto cursor = lmdb::cursor::open(txn, spacesParentsDb_); + bool first = true; + std::string_view sp = room_id, space_parent; + if (cursor.get(sp, space_parent, MDB_SET)) { + while (cursor.get(sp, space_parent, first ? MDB_FIRST_DUP : MDB_NEXT_DUP)) { + first = false; + + if (!space_parent.empty()) + roomids.emplace_back(space_parent); + } + } + cursor.close(); + } + + return roomids; +} + +std::vector<std::string> +Cache::getChildRoomIds(const std::string &room_id) +{ + auto txn = lmdb::txn::begin(env_, nullptr, MDB_RDONLY); + + std::vector<std::string> roomids; + { + auto cursor = lmdb::cursor::open(txn, spacesChildrenDb_); + bool first = true; + std::string_view sp = room_id, space_child; + if (cursor.get(sp, space_child, MDB_SET)) { + while (cursor.get(sp, space_child, first ? MDB_FIRST_DUP : MDB_NEXT_DUP)) { + first = false; + + if (!space_child.empty()) + roomids.emplace_back(space_child); + } + } + cursor.close(); + } + + return roomids; +} + std::optional<mtx::events::collections::RoomAccountDataEvents> Cache::getAccountData(lmdb::txn &txn, mtx::events::EventType type, const std::string &room_id) { @@ -3884,6 +4080,7 @@ to_json(json &j, const RoomInfo &info) j["avatar_url"] = info.avatar_url; j["version"] = info.version; j["is_invite"] = info.is_invite; + j["is_space"] = info.is_space; j["join_rule"] = info.join_rule; j["guest_access"] = info.guest_access; @@ -3903,6 +4100,7 @@ from_json(const json &j, RoomInfo &info) info.version = j.value( "version", QCoreApplication::translate("RoomInfo", "no version stored").toStdString()); info.is_invite = j.at("is_invite"); + info.is_space = j.value("is_space", false); info.join_rule = j.at("join_rule"); info.guest_access = j.at("guest_access"); @@ -4158,12 +4356,6 @@ getRoomAvatarUrl(lmdb::txn &txn, lmdb::dbi &statesdb, lmdb::dbi &membersdb) return instance_->getRoomAvatarUrl(txn, statesdb, membersdb); } -QString -getRoomVersion(lmdb::txn &txn, lmdb::dbi &statesdb) -{ - return instance_->getRoomVersion(txn, statesdb); -} - std::vector<RoomMember> getMembers(const std::string &room_id, std::size_t startIndex, std::size_t len) { @@ -4305,11 +4497,7 @@ roomsWithStateUpdates(const mtx::responses::Sync &res) { return instance_->roomsWithStateUpdates(res); } -std::vector<std::string> -roomsWithTagUpdates(const mtx::responses::Sync &res) -{ - return instance_->roomsWithTagUpdates(res); -} + std::map<QString, RoomInfo> getRoomInfo(const std::vector<std::string> &rooms) { @@ -4329,12 +4517,6 @@ calculateRoomReadStatus() instance_->calculateRoomReadStatus(); } -std::vector<RoomSearchResult> -searchRooms(const std::string &query, std::uint8_t max_items) -{ - return instance_->searchRooms(query, max_items); -} - void markSentNotification(const std::string &event_id) { diff --git a/src/Cache.h b/src/Cache.h index 74ec9695c9bbe7b8f4c7c8a17e9b533d5c19b6fd..b0520f6b112489170576b3583fc4134e2d07c4f4 100644 --- a/src/Cache.h +++ b/src/Cache.h @@ -79,9 +79,6 @@ getRoomTopic(lmdb::txn &txn, lmdb::dbi &statesdb); //! Retrieve the room avatar's url if any. QString getRoomAvatarUrl(lmdb::txn &txn, lmdb::dbi &statesdb, lmdb::dbi &membersdb); -//! Retrieve the version of the room if any. -QString -getRoomVersion(lmdb::txn &txn, lmdb::dbi &statesdb); //! Retrieve member info from a room. std::vector<RoomMember> @@ -166,9 +163,6 @@ calculateRoomReadStatus(const std::string &room_id); void calculateRoomReadStatus(); -std::vector<RoomSearchResult> -searchRooms(const std::string &query, std::uint8_t max_items = 5); - void markSentNotification(const std::string &event_id); //! Removes an event from the sent notifications. diff --git a/src/CacheStructs.h b/src/CacheStructs.h index f7d6f0e24d8f534e04a04f79a0c846070728c04f..1d0f0d705aaeaeaa57745ff8515f19c479d0bf77 100644 --- a/src/CacheStructs.h +++ b/src/CacheStructs.h @@ -76,6 +76,8 @@ struct RoomInfo std::string version; //! Whether or not the room is an invite. bool is_invite = false; + //! Wheter or not the room is a space + bool is_space = false; //! Total number of members in the room. size_t member_count = 0; //! Who can access to the room. diff --git a/src/Cache_p.h b/src/Cache_p.h index 669f1895212150e537cc70c486d51446aa617d5b..cfcf9c9eb6358ca99a2edab4ddf2a7fbc4c589ac 100644 --- a/src/Cache_p.h +++ b/src/Cache_p.h @@ -72,6 +72,7 @@ public: std::optional<mtx::events::state::CanonicalAlias> getRoomAliases(const std::string &roomid); QHash<QString, RoomInfo> invites(); std::optional<RoomInfo> invite(std::string_view roomid); + QMap<QString, std::optional<RoomInfo>> spaces(); //! Calculate & return the name of the room. QString getRoomName(lmdb::txn &txn, lmdb::dbi &statesdb, lmdb::dbi &membersdb); @@ -84,6 +85,8 @@ public: QString getRoomAvatarUrl(lmdb::txn &txn, lmdb::dbi &statesdb, lmdb::dbi &membersdb); //! Retrieve the version of the room if any. QString getRoomVersion(lmdb::txn &txn, lmdb::dbi &statesdb); + //! Retrieve if the room is a space + bool getRoomIsSpace(lmdb::txn &txn, lmdb::dbi &statesdb); //! Get a specific state event template<typename T> @@ -98,6 +101,7 @@ public: std::vector<RoomMember> getMembers(const std::string &room_id, std::size_t startIndex = 0, std::size_t len = 30); + size_t memberCount(const std::string &room_id); void saveState(const mtx::responses::Sync &res); bool isInitialized(); @@ -146,7 +150,6 @@ public: RoomInfo singleRoomInfo(const std::string &room_id); std::vector<std::string> roomsWithStateUpdates(const mtx::responses::Sync &res); - std::vector<std::string> roomsWithTagUpdates(const mtx::responses::Sync &res); std::map<QString, RoomInfo> getRoomInfo(const std::vector<std::string> &rooms); //! Calculates which the read status of a room. @@ -154,9 +157,6 @@ public: bool calculateRoomReadStatus(const std::string &room_id); void calculateRoomReadStatus(); - std::vector<RoomSearchResult> searchRooms(const std::string &query, - std::uint8_t max_items = 5); - void markSentNotification(const std::string &event_id); //! Removes an event from the sent notifications. void removeReadNotification(const std::string &event_id); @@ -222,6 +222,8 @@ public: void deleteOldData() noexcept; //! Retrieve all saved room ids. std::vector<std::string> getRoomIds(lmdb::txn &txn); + std::vector<std::string> getParentRoomIds(const std::string &room_id); + std::vector<std::string> getChildRoomIds(const std::string &room_id); //! Mark a room that uses e2e encryption. void setEncryptedRoom(lmdb::txn &txn, const std::string &room_id); @@ -327,12 +329,14 @@ private: QString getInviteRoomName(lmdb::txn &txn, lmdb::dbi &statesdb, lmdb::dbi &membersdb); QString getInviteRoomTopic(lmdb::txn &txn, lmdb::dbi &statesdb); QString getInviteRoomAvatarUrl(lmdb::txn &txn, lmdb::dbi &statesdb, lmdb::dbi &membersdb); + bool getInviteRoomIsSpace(lmdb::txn &txn, lmdb::dbi &db); std::optional<MemberInfo> getMember(const std::string &room_id, const std::string &user_id); std::string getLastEventId(lmdb::txn &txn, const std::string &room_id); DescInfo getLastMessageInfo(lmdb::txn &txn, const std::string &room_id); void saveTimelineMessages(lmdb::txn &txn, + lmdb::dbi &eventsDb, const std::string &room_id, const mtx::responses::Timeline &res); @@ -351,11 +355,12 @@ private: lmdb::dbi &statesdb, lmdb::dbi &stateskeydb, lmdb::dbi &membersdb, + lmdb::dbi &eventsDb, const std::string &room_id, const std::vector<T> &events) { for (const auto &e : events) - saveStateEvent(txn, statesdb, stateskeydb, membersdb, room_id, e); + saveStateEvent(txn, statesdb, stateskeydb, membersdb, eventsDb, room_id, e); } template<class T> @@ -363,6 +368,7 @@ private: lmdb::dbi &statesdb, lmdb::dbi &stateskeydb, lmdb::dbi &membersdb, + lmdb::dbi &eventsDb, const std::string &room_id, const T &event) { @@ -399,8 +405,10 @@ private: } std::visit( - [&txn, &statesdb, &stateskeydb](auto e) { - if constexpr (isStateEvent(e)) + [&txn, &statesdb, &stateskeydb, &eventsDb](auto e) { + if constexpr (isStateEvent(e)) { + eventsDb.put(txn, e.event_id, json(e).dump()); + if (e.type != EventType::Unsupported) { if (e.state_key.empty()) statesdb.put( @@ -415,6 +423,7 @@ private: }) .dump()); } + } }, event); } @@ -430,20 +439,22 @@ private: if (room_id.empty()) return std::nullopt; + const auto typeStr = to_string(type); std::string_view value; if (state_key.empty()) { auto db = getStatesDb(txn, room_id); - if (!db.get(txn, to_string(type), value)) { + if (!db.get(txn, typeStr, value)) { return std::nullopt; } } else { - auto db = getStatesKeyDb(txn, room_id); - std::string d = json::object({{"key", state_key}}).dump(); - std::string_view data = d; + auto db = getStatesKeyDb(txn, room_id); + std::string d = json::object({{"key", state_key}}).dump(); + std::string_view data = d; + std::string_view typeStrV = typeStr; auto cursor = lmdb::cursor::open(txn, db); - if (!cursor.get(state_key, data, MDB_GET_BOTH)) + if (!cursor.get(typeStrV, data, MDB_GET_BOTH)) return std::nullopt; try { @@ -463,6 +474,47 @@ private: } } + template<typename T> + std::vector<mtx::events::StateEvent<T>> getStateEventsWithType(lmdb::txn &txn, + const std::string &room_id) + + { + constexpr auto type = mtx::events::state_content_to_type<T>; + static_assert(type != mtx::events::EventType::Unsupported, + "Not a supported type in state events."); + + if (room_id.empty()) + return {}; + + std::vector<mtx::events::StateEvent<T>> events; + + { + auto db = getStatesKeyDb(txn, room_id); + auto eventsDb = getEventsDb(txn, room_id); + const auto typeStr = to_string(type); + std::string_view typeStrV = typeStr; + std::string_view data; + std::string_view value; + + auto cursor = lmdb::cursor::open(txn, db); + bool first = true; + if (cursor.get(typeStrV, data, MDB_SET)) { + while (cursor.get( + typeStrV, data, first ? MDB_FIRST_DUP : MDB_NEXT_DUP)) { + first = false; + + if (eventsDb.get(txn, + json::parse(data)["id"].get<std::string>(), + value)) + events.push_back( + json::parse(value) + .get<mtx::events::StateEvent<T>>()); + } + } + } + + return events; + } void saveInvites(lmdb::txn &txn, const std::map<std::string, mtx::responses::InvitedRoom> &rooms); @@ -482,6 +534,10 @@ private: } } + void updateSpaces(lmdb::txn &txn, + const std::set<std::string> &spaces_with_updates, + std::set<std::string> rooms_with_updates); + lmdb::dbi getPendingReceiptsDb(lmdb::txn &txn) { return lmdb::dbi::open(txn, "pending_receipts", MDB_CREATE); @@ -548,8 +604,8 @@ private: lmdb::dbi getStatesKeyDb(lmdb::txn &txn, const std::string &room_id) { - auto db = - lmdb::dbi::open(txn, std::string(room_id + "/state_by_key").c_str(), MDB_CREATE); + auto db = lmdb::dbi::open( + txn, std::string(room_id + "/state_by_key").c_str(), MDB_CREATE | MDB_DUPSORT); lmdb::dbi_set_dupsort(txn, db, compare_state_key); return db; } @@ -611,6 +667,7 @@ private: lmdb::env env_; lmdb::dbi syncStateDb_; lmdb::dbi roomsDb_; + lmdb::dbi spacesChildrenDb_, spacesParentsDb_; lmdb::dbi invitesDb_; lmdb::dbi readReceiptsDb_; lmdb::dbi notificationsDb_; diff --git a/src/UserSettingsPage.cpp b/src/UserSettingsPage.cpp index 9b9065552687e6547364b9096c1dd750bfc844ed..6c0d8728f726d8d0ec421409a9c2613aa51918f1 100644 --- a/src/UserSettingsPage.cpp +++ b/src/UserSettingsPage.cpp @@ -613,6 +613,7 @@ UserSettings::save() settings.setValue("mobile_mode", mobileMode_); settings.setValue("font_size", baseFontSize_); settings.setValue("typing_notifications", typingNotifications_); + settings.setValue("sort_by_unread", sortByImportance_); settings.setValue("minor_events", sortByImportance_); settings.setValue("read_receipts", readReceipts_); settings.setValue("group_view", groupView_); diff --git a/src/timeline/CommunitiesModel.cpp b/src/timeline/CommunitiesModel.cpp index 6c2367842dbffc01dd08e3d378fa5cb25d537a24..97bfa76d58dfdfd53af7c8b45b22d1163b2c5834 100644 --- a/src/timeline/CommunitiesModel.cpp +++ b/src/timeline/CommunitiesModel.cpp @@ -44,8 +44,23 @@ CommunitiesModel::data(const QModelIndex &index, int role) const case CommunitiesModel::Roles::Id: return ""; } - } else if (index.row() - 1 < tags_.size()) { - auto tag = tags_.at(index.row() - 1); + } else if (index.row() - 1 < spaceOrder_.size()) { + auto id = spaceOrder_.at(index.row() - 1); + switch (role) { + case CommunitiesModel::Roles::AvatarUrl: + return QString::fromStdString(spaces_.at(id).avatar_url); + case CommunitiesModel::Roles::DisplayName: + case CommunitiesModel::Roles::Tooltip: + return QString::fromStdString(spaces_.at(id).name); + case CommunitiesModel::Roles::ChildrenHidden: + return true; + case CommunitiesModel::Roles::Hidden: + return hiddentTagIds_.contains("space:" + id); + case CommunitiesModel::Roles::Id: + return "space:" + id; + } + } else if (index.row() - 1 < tags_.size() + spaceOrder_.size()) { + auto tag = tags_.at(index.row() - 1 - spaceOrder_.size()); if (tag == "m.favourite") { switch (role) { case CommunitiesModel::Roles::AvatarUrl: @@ -78,7 +93,6 @@ CommunitiesModel::data(const QModelIndex &index, int role) const case CommunitiesModel::Roles::AvatarUrl: return QString(":/icons/icons/ui/tag.png"); case CommunitiesModel::Roles::DisplayName: - return tag.mid(2); case CommunitiesModel::Roles::Tooltip: return tag.mid(2); } @@ -99,17 +113,27 @@ CommunitiesModel::data(const QModelIndex &index, int role) const void CommunitiesModel::initializeSidebar() { + beginResetModel(); + tags_.clear(); + spaceOrder_.clear(); + spaces_.clear(); + std::set<std::string> ts; - for (const auto &e : cache::roomInfo()) { - for (const auto &t : e.tags) { - if (t.find("u.") == 0 || t.find("m." == 0)) { - ts.insert(t); + std::vector<RoomInfo> tempSpaces; + auto infos = cache::roomInfo(); + for (auto it = infos.begin(); it != infos.end(); it++) { + if (it.value().is_space) { + spaceOrder_.push_back(it.key()); + spaces_[it.key()] = it.value(); + } else { + for (const auto &t : it.value().tags) { + if (t.find("u.") == 0 || t.find("m." == 0)) { + ts.insert(t); + } } } } - beginResetModel(); - tags_.clear(); for (const auto &t : ts) tags_.push_back(QString::fromStdString(t)); @@ -143,6 +167,25 @@ CommunitiesModel::sync(const mtx::responses::Rooms &rooms) mtx::events::AccountDataEvent<mtx::events::account_data::Tags>>(e)) { tagsUpdated = true; } + for (const auto &e : room.state.events) + if (std::holds_alternative< + mtx::events::StateEvent<mtx::events::state::space::Child>>(e) || + std::holds_alternative< + mtx::events::StateEvent<mtx::events::state::space::Parent>>(e)) { + tagsUpdated = true; + } + for (const auto &e : room.timeline.events) + if (std::holds_alternative< + mtx::events::StateEvent<mtx::events::state::space::Child>>(e) || + std::holds_alternative< + mtx::events::StateEvent<mtx::events::state::space::Parent>>(e)) { + tagsUpdated = true; + } + } + for (const auto &[roomid, room] : rooms.leave) { + (void)room; + if (spaceOrder_.contains(QString::fromStdString(roomid))) + tagsUpdated = true; } if (tagsUpdated) @@ -161,6 +204,15 @@ CommunitiesModel::setCurrentTagId(QString tagId) return; } } + } else if (tagId.startsWith("space:")) { + auto tag = tagId.mid(6); + for (const auto &t : spaceOrder_) { + if (t == tag) { + this->currentTagId_ = tagId; + emit currentTagIdChanged(currentTagId_); + return; + } + } } this->currentTagId_ = ""; @@ -181,7 +233,13 @@ CommunitiesModel::toggleTagId(QString tagId) if (tagId.startsWith("tag:")) { auto idx = tags_.indexOf(tagId.mid(4)); if (idx != -1) - emit dataChanged(index(idx), index(idx), {Hidden}); + emit dataChanged(index(idx + 1 + spaceOrder_.size()), + index(idx + 1 + spaceOrder_.size()), + {Hidden}); + } else if (tagId.startsWith("space:")) { + auto idx = spaceOrder_.indexOf(tagId.mid(6)); + if (idx != -1) + emit dataChanged(index(idx + 1), index(idx + 1), {Hidden}); } emit hiddenTagsChanged(); diff --git a/src/timeline/CommunitiesModel.h b/src/timeline/CommunitiesModel.h index 66d6b21b6148cf568157cf5b3c7ad378076f4ab5..8c40ec5b05ba1d1d4cde235822f49ed7c797e8ac 100644 --- a/src/timeline/CommunitiesModel.h +++ b/src/timeline/CommunitiesModel.h @@ -11,6 +11,8 @@ #include <mtx/responses/sync.hpp> +#include "CacheStructs.h" + class CommunitiesModel : public QAbstractListModel { Q_OBJECT @@ -71,4 +73,6 @@ private: QStringList tags_; QString currentTagId_; QStringList hiddentTagIds_; + QStringList spaceOrder_; + std::map<QString, RoomInfo> spaces_; }; diff --git a/src/timeline/EventStore.cpp b/src/timeline/EventStore.cpp index 04f7ef7685b2f3daedc60c229d3320a37488c626..9a91ff7940d7c0833f25fbc7a7b73252c80776c5 100644 --- a/src/timeline/EventStore.cpp +++ b/src/timeline/EventStore.cpp @@ -675,6 +675,9 @@ EventStore::decryptEvent(const IdIndex &idx, index.room_id, index.session_id, e.sender); + // we may not want to request keys during initial sync and such + if (suppressKeyRequests) + break; // TODO: Check if this actually works and look in key backup auto copy = e; copy.room_id = room_id_; @@ -816,6 +819,18 @@ EventStore::decryptEvent(const IdIndex &idx, return asCacheEntry(std::move(decryptionResult.event.value())); } +void +EventStore::enableKeyRequests(bool suppressKeyRequests_) +{ + if (!suppressKeyRequests_) { + for (const auto &key : decryptedEvents_.keys()) + if (key.room == this->room_id_) + decryptedEvents_.remove(key); + suppressKeyRequests = false; + } else + suppressKeyRequests = true; +} + mtx::events::collections::TimelineEvents * EventStore::get(std::string id, std::string_view related_to, bool decrypt, bool resolve_edits) { diff --git a/src/timeline/EventStore.h b/src/timeline/EventStore.h index d9bb86cb108fe4dd27812a336019fb8e4a588864..7c40410262256546ba254aac7633a5f196a4a0f6 100644 --- a/src/timeline/EventStore.h +++ b/src/timeline/EventStore.h @@ -115,6 +115,7 @@ public slots: void addPending(mtx::events::collections::TimelineEvents event); void receivedSessionKey(const std::string &session_id); void clearTimeline(); + void enableKeyRequests(bool suppressKeyRequests_); private: std::vector<mtx::events::collections::TimelineEvents> edits(const std::string &event_id); @@ -142,4 +143,5 @@ private: std::string current_txn; int current_txn_error_count = 0; bool noMoreMessages = false; + bool suppressKeyRequests = true; }; diff --git a/src/timeline/RoomlistModel.cpp b/src/timeline/RoomlistModel.cpp index 0f980c6cb5fb558509d10744f1d9abf581af1e36..7f59b11296d95581e594f3f900cb07582942c4b0 100644 --- a/src/timeline/RoomlistModel.cpp +++ b/src/timeline/RoomlistModel.cpp @@ -51,6 +51,7 @@ RoomlistModel::roleNames() const {IsInvite, "isInvite"}, {IsSpace, "isSpace"}, {Tags, "tags"}, + {ParentSpaces, "parentSpaces"}, }; } @@ -84,8 +85,9 @@ RoomlistModel::data(const QModelIndex &index, int role) const case Roles::NotificationCount: return room->notificationCount(); case Roles::IsInvite: - case Roles::IsSpace: return false; + case Roles::IsSpace: + return room->isSpace(); case Roles::Tags: { auto info = cache::singleRoomInfo(roomid.toStdString()); QStringList list; @@ -93,6 +95,14 @@ RoomlistModel::data(const QModelIndex &index, int role) const list.push_back(QString::fromStdString(t)); return list; } + case Roles::ParentSpaces: { + auto parents = + cache::client()->getParentRoomIds(roomid.toStdString()); + QStringList list; + for (const auto &t : parents) + list.push_back(QString::fromStdString(t)); + return list; + } default: return {}; } @@ -122,6 +132,14 @@ RoomlistModel::data(const QModelIndex &index, int role) const return false; case Roles::Tags: return QStringList(); + case Roles::ParentSpaces: { + auto parents = + cache::client()->getParentRoomIds(roomid.toStdString()); + QStringList list; + for (const auto &t : parents) + list.push_back(QString::fromStdString(t)); + return list; + } default: return {}; } @@ -412,7 +430,9 @@ enum NotificationImportance : short AllEventsRead = 0, NewMessage = 1, NewMentions = 2, - Invite = 3 + Invite = 3, + SubSpace = 4, + CurrentSpace = 5, }; } @@ -422,7 +442,13 @@ FilteredRoomlistModel::calculateImportance(const QModelIndex &idx) const // Returns the degree of importance of the unread messages in the room. // If sorting by importance is disabled in settings, this only ever // returns ImportanceDisabled or Invite - if (sourceModel()->data(idx, RoomlistModel::IsInvite).toBool()) { + if (sourceModel()->data(idx, RoomlistModel::IsSpace).toBool()) { + if (filterType == FilterBy::Space && + filterStr == sourceModel()->data(idx, RoomlistModel::RoomId).toString()) + return CurrentSpace; + else + return SubSpace; + } else if (sourceModel()->data(idx, RoomlistModel::IsInvite).toBool()) { return Invite; } else if (!this->sortByImportance) { return ImportanceDisabled; @@ -505,6 +531,12 @@ bool FilteredRoomlistModel::filterAcceptsRow(int sourceRow, const QModelIndex &) const { if (filterType == FilterBy::Nothing) { + if (sourceModel() + ->data(sourceModel()->index(sourceRow, 0), RoomlistModel::IsSpace) + .toBool()) { + return false; + } + if (!hiddenTags.empty()) { auto tags = sourceModel() @@ -516,19 +548,86 @@ FilteredRoomlistModel::filterAcceptsRow(int sourceRow, const QModelIndex &) cons return false; } + if (!hiddenSpaces.empty()) { + auto parents = + sourceModel() + ->data(sourceModel()->index(sourceRow, 0), RoomlistModel::ParentSpaces) + .toStringList(); + for (const auto &t : parents) + if (hiddenSpaces.contains(t)) + return false; + } + return true; } else if (filterType == FilterBy::Tag) { + if (sourceModel() + ->data(sourceModel()->index(sourceRow, 0), RoomlistModel::IsSpace) + .toBool()) { + return false; + } + auto tags = sourceModel() ->data(sourceModel()->index(sourceRow, 0), RoomlistModel::Tags) .toStringList(); if (!tags.contains(filterStr)) return false; - else if (!hiddenTags.empty()) { + + if (!hiddenTags.empty()) { for (const auto &t : tags) if (t != filterStr && hiddenTags.contains(t)) return false; } + + if (!hiddenSpaces.empty()) { + auto parents = + sourceModel() + ->data(sourceModel()->index(sourceRow, 0), RoomlistModel::ParentSpaces) + .toStringList(); + for (const auto &t : parents) + if (hiddenSpaces.contains(t)) + return false; + } + + return true; + } else if (filterType == FilterBy::Space) { + if (filterStr == sourceModel() + ->data(sourceModel()->index(sourceRow, 0), RoomlistModel::RoomId) + .toString()) + return true; + + auto parents = + sourceModel() + ->data(sourceModel()->index(sourceRow, 0), RoomlistModel::ParentSpaces) + .toStringList(); + + if (!parents.contains(filterStr)) + return false; + + if (!hiddenTags.empty()) { + auto tags = + sourceModel() + ->data(sourceModel()->index(sourceRow, 0), RoomlistModel::Tags) + .toStringList(); + + for (const auto &t : tags) + if (hiddenTags.contains(t)) + return false; + } + + if (!hiddenSpaces.empty()) { + for (const auto &t : parents) + if (hiddenSpaces.contains(t)) + return false; + } + + if (sourceModel() + ->data(sourceModel()->index(sourceRow, 0), RoomlistModel::IsSpace) + .toBool() && + !parents.contains(filterStr)) { + return false; + } + return true; } else { return true; @@ -582,7 +681,7 @@ FilteredRoomlistModel::previousRoom() if (r) { int idx = roomidToIndex(r->roomId()); idx--; - if (idx > 0) { + if (idx >= 0) { setCurrentRoom( data(index(idx, 0), RoomlistModel::Roles::RoomId).toString()); } diff --git a/src/timeline/RoomlistModel.h b/src/timeline/RoomlistModel.h index b024488611ace19ecd334209fe9807d2633326c9..d6cbb462b570eda16c360a70658be8f2493ba2ca 100644 --- a/src/timeline/RoomlistModel.h +++ b/src/timeline/RoomlistModel.h @@ -38,6 +38,7 @@ public: IsInvite, IsSpace, Tags, + ParentSpaces, }; RoomlistModel(TimelineViewManager *parent = nullptr); @@ -134,6 +135,9 @@ public slots: if (tagId.startsWith("tag:")) { filterType = FilterBy::Tag; filterStr = tagId.mid(4); + } else if (tagId.startsWith("space:")) { + filterType = FilterBy::Space; + filterStr = tagId.mid(6); } else { filterType = FilterBy::Nothing; filterStr.clear(); diff --git a/src/timeline/TimelineModel.cpp b/src/timeline/TimelineModel.cpp index 99547b15b496acb69d6b333e7f1e2603472dd58b..067f219aaa86fefd9ce76344bdf628a2166c5ef9 100644 --- a/src/timeline/TimelineModel.cpp +++ b/src/timeline/TimelineModel.cpp @@ -320,6 +320,10 @@ TimelineModel::TimelineModel(TimelineViewManager *manager, QString room_id, QObj { lastMessage_.timestamp = 0; + if (auto create = + cache::client()->getStateEvent<mtx::events::state::Create>(room_id.toStdString())) + this->isSpace_ = create->content.type == mtx::events::state::room_type::space; + connect( this, &TimelineModel::redactionFailed, @@ -375,6 +379,7 @@ TimelineModel::TimelineModel(TimelineViewManager *manager, QString room_id, QObj connect(&events, &EventStore::updateFlowEventId, this, [this](std::string event_id) { this->updateFlowEventId(event_id); }); + // When a message is sent, check if the current edit/reply relates to that message, // and update the event_id so that it points to the sent message and not the pending one. connect(&events, @@ -391,6 +396,11 @@ TimelineModel::TimelineModel(TimelineViewManager *manager, QString room_id, QObj } }); + connect(manager_, + &TimelineViewManager::initialSyncChanged, + &events, + &EventStore::enableKeyRequests); + showEventTimer.callOnTimeout(this, &TimelineModel::scrollTimerEvent); } @@ -770,6 +780,7 @@ TimelineModel::syncState(const mtx::responses::State &s) } else if (std::holds_alternative<StateEvent<state::Member>>(e)) { emit roomAvatarUrlChanged(); emit roomNameChanged(); + emit roomMemberCountChanged(); } } } @@ -826,6 +837,7 @@ TimelineModel::addEvents(const mtx::responses::Timeline &timeline) } else if (std::holds_alternative<StateEvent<state::Member>>(e)) { emit roomAvatarUrlChanged(); emit roomNameChanged(); + emit roomMemberCountChanged(); } } updateLastMessage(); @@ -1931,3 +1943,9 @@ TimelineModel::roomTopic() const return utils::replaceEmoji(utils::linkifyMessage( QString::fromStdString(info[room_id_].topic).toHtmlEscaped())); } + +int +TimelineModel::roomMemberCount() const +{ + return (int)cache::client()->memberCount(room_id_.toStdString()); +} diff --git a/src/timeline/TimelineModel.h b/src/timeline/TimelineModel.h index 3ebbe120db3de93a0ddb68df065acc27a3dd04a1..3392d4746f4e7b98b4d5e34e9e00acfb858a91cb 100644 --- a/src/timeline/TimelineModel.h +++ b/src/timeline/TimelineModel.h @@ -161,6 +161,8 @@ class TimelineModel : public QAbstractListModel Q_PROPERTY(QString roomName READ roomName NOTIFY roomNameChanged) Q_PROPERTY(QString roomAvatarUrl READ roomAvatarUrl NOTIFY roomAvatarUrlChanged) Q_PROPERTY(QString roomTopic READ roomTopic NOTIFY roomTopicChanged) + Q_PROPERTY(int roomMemberCount READ roomMemberCount NOTIFY roomMemberCountChanged) + Q_PROPERTY(bool isSpace READ isSpace CONSTANT) Q_PROPERTY(InputBar *input READ input CONSTANT) Q_PROPERTY(Permissions *permissions READ permissions NOTIFY permissionsChanged) @@ -262,6 +264,8 @@ public: RelatedInfo relatedInfo(QString id); DescInfo lastMessage() const { return lastMessage_; } + bool isSpace() const { return isSpace_; } + int roomMemberCount() const; public slots: void setCurrentIndex(int index); @@ -348,6 +352,7 @@ signals: void roomNameChanged(); void roomTopicChanged(); void roomAvatarUrlChanged(); + void roomMemberCountChanged(); void permissionsChanged(); void forwardToRoom(mtx::events::collections::TimelineEvents *e, QString roomId); @@ -366,9 +371,6 @@ private: QString room_id_; - bool decryptDescription = true; - bool m_paginationInProgress = false; - QString currentId, currentReadId; QString reply_, edit_; QString textBeforeEdit, replyBeforeEdit; @@ -388,6 +390,10 @@ private: friend struct SendMessageVisitor; int notification_count = 0, highlight_count = 0; + + bool decryptDescription = true; + bool m_paginationInProgress = false; + bool isSpace_ = false; }; template<class T> diff --git a/src/ui/Theme.cpp b/src/ui/Theme.cpp index 261193936ae1b04350ef1e050d0bdb436bc9cbd2..732a044385ee4f9629c701dfdb10793019fa2308 100644 --- a/src/ui/Theme.cpp +++ b/src/ui/Theme.cpp @@ -47,7 +47,7 @@ Theme::paletteFromTheme(std::string_view theme) darkActive.setColor(QPalette::ToolTipBase, darkActive.base().color()); darkActive.setColor(QPalette::ToolTipText, darkActive.text().color()); darkActive.setColor(QPalette::Link, QColor("#38a3d8")); - darkActive.setColor(QPalette::ButtonText, "#727274"); + darkActive.setColor(QPalette::ButtonText, "#828284"); return darkActive; } else { return original; diff --git a/third_party/SingleApplication-3.3.0/.github/FUNDING.yml b/third_party/SingleApplication-3.3.0/.github/FUNDING.yml new file mode 100644 index 0000000000000000000000000000000000000000..3ca4d97aa50ff5f95126eb11d2ca3d24215e1a37 --- /dev/null +++ b/third_party/SingleApplication-3.3.0/.github/FUNDING.yml @@ -0,0 +1 @@ +github: itay-grudev diff --git a/third_party/SingleApplication-3.3.0/.github/workflows/build-cmake.yml b/third_party/SingleApplication-3.3.0/.github/workflows/build-cmake.yml new file mode 100644 index 0000000000000000000000000000000000000000..6344b504842201eae4d28f253613b6000b387e43 --- /dev/null +++ b/third_party/SingleApplication-3.3.0/.github/workflows/build-cmake.yml @@ -0,0 +1,56 @@ +name: "CI: Build Test" + +on: [push, pull_request] + +jobs: + build: + + strategy: + matrix: + qt_version: [5.12.6, 5.13.2, 5.14.0, 5.15.0, 6.0.0] + platform: [ubuntu-20.04, windows-latest, macos-latest] + include: + - qt_version: 6.0.0 + additional_arguments: -D QT_DEFAULT_MAJOR_VERSION=6 + - platform: ubuntu-20.04 + CXXFLAGS: -Wall -Wextra -pedantic -Werror + - platform: macos-latest + CXXFLAGS: -Wall -Wextra -pedantic -Werror + - platform: windows-latest + CXXFLAGS: /W4 /WX + + runs-on: ${{ matrix.platform }} + env: + CXXFLAGS: ${{ matrix.CXXFLAGS }} + + steps: + - uses: actions/checkout@v2.3.4 + + - name: Install Qt + uses: jurplel/install-qt-action@v2.11.1 + with: + version: ${{ matrix.qt_version }} + + - name: cmake + run: cmake . ${{ matrix.additional_arguments }} + + - name: cmake build + run: cmake --build . + + - name: Build example - basic (cmake) + working-directory: examples/basic/ + run: | + cmake . ${{ matrix.additional_arguments }} + cmake --build . + + - name: Build example - calculator (cmake) + working-directory: examples/calculator/ + run: | + cmake . ${{ matrix.additional_arguments }} + cmake --build . + + - name: Build example - sending_arguments (cmake) + working-directory: examples/sending_arguments/ + run: | + cmake . ${{ matrix.additional_arguments }} + cmake --build . diff --git a/third_party/SingleApplication-3.2.0-dc8042b/.gitignore b/third_party/SingleApplication-3.3.0/.gitignore similarity index 100% rename from third_party/SingleApplication-3.2.0-dc8042b/.gitignore rename to third_party/SingleApplication-3.3.0/.gitignore diff --git a/third_party/SingleApplication-3.2.0-dc8042b/CHANGELOG.md b/third_party/SingleApplication-3.3.0/CHANGELOG.md similarity index 98% rename from third_party/SingleApplication-3.2.0-dc8042b/CHANGELOG.md rename to third_party/SingleApplication-3.3.0/CHANGELOG.md index e2ba290e2a1cdaa30946224ceb7d6ed40ad5a780..51669b90c010c98d968edfac9bfd4ec526b3775d 100644 --- a/third_party/SingleApplication-3.2.0-dc8042b/CHANGELOG.md +++ b/third_party/SingleApplication-3.3.0/CHANGELOG.md @@ -3,6 +3,12 @@ Changelog If by accident I have forgotten to credit someone in the CHANGELOG, email me and I will fix it. + +__3.3.0__ +--------- + +* Fixed message fragmentation issue causing crashes and incorrectly and inconsistently received messages. - _Nils Jeisecke_ + __3.2.0__ --------- diff --git a/third_party/SingleApplication-3.2.0-dc8042b/CMakeLists.txt b/third_party/SingleApplication-3.3.0/CMakeLists.txt similarity index 100% rename from third_party/SingleApplication-3.2.0-dc8042b/CMakeLists.txt rename to third_party/SingleApplication-3.3.0/CMakeLists.txt diff --git a/third_party/SingleApplication-3.2.0-dc8042b/LICENSE b/third_party/SingleApplication-3.3.0/LICENSE similarity index 100% rename from third_party/SingleApplication-3.2.0-dc8042b/LICENSE rename to third_party/SingleApplication-3.3.0/LICENSE diff --git a/third_party/SingleApplication-3.2.0-dc8042b/README.md b/third_party/SingleApplication-3.3.0/README.md similarity index 100% rename from third_party/SingleApplication-3.2.0-dc8042b/README.md rename to third_party/SingleApplication-3.3.0/README.md diff --git a/third_party/SingleApplication-3.2.0-dc8042b/SingleApplication b/third_party/SingleApplication-3.3.0/SingleApplication similarity index 100% rename from third_party/SingleApplication-3.2.0-dc8042b/SingleApplication rename to third_party/SingleApplication-3.3.0/SingleApplication diff --git a/third_party/SingleApplication-3.2.0-dc8042b/Windows.md b/third_party/SingleApplication-3.3.0/Windows.md similarity index 100% rename from third_party/SingleApplication-3.2.0-dc8042b/Windows.md rename to third_party/SingleApplication-3.3.0/Windows.md diff --git a/third_party/SingleApplication-3.3.0/examples/basic/CMakeLists.txt b/third_party/SingleApplication-3.3.0/examples/basic/CMakeLists.txt new file mode 100644 index 0000000000000000000000000000000000000000..c1429230c07e2ed57be763d56b97e0732b029304 --- /dev/null +++ b/third_party/SingleApplication-3.3.0/examples/basic/CMakeLists.txt @@ -0,0 +1,12 @@ +cmake_minimum_required(VERSION 3.7.0) + +project(basic LANGUAGES CXX) + +# SingleApplication base class +set(QAPPLICATION_CLASS QCoreApplication) +add_subdirectory(../.. SingleApplication) + +add_executable(basic main.cpp) + +target_link_libraries(${PROJECT_NAME} SingleApplication::SingleApplication) + diff --git a/third_party/SingleApplication-3.3.0/examples/basic/basic.pro b/third_party/SingleApplication-3.3.0/examples/basic/basic.pro new file mode 100755 index 0000000000000000000000000000000000000000..b7af16cf66d2610f41e3b603910b37deb704a3b7 --- /dev/null +++ b/third_party/SingleApplication-3.3.0/examples/basic/basic.pro @@ -0,0 +1,5 @@ +# Single Application implementation +include(../../singleapplication.pri) +DEFINES += QAPPLICATION_CLASS=QCoreApplication + +SOURCES += main.cpp diff --git a/third_party/SingleApplication-3.3.0/examples/basic/main.cpp b/third_party/SingleApplication-3.3.0/examples/basic/main.cpp new file mode 100755 index 0000000000000000000000000000000000000000..b2092c6db9ec52052471a579bd4ff6cc49976a5f --- /dev/null +++ b/third_party/SingleApplication-3.3.0/examples/basic/main.cpp @@ -0,0 +1,10 @@ +#include <singleapplication.h> + +int main(int argc, char *argv[]) +{ + SingleApplication app( argc, argv ); + + qWarning() << "Started a new instance"; + + return app.exec(); +} diff --git a/third_party/SingleApplication-3.3.0/examples/calculator/CMakeLists.txt b/third_party/SingleApplication-3.3.0/examples/calculator/CMakeLists.txt new file mode 100644 index 0000000000000000000000000000000000000000..82305f0470e700d9494eb04299e41d65f24a4f4b --- /dev/null +++ b/third_party/SingleApplication-3.3.0/examples/calculator/CMakeLists.txt @@ -0,0 +1,21 @@ +cmake_minimum_required(VERSION 3.7.0) + +project(calculator LANGUAGES CXX) + +set(CMAKE_AUTOMOC ON) + +# SingleApplication base class +set(QAPPLICATION_CLASS QApplication) +add_subdirectory(../.. SingleApplication) + +find_package(Qt${QT_DEFAULT_MAJOR_VERSION} COMPONENTS Core REQUIRED) + +add_executable(${PROJECT_NAME} + button.h + calculator.h + button.cpp + calculator.cpp + main.cpp +) + +target_link_libraries(${PROJECT_NAME} SingleApplication::SingleApplication) diff --git a/third_party/SingleApplication-3.3.0/examples/calculator/button.cpp b/third_party/SingleApplication-3.3.0/examples/calculator/button.cpp new file mode 100644 index 0000000000000000000000000000000000000000..d6cca0a07d1e3189b70151a276640ad677080d4d --- /dev/null +++ b/third_party/SingleApplication-3.3.0/examples/calculator/button.cpp @@ -0,0 +1,73 @@ +/**************************************************************************** +** +** Copyright (C) 2016 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the examples of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:BSD$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** BSD License Usage +** Alternatively, you may use this file under the terms of the BSD license +** as follows: +** +** "Redistribution and use in source and binary forms, with or without +** modification, are permitted provided that the following conditions are +** met: +** * Redistributions of source code must retain the above copyright +** notice, this list of conditions and the following disclaimer. +** * Redistributions in binary form must reproduce the above copyright +** notice, this list of conditions and the following disclaimer in +** the documentation and/or other materials provided with the +** distribution. +** * Neither the name of The Qt Company Ltd nor the names of its +** contributors may be used to endorse or promote products derived +** from this software without specific prior written permission. +** +** +** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include <QtWidgets> + +#include "button.h" + +//! [0] +Button::Button(const QString &text, QWidget *parent) + : QToolButton(parent) +{ + setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); + setText(text); +} +//! [0] + +//! [1] +QSize Button::sizeHint() const +//! [1] //! [2] +{ + QSize size = QToolButton::sizeHint(); + size.rheight() += 20; + size.rwidth() = qMax(size.width(), size.height()); + return size; +} +//! [2] diff --git a/third_party/SingleApplication-3.3.0/examples/calculator/button.h b/third_party/SingleApplication-3.3.0/examples/calculator/button.h new file mode 100644 index 0000000000000000000000000000000000000000..2c014c7b91afc1af766999778a4cc069fcf1a245 --- /dev/null +++ b/third_party/SingleApplication-3.3.0/examples/calculator/button.h @@ -0,0 +1,68 @@ +/**************************************************************************** +** +** Copyright (C) 2016 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the examples of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:BSD$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** BSD License Usage +** Alternatively, you may use this file under the terms of the BSD license +** as follows: +** +** "Redistribution and use in source and binary forms, with or without +** modification, are permitted provided that the following conditions are +** met: +** * Redistributions of source code must retain the above copyright +** notice, this list of conditions and the following disclaimer. +** * Redistributions in binary form must reproduce the above copyright +** notice, this list of conditions and the following disclaimer in +** the documentation and/or other materials provided with the +** distribution. +** * Neither the name of The Qt Company Ltd nor the names of its +** contributors may be used to endorse or promote products derived +** from this software without specific prior written permission. +** +** +** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef BUTTON_H +#define BUTTON_H + +#include <QToolButton> + +//! [0] +class Button : public QToolButton +{ + Q_OBJECT + +public: + explicit Button(const QString &text, QWidget *parent = 0); + + QSize sizeHint() const Q_DECL_OVERRIDE; +}; +//! [0] + +#endif diff --git a/third_party/SingleApplication-3.3.0/examples/calculator/calculator.cpp b/third_party/SingleApplication-3.3.0/examples/calculator/calculator.cpp new file mode 100644 index 0000000000000000000000000000000000000000..3d34c2a743d96a438ec6c1b6b4efe63119f7b13b --- /dev/null +++ b/third_party/SingleApplication-3.3.0/examples/calculator/calculator.cpp @@ -0,0 +1,406 @@ +/**************************************************************************** +** +** Copyright (C) 2016 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the examples of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:BSD$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** BSD License Usage +** Alternatively, you may use this file under the terms of the BSD license +** as follows: +** +** "Redistribution and use in source and binary forms, with or without +** modification, are permitted provided that the following conditions are +** met: +** * Redistributions of source code must retain the above copyright +** notice, this list of conditions and the following disclaimer. +** * Redistributions in binary form must reproduce the above copyright +** notice, this list of conditions and the following disclaimer in +** the documentation and/or other materials provided with the +** distribution. +** * Neither the name of The Qt Company Ltd nor the names of its +** contributors may be used to endorse or promote products derived +** from this software without specific prior written permission. +** +** +** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include <QtWidgets> + +#include <cmath> + +#include "button.h" +#include "calculator.h" + +//! [0] +Calculator::Calculator(QWidget *parent) + : QWidget(parent) +{ + sumInMemory = 0.0; + sumSoFar = 0.0; + factorSoFar = 0.0; + waitingForOperand = true; +//! [0] + +//! [1] + display = new QLineEdit("0"); +//! [1] //! [2] + display->setReadOnly(true); + display->setAlignment(Qt::AlignRight); + display->setMaxLength(15); + + QFont font = display->font(); + font.setPointSize(font.pointSize() + 8); + display->setFont(font); +//! [2] + +//! [4] + for (int i = 0; i < NumDigitButtons; ++i) { + digitButtons[i] = createButton(QString::number(i), SLOT(digitClicked())); + } + + Button *pointButton = createButton(".", SLOT(pointClicked())); + Button *changeSignButton = createButton("\302\261", SLOT(changeSignClicked())); + + Button *backspaceButton = createButton("Backspace", SLOT(backspaceClicked())); + Button *clearButton = createButton("Clear", SLOT(clear())); + Button *clearAllButton = createButton("Clear All", SLOT(clearAll())); + + Button *clearMemoryButton = createButton("MC", SLOT(clearMemory())); + Button *readMemoryButton = createButton("MR", SLOT(readMemory())); + Button *setMemoryButton = createButton("MS", SLOT(setMemory())); + Button *addToMemoryButton = createButton("M+", SLOT(addToMemory())); + + Button *divisionButton = createButton("\303\267", SLOT(multiplicativeOperatorClicked())); + Button *timesButton = createButton("\303\227", SLOT(multiplicativeOperatorClicked())); + Button *minusButton = createButton("-", SLOT(additiveOperatorClicked())); + Button *plusButton = createButton("+", SLOT(additiveOperatorClicked())); + + Button *squareRootButton = createButton("Sqrt", SLOT(unaryOperatorClicked())); + Button *powerButton = createButton("x\302\262", SLOT(unaryOperatorClicked())); + Button *reciprocalButton = createButton("1/x", SLOT(unaryOperatorClicked())); + Button *equalButton = createButton("=", SLOT(equalClicked())); +//! [4] + +//! [5] + QGridLayout *mainLayout = new QGridLayout; +//! [5] //! [6] + mainLayout->setSizeConstraint(QLayout::SetFixedSize); + mainLayout->addWidget(display, 0, 0, 1, 6); + mainLayout->addWidget(backspaceButton, 1, 0, 1, 2); + mainLayout->addWidget(clearButton, 1, 2, 1, 2); + mainLayout->addWidget(clearAllButton, 1, 4, 1, 2); + + mainLayout->addWidget(clearMemoryButton, 2, 0); + mainLayout->addWidget(readMemoryButton, 3, 0); + mainLayout->addWidget(setMemoryButton, 4, 0); + mainLayout->addWidget(addToMemoryButton, 5, 0); + + for (int i = 1; i < NumDigitButtons; ++i) { + int row = ((9 - i) / 3) + 2; + int column = ((i - 1) % 3) + 1; + mainLayout->addWidget(digitButtons[i], row, column); + } + + mainLayout->addWidget(digitButtons[0], 5, 1); + mainLayout->addWidget(pointButton, 5, 2); + mainLayout->addWidget(changeSignButton, 5, 3); + + mainLayout->addWidget(divisionButton, 2, 4); + mainLayout->addWidget(timesButton, 3, 4); + mainLayout->addWidget(minusButton, 4, 4); + mainLayout->addWidget(plusButton, 5, 4); + + mainLayout->addWidget(squareRootButton, 2, 5); + mainLayout->addWidget(powerButton, 3, 5); + mainLayout->addWidget(reciprocalButton, 4, 5); + mainLayout->addWidget(equalButton, 5, 5); + setLayout(mainLayout); + + setWindowTitle("Calculator"); +} +//! [6] + +//! [7] +void Calculator::digitClicked() +{ + Button *clickedButton = qobject_cast<Button *>(sender()); + int digitValue = clickedButton->text().toInt(); + if (display->text() == "0" && digitValue == 0.0) + return; + + if (waitingForOperand) { + display->clear(); + waitingForOperand = false; + } + display->setText(display->text() + QString::number(digitValue)); +} +//! [7] + +//! [8] +void Calculator::unaryOperatorClicked() +//! [8] //! [9] +{ + Button *clickedButton = qobject_cast<Button *>(sender()); + QString clickedOperator = clickedButton->text(); + double operand = display->text().toDouble(); + double result = 0.0; + + if (clickedOperator == "Sqrt") { + if (operand < 0.0) { + abortOperation(); + return; + } + result = std::sqrt(operand); + } else if (clickedOperator == "x\302\262") { + result = std::pow(operand, 2.0); + } else if (clickedOperator == "1/x") { + if (operand == 0.0) { + abortOperation(); + return; + } + result = 1.0 / operand; + } + display->setText(QString::number(result)); + waitingForOperand = true; +} +//! [9] + +//! [10] +void Calculator::additiveOperatorClicked() +//! [10] //! [11] +{ + Button *clickedButton = qobject_cast<Button *>(sender()); + QString clickedOperator = clickedButton->text(); + double operand = display->text().toDouble(); + +//! [11] //! [12] + if (!pendingMultiplicativeOperator.isEmpty()) { +//! [12] //! [13] + if (!calculate(operand, pendingMultiplicativeOperator)) { + abortOperation(); + return; + } + display->setText(QString::number(factorSoFar)); + operand = factorSoFar; + factorSoFar = 0.0; + pendingMultiplicativeOperator.clear(); + } + +//! [13] //! [14] + if (!pendingAdditiveOperator.isEmpty()) { +//! [14] //! [15] + if (!calculate(operand, pendingAdditiveOperator)) { + abortOperation(); + return; + } + display->setText(QString::number(sumSoFar)); + } else { + sumSoFar = operand; + } + +//! [15] //! [16] + pendingAdditiveOperator = clickedOperator; +//! [16] //! [17] + waitingForOperand = true; +} +//! [17] + +//! [18] +void Calculator::multiplicativeOperatorClicked() +{ + Button *clickedButton = qobject_cast<Button *>(sender()); + QString clickedOperator = clickedButton->text(); + double operand = display->text().toDouble(); + + if (!pendingMultiplicativeOperator.isEmpty()) { + if (!calculate(operand, pendingMultiplicativeOperator)) { + abortOperation(); + return; + } + display->setText(QString::number(factorSoFar)); + } else { + factorSoFar = operand; + } + + pendingMultiplicativeOperator = clickedOperator; + waitingForOperand = true; +} +//! [18] + +//! [20] +void Calculator::equalClicked() +{ + double operand = display->text().toDouble(); + + if (!pendingMultiplicativeOperator.isEmpty()) { + if (!calculate(operand, pendingMultiplicativeOperator)) { + abortOperation(); + return; + } + operand = factorSoFar; + factorSoFar = 0.0; + pendingMultiplicativeOperator.clear(); + } + if (!pendingAdditiveOperator.isEmpty()) { + if (!calculate(operand, pendingAdditiveOperator)) { + abortOperation(); + return; + } + pendingAdditiveOperator.clear(); + } else { + sumSoFar = operand; + } + + display->setText(QString::number(sumSoFar)); + sumSoFar = 0.0; + waitingForOperand = true; +} +//! [20] + +//! [22] +void Calculator::pointClicked() +{ + if (waitingForOperand) + display->setText("0"); + if (!display->text().contains('.')) + display->setText(display->text() + "."); + waitingForOperand = false; +} +//! [22] + +//! [24] +void Calculator::changeSignClicked() +{ + QString text = display->text(); + double value = text.toDouble(); + + if (value > 0.0) { + text.prepend("-"); + } else if (value < 0.0) { + text.remove(0, 1); + } + display->setText(text); +} +//! [24] + +//! [26] +void Calculator::backspaceClicked() +{ + if (waitingForOperand) + return; + + QString text = display->text(); + text.chop(1); + if (text.isEmpty()) { + text = "0"; + waitingForOperand = true; + } + display->setText(text); +} +//! [26] + +//! [28] +void Calculator::clear() +{ + if (waitingForOperand) + return; + + display->setText("0"); + waitingForOperand = true; +} +//! [28] + +//! [30] +void Calculator::clearAll() +{ + sumSoFar = 0.0; + factorSoFar = 0.0; + pendingAdditiveOperator.clear(); + pendingMultiplicativeOperator.clear(); + display->setText("0"); + waitingForOperand = true; +} +//! [30] + +//! [32] +void Calculator::clearMemory() +{ + sumInMemory = 0.0; +} + +void Calculator::readMemory() +{ + display->setText(QString::number(sumInMemory)); + waitingForOperand = true; +} + +void Calculator::setMemory() +{ + equalClicked(); + sumInMemory = display->text().toDouble(); +} + +void Calculator::addToMemory() +{ + equalClicked(); + sumInMemory += display->text().toDouble(); +} +//! [32] +//! [34] +Button *Calculator::createButton(const QString &text, const char *member) +{ + Button *button = new Button(text); + connect(button, SIGNAL(clicked()), this, member); + return button; +} +//! [34] + +//! [36] +void Calculator::abortOperation() +{ + clearAll(); + display->setText("####"); +} +//! [36] + +//! [38] +bool Calculator::calculate(double rightOperand, const QString &pendingOperator) +{ + if (pendingOperator == "+") { + sumSoFar += rightOperand; + } else if (pendingOperator == "-") { + sumSoFar -= rightOperand; + } else if (pendingOperator == "\303\227") { + factorSoFar *= rightOperand; + } else if (pendingOperator == "\303\267") { + if (rightOperand == 0.0) + return false; + factorSoFar /= rightOperand; + } + return true; +} +//! [38] diff --git a/third_party/SingleApplication-3.3.0/examples/calculator/calculator.h b/third_party/SingleApplication-3.3.0/examples/calculator/calculator.h new file mode 100644 index 0000000000000000000000000000000000000000..250a2f3e77bbd4851cf86348e24397eee55ff257 --- /dev/null +++ b/third_party/SingleApplication-3.3.0/examples/calculator/calculator.h @@ -0,0 +1,117 @@ +/**************************************************************************** +** +** Copyright (C) 2016 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the examples of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:BSD$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** BSD License Usage +** Alternatively, you may use this file under the terms of the BSD license +** as follows: +** +** "Redistribution and use in source and binary forms, with or without +** modification, are permitted provided that the following conditions are +** met: +** * Redistributions of source code must retain the above copyright +** notice, this list of conditions and the following disclaimer. +** * Redistributions in binary form must reproduce the above copyright +** notice, this list of conditions and the following disclaimer in +** the documentation and/or other materials provided with the +** distribution. +** * Neither the name of The Qt Company Ltd nor the names of its +** contributors may be used to endorse or promote products derived +** from this software without specific prior written permission. +** +** +** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef CALCULATOR_H +#define CALCULATOR_H + +#include <QWidget> + +QT_BEGIN_NAMESPACE +class QLineEdit; +QT_END_NAMESPACE +class Button; + +//! [0] +class Calculator : public QWidget +{ + Q_OBJECT + +public: + Calculator(QWidget *parent = 0); + +private slots: + void digitClicked(); + void unaryOperatorClicked(); + void additiveOperatorClicked(); + void multiplicativeOperatorClicked(); + void equalClicked(); + void pointClicked(); + void changeSignClicked(); + void backspaceClicked(); + void clear(); + void clearAll(); + void clearMemory(); + void readMemory(); + void setMemory(); + void addToMemory(); +//! [0] + +//! [1] +private: +//! [1] //! [2] + Button *createButton(const QString &text, const char *member); + void abortOperation(); + bool calculate(double rightOperand, const QString &pendingOperator); +//! [2] + +//! [3] + double sumInMemory; +//! [3] //! [4] + double sumSoFar; +//! [4] //! [5] + double factorSoFar; +//! [5] //! [6] + QString pendingAdditiveOperator; +//! [6] //! [7] + QString pendingMultiplicativeOperator; +//! [7] //! [8] + bool waitingForOperand; +//! [8] + +//! [9] + QLineEdit *display; +//! [9] //! [10] + + enum { NumDigitButtons = 10 }; + Button *digitButtons[NumDigitButtons]; +}; +//! [10] + +#endif diff --git a/third_party/SingleApplication-3.3.0/examples/calculator/calculator.pro b/third_party/SingleApplication-3.3.0/examples/calculator/calculator.pro new file mode 100644 index 0000000000000000000000000000000000000000..8f132609c547f9cbf46fb7cd1276f613eabb5a7c --- /dev/null +++ b/third_party/SingleApplication-3.3.0/examples/calculator/calculator.pro @@ -0,0 +1,11 @@ +QT += widgets + +HEADERS = button.h \ + calculator.h +SOURCES = button.cpp \ + calculator.cpp \ + main.cpp + +# Single Application implementation +include(../../singleapplication.pri) +DEFINES += QAPPLICATION_CLASS=QApplication diff --git a/third_party/SingleApplication-3.3.0/examples/calculator/main.cpp b/third_party/SingleApplication-3.3.0/examples/calculator/main.cpp new file mode 100644 index 0000000000000000000000000000000000000000..d45438f483028023889f911d007ead25b4831601 --- /dev/null +++ b/third_party/SingleApplication-3.3.0/examples/calculator/main.cpp @@ -0,0 +1,71 @@ +/**************************************************************************** +** +** Copyright (C) 2016 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the examples of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:BSD$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** BSD License Usage +** Alternatively, you may use this file under the terms of the BSD license +** as follows: +** +** "Redistribution and use in source and binary forms, with or without +** modification, are permitted provided that the following conditions are +** met: +** * Redistributions of source code must retain the above copyright +** notice, this list of conditions and the following disclaimer. +** * Redistributions in binary form must reproduce the above copyright +** notice, this list of conditions and the following disclaimer in +** the documentation and/or other materials provided with the +** distribution. +** * Neither the name of The Qt Company Ltd nor the names of its +** contributors may be used to endorse or promote products derived +** from this software without specific prior written permission. +** +** +** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include <QApplication> + +#include <singleapplication.h> + +#include "calculator.h" + +int main(int argc, char *argv[]) +{ + SingleApplication app(argc, argv); + + Calculator calc; + + QObject::connect( &app, &SingleApplication::instanceStarted, [ &calc ]() { + calc.raise(); + calc.activateWindow(); + }); + + calc.show(); + + return app.exec(); +} diff --git a/third_party/SingleApplication-3.3.0/examples/sending_arguments/CMakeLists.txt b/third_party/SingleApplication-3.3.0/examples/sending_arguments/CMakeLists.txt new file mode 100644 index 0000000000000000000000000000000000000000..2cc5597572f79a3e63dc5ebdf069d8de0a9e7f96 --- /dev/null +++ b/third_party/SingleApplication-3.3.0/examples/sending_arguments/CMakeLists.txt @@ -0,0 +1,20 @@ +cmake_minimum_required(VERSION 3.7.0) + +project(sending_arguments LANGUAGES CXX) + +set(CMAKE_AUTOMOC ON) + +# SingleApplication base class +set(QAPPLICATION_CLASS QCoreApplication) +add_subdirectory(../.. SingleApplication) + +find_package(Qt${QT_DEFAULT_MAJOR_VERSION} COMPONENTS Core REQUIRED) + +add_executable(${PROJECT_NAME} + main.cpp + messagereceiver.cpp + messagereceiver.h + main.cpp +) + +target_link_libraries(${PROJECT_NAME} SingleApplication::SingleApplication) diff --git a/third_party/SingleApplication-3.3.0/examples/sending_arguments/main.cpp b/third_party/SingleApplication-3.3.0/examples/sending_arguments/main.cpp new file mode 100755 index 0000000000000000000000000000000000000000..a9d34dd97ad671d96e3cef454c84bebe0a1c97fb --- /dev/null +++ b/third_party/SingleApplication-3.3.0/examples/sending_arguments/main.cpp @@ -0,0 +1,28 @@ +#include <singleapplication.h> +#include "messagereceiver.h" + +int main(int argc, char *argv[]) +{ + // Allow secondary instances + SingleApplication app( argc, argv, true ); + + MessageReceiver msgReceiver; + + // If this is a secondary instance + if( app.isSecondary() ) { + app.sendMessage( app.arguments().join(' ').toUtf8() ); + qDebug() << "App already running."; + qDebug() << "Primary instance PID: " << app.primaryPid(); + qDebug() << "Primary instance user: " << app.primaryUser(); + return 0; + } else { + QObject::connect( + &app, + &SingleApplication::receivedMessage, + &msgReceiver, + &MessageReceiver::receivedMessage + ); + } + + return app.exec(); +} diff --git a/third_party/SingleApplication-3.3.0/examples/sending_arguments/messagereceiver.cpp b/third_party/SingleApplication-3.3.0/examples/sending_arguments/messagereceiver.cpp new file mode 100644 index 0000000000000000000000000000000000000000..0560b0726b536b179c83386c29b80864f58a29ce --- /dev/null +++ b/third_party/SingleApplication-3.3.0/examples/sending_arguments/messagereceiver.cpp @@ -0,0 +1,12 @@ +#include <QDebug> +#include "messagereceiver.h" + +MessageReceiver::MessageReceiver(QObject *parent) : QObject(parent) +{ +} + +void MessageReceiver::receivedMessage(int instanceId, QByteArray message) +{ + qDebug() << "Received message from instance: " << instanceId; + qDebug() << "Message Text: " << message; +} diff --git a/third_party/SingleApplication-3.3.0/examples/sending_arguments/messagereceiver.h b/third_party/SingleApplication-3.3.0/examples/sending_arguments/messagereceiver.h new file mode 100644 index 0000000000000000000000000000000000000000..50a970c8fbd6811f00321ff2608e3ae316a95cd7 --- /dev/null +++ b/third_party/SingleApplication-3.3.0/examples/sending_arguments/messagereceiver.h @@ -0,0 +1,15 @@ +#ifndef MESSAGERECEIVER_H +#define MESSAGERECEIVER_H + +#include <QObject> + +class MessageReceiver : public QObject +{ + Q_OBJECT +public: + explicit MessageReceiver(QObject *parent = 0); +public slots: + void receivedMessage( int instanceId, QByteArray message ); +}; + +#endif // MESSAGERECEIVER_H diff --git a/third_party/SingleApplication-3.3.0/examples/sending_arguments/sending_arguments.pro b/third_party/SingleApplication-3.3.0/examples/sending_arguments/sending_arguments.pro new file mode 100755 index 0000000000000000000000000000000000000000..897636a9b64656acd81f333f0d258fa5c48da36e --- /dev/null +++ b/third_party/SingleApplication-3.3.0/examples/sending_arguments/sending_arguments.pro @@ -0,0 +1,9 @@ +# Single Application implementation +include(../../singleapplication.pri) +DEFINES += QAPPLICATION_CLASS=QCoreApplication + +SOURCES += main.cpp \ + messagereceiver.cpp + +HEADERS += \ + messagereceiver.h diff --git a/third_party/SingleApplication-3.2.0-dc8042b/singleapplication.cpp b/third_party/SingleApplication-3.3.0/singleapplication.cpp similarity index 92% rename from third_party/SingleApplication-3.2.0-dc8042b/singleapplication.cpp rename to third_party/SingleApplication-3.3.0/singleapplication.cpp index 276ceee9247e1a80f369cedb0cc92e346ce578e4..09e264ef51f667ce67efc3db055da82a09a79cba 100644 --- a/third_party/SingleApplication-3.2.0-dc8042b/singleapplication.cpp +++ b/third_party/SingleApplication-3.3.0/singleapplication.cpp @@ -36,7 +36,7 @@ * @param options Optional flags to toggle specific behaviour * @param timeout Maximum time blocking functions are allowed during app load */ -SingleApplication::SingleApplication( int &argc, char *argv[], bool allowSecondary, Options options, int timeout, QString userData ) +SingleApplication::SingleApplication( int &argc, char *argv[], bool allowSecondary, Options options, int timeout, const QString &userData ) : app_t( argc, argv ), d_ptr( new SingleApplicationPrivate( this ) ) { Q_D( SingleApplication ); @@ -172,9 +172,9 @@ SingleApplication::~SingleApplication() * Checks if the current application instance is primary. * @return Returns true if the instance is primary, false otherwise. */ -bool SingleApplication::isPrimary() +bool SingleApplication::isPrimary() const { - Q_D( SingleApplication ); + Q_D( const SingleApplication ); return d->server != nullptr; } @@ -182,9 +182,9 @@ bool SingleApplication::isPrimary() * Checks if the current application instance is secondary. * @return Returns true if the instance is secondary, false otherwise. */ -bool SingleApplication::isSecondary() +bool SingleApplication::isSecondary() const { - Q_D( SingleApplication ); + Q_D( const SingleApplication ); return d->server == nullptr; } @@ -194,9 +194,9 @@ bool SingleApplication::isSecondary() * only incremented afterwards. * @return Returns a unique instance id. */ -quint32 SingleApplication::instanceId() +quint32 SingleApplication::instanceId() const { - Q_D( SingleApplication ); + Q_D( const SingleApplication ); return d->instanceNumber; } @@ -206,9 +206,9 @@ quint32 SingleApplication::instanceId() * specific APIs. * @return Returns the primary instance PID. */ -qint64 SingleApplication::primaryPid() +qint64 SingleApplication::primaryPid() const { - Q_D( SingleApplication ); + Q_D( const SingleApplication ); return d->primaryPid(); } @@ -216,9 +216,9 @@ qint64 SingleApplication::primaryPid() * Returns the username the primary instance is running as. * @return Returns the username the primary instance is running as. */ -QString SingleApplication::primaryUser() +QString SingleApplication::primaryUser() const { - Q_D( SingleApplication ); + Q_D( const SingleApplication ); return d->primaryUser(); } @@ -226,7 +226,7 @@ QString SingleApplication::primaryUser() * Returns the username the current instance is running as. * @return Returns the username the current instance is running as. */ -QString SingleApplication::currentUser() +QString SingleApplication::currentUser() const { return SingleApplicationPrivate::getUsername(); } @@ -248,10 +248,7 @@ bool SingleApplication::sendMessage( const QByteArray &message, int timeout ) if( ! d->connectToPrimary( timeout, SingleApplicationPrivate::Reconnect ) ) return false; - d->socket->write( message ); - bool dataWritten = d->socket->waitForBytesWritten( timeout ); - d->socket->flush(); - return dataWritten; + return d->writeConfirmedMessage( timeout, message ); } /** @@ -267,8 +264,8 @@ void SingleApplication::abortSafely() ::exit( EXIT_FAILURE ); } -QStringList SingleApplication::userData() +QStringList SingleApplication::userData() const { - Q_D( SingleApplication ); + Q_D( const SingleApplication ); return d->appData(); } diff --git a/third_party/SingleApplication-3.2.0-dc8042b/singleapplication.h b/third_party/SingleApplication-3.3.0/singleapplication.h similarity index 95% rename from third_party/SingleApplication-3.2.0-dc8042b/singleapplication.h rename to third_party/SingleApplication-3.3.0/singleapplication.h index d39a661433bcf43c83ea95c236d8ac5787184d24..91cabf93e858cfe444d0905e20e76376a7244417 100644 --- a/third_party/SingleApplication-3.2.0-dc8042b/singleapplication.h +++ b/third_party/SingleApplication-3.3.0/singleapplication.h @@ -85,44 +85,44 @@ public: * Usually 4*timeout would be the worst case (fail) scenario. * @see See the corresponding QAPPLICATION_CLASS constructor for reference */ - explicit SingleApplication( int &argc, char *argv[], bool allowSecondary = false, Options options = Mode::User, int timeout = 1000, QString userData = QString() ); + explicit SingleApplication( int &argc, char *argv[], bool allowSecondary = false, Options options = Mode::User, int timeout = 1000, const QString &userData = {} ); ~SingleApplication() override; /** * @brief Returns if the instance is the primary instance * @returns {bool} */ - bool isPrimary(); + bool isPrimary() const; /** * @brief Returns if the instance is a secondary instance * @returns {bool} */ - bool isSecondary(); + bool isSecondary() const; /** * @brief Returns a unique identifier for the current instance * @returns {qint32} */ - quint32 instanceId(); + quint32 instanceId() const; /** * @brief Returns the process ID (PID) of the primary instance * @returns {qint64} */ - qint64 primaryPid(); + qint64 primaryPid() const; /** * @brief Returns the username of the user running the primary instance * @returns {QString} */ - QString primaryUser(); + QString primaryUser() const; /** * @brief Returns the username of the current user * @returns {QString} */ - QString currentUser(); + QString currentUser() const; /** * @brief Sends a message to the primary instance. Returns true on success. @@ -137,7 +137,7 @@ public: * @brief Get the set user data. * @returns {QStringList} */ - QStringList userData(); + QStringList userData() const; Q_SIGNALS: void instanceStarted(); diff --git a/third_party/SingleApplication-3.2.0-dc8042b/singleapplication.pri b/third_party/SingleApplication-3.3.0/singleapplication.pri similarity index 100% rename from third_party/SingleApplication-3.2.0-dc8042b/singleapplication.pri rename to third_party/SingleApplication-3.3.0/singleapplication.pri diff --git a/third_party/SingleApplication-3.2.0-dc8042b/singleapplication_p.cpp b/third_party/SingleApplication-3.3.0/singleapplication_p.cpp similarity index 85% rename from third_party/SingleApplication-3.2.0-dc8042b/singleapplication_p.cpp rename to third_party/SingleApplication-3.3.0/singleapplication_p.cpp index 1ab58c239f0c0d84b03a14d931b87ef34759cff5..133972825040f003e4de1a39b57c22115bfe01e1 100644 --- a/third_party/SingleApplication-3.2.0-dc8042b/singleapplication_p.cpp +++ b/third_party/SingleApplication-3.3.0/singleapplication_p.cpp @@ -263,20 +263,46 @@ bool SingleApplicationPrivate::connectToPrimary( int msecs, ConnectionType conne #endif writeStream << checksum; - // The header indicates the message length that follows + return writeConfirmedMessage( static_cast<int>(msecs - time.elapsed()), initMsg ); +} + +void SingleApplicationPrivate::writeAck( QLocalSocket *sock ) { + sock->putChar('\n'); +} + +bool SingleApplicationPrivate::writeConfirmedMessage (int msecs, const QByteArray &msg) +{ + QElapsedTimer time; + time.start(); + + // Frame 1: The header indicates the message length that follows QByteArray header; QDataStream headerStream(&header, QIODevice::WriteOnly); #if (QT_VERSION >= QT_VERSION_CHECK(5, 6, 0)) headerStream.setVersion(QDataStream::Qt_5_6); #endif - headerStream << static_cast <quint64>( initMsg.length() ); + headerStream << static_cast <quint64>( msg.length() ); - socket->write( header ); - socket->write( initMsg ); - bool result = socket->waitForBytesWritten( static_cast<int>(msecs - time.elapsed()) ); + if( ! writeConfirmedFrame( static_cast<int>(msecs - time.elapsed()), header )) + return false; + + // Frame 2: The message + return writeConfirmedFrame( static_cast<int>(msecs - time.elapsed()), msg ); +} + +bool SingleApplicationPrivate::writeConfirmedFrame( int msecs, const QByteArray &msg ) +{ + socket->write( msg ); socket->flush(); - return result; + + bool result = socket->waitForReadyRead( msecs ); // await ack byte + if (result) { + socket->read( 1 ); + return true; + } + + return false; } quint16 SingleApplicationPrivate::blockChecksum() const @@ -321,32 +347,36 @@ void SingleApplicationPrivate::slotConnectionEstablished() QLocalSocket *nextConnSocket = server->nextPendingConnection(); connectionMap.insert(nextConnSocket, ConnectionInfo()); - QObject::connect(nextConnSocket, &QLocalSocket::aboutToClose, + QObject::connect(nextConnSocket, &QLocalSocket::aboutToClose, this, [nextConnSocket, this](){ auto &info = connectionMap[nextConnSocket]; - Q_EMIT this->slotClientConnectionClosed( nextConnSocket, info.instanceId ); + this->slotClientConnectionClosed( nextConnSocket, info.instanceId ); } ); - QObject::connect(nextConnSocket, &QLocalSocket::disconnected, + QObject::connect(nextConnSocket, &QLocalSocket::disconnected, nextConnSocket, &QLocalSocket::deleteLater); + + QObject::connect(nextConnSocket, &QLocalSocket::destroyed, this, [nextConnSocket, this](){ connectionMap.remove(nextConnSocket); - nextConnSocket->deleteLater(); } ); - QObject::connect(nextConnSocket, &QLocalSocket::readyRead, + QObject::connect(nextConnSocket, &QLocalSocket::readyRead, this, [nextConnSocket, this](){ auto &info = connectionMap[nextConnSocket]; switch(info.stage){ - case StageHeader: - readInitMessageHeader(nextConnSocket); + case StageInitHeader: + readMessageHeader( nextConnSocket, StageInitBody ); break; - case StageBody: + case StageInitBody: readInitMessageBody(nextConnSocket); break; - case StageConnected: - Q_EMIT this->slotDataAvailable( nextConnSocket, info.instanceId ); + case StageConnectedHeader: + readMessageHeader( nextConnSocket, StageConnectedBody ); + break; + case StageConnectedBody: + this->slotDataAvailable( nextConnSocket, info.instanceId ); break; default: break; @@ -355,7 +385,7 @@ void SingleApplicationPrivate::slotConnectionEstablished() ); } -void SingleApplicationPrivate::readInitMessageHeader( QLocalSocket *sock ) +void SingleApplicationPrivate::readMessageHeader( QLocalSocket *sock, SingleApplicationPrivate::ConnectionStage nextStage ) { if (!connectionMap.contains( sock )){ return; @@ -375,29 +405,35 @@ void SingleApplicationPrivate::readInitMessageHeader( QLocalSocket *sock ) quint64 msgLen = 0; headerStream >> msgLen; ConnectionInfo &info = connectionMap[sock]; - info.stage = StageBody; + info.stage = nextStage; info.msgLen = msgLen; - if ( sock->bytesAvailable() >= (qint64) msgLen ){ - readInitMessageBody( sock ); - } + writeAck( sock ); } -void SingleApplicationPrivate::readInitMessageBody( QLocalSocket *sock ) +bool SingleApplicationPrivate::isFrameComplete( QLocalSocket *sock ) { - Q_Q(SingleApplication); - if (!connectionMap.contains( sock )){ - return; + return false; } ConnectionInfo &info = connectionMap[sock]; if( sock->bytesAvailable() < ( qint64 )info.msgLen ){ - return; + return false; } + return true; +} + +void SingleApplicationPrivate::readInitMessageBody( QLocalSocket *sock ) +{ + Q_Q(SingleApplication); + + if( !isFrameComplete( sock ) ) + return; + // Read the message body - QByteArray msgBytes = sock->read(info.msgLen); + QByteArray msgBytes = sock->readAll(); QDataStream readStream(msgBytes); #if (QT_VERSION >= QT_VERSION_CHECK(5, 6, 0)) @@ -437,8 +473,9 @@ void SingleApplicationPrivate::readInitMessageBody( QLocalSocket *sock ) return; } + ConnectionInfo &info = connectionMap[sock]; info.instanceId = instanceId; - info.stage = StageConnected; + info.stage = StageConnectedHeader; if( connectionType == NewInstance || ( connectionType == SecondaryInstance && @@ -447,21 +484,28 @@ void SingleApplicationPrivate::readInitMessageBody( QLocalSocket *sock ) Q_EMIT q->instanceStarted(); } - if (sock->bytesAvailable() > 0){ - Q_EMIT this->slotDataAvailable( sock, instanceId ); - } + writeAck( sock ); } void SingleApplicationPrivate::slotDataAvailable( QLocalSocket *dataSocket, quint32 instanceId ) { Q_Q(SingleApplication); + + if ( !isFrameComplete( dataSocket ) ) + return; + Q_EMIT q->receivedMessage( instanceId, dataSocket->readAll() ); + + writeAck( dataSocket ); + + ConnectionInfo &info = connectionMap[dataSocket]; + info.stage = StageConnectedHeader; } void SingleApplicationPrivate::slotClientConnectionClosed( QLocalSocket *closedSocket, quint32 instanceId ) { if( closedSocket->bytesAvailable() > 0 ) - Q_EMIT slotDataAvailable( closedSocket, instanceId ); + slotDataAvailable( closedSocket, instanceId ); } void SingleApplicationPrivate::randomSleep() @@ -470,7 +514,7 @@ void SingleApplicationPrivate::randomSleep() QThread::msleep( QRandomGenerator::global()->bounded( 8u, 18u )); #else qsrand( QDateTime::currentMSecsSinceEpoch() % std::numeric_limits<uint>::max() ); - QThread::msleep( 8 + static_cast <unsigned long>( static_cast <float>( qrand() ) / RAND_MAX * 10 )); + QThread::msleep( qrand() % 11 + 8); #endif } diff --git a/third_party/SingleApplication-3.2.0-dc8042b/singleapplication_p.h b/third_party/SingleApplication-3.3.0/singleapplication_p.h similarity index 88% rename from third_party/SingleApplication-3.2.0-dc8042b/singleapplication_p.h rename to third_party/SingleApplication-3.3.0/singleapplication_p.h index c49a46ddce4511e7e964e4c0dc3661bfb15e1b4c..58507cf3ea647e29de6adf3d61e490d2d676e747 100644 --- a/third_party/SingleApplication-3.2.0-dc8042b/singleapplication_p.h +++ b/third_party/SingleApplication-3.3.0/singleapplication_p.h @@ -61,9 +61,10 @@ public: Reconnect = 3 }; enum ConnectionStage : quint8 { - StageHeader = 0, - StageBody = 1, - StageConnected = 2, + StageInitHeader = 0, + StageInitBody = 1, + StageConnectedHeader = 2, + StageConnectedBody = 3, }; Q_DECLARE_PUBLIC(SingleApplication) @@ -79,8 +80,12 @@ public: quint16 blockChecksum() const; qint64 primaryPid() const; QString primaryUser() const; - void readInitMessageHeader(QLocalSocket *socket); + bool isFrameComplete(QLocalSocket *sock); + void readMessageHeader(QLocalSocket *socket, ConnectionStage nextStage); void readInitMessageBody(QLocalSocket *socket); + void writeAck(QLocalSocket *sock); + bool writeConfirmedFrame(int msecs, const QByteArray &msg); + bool writeConfirmedMessage(int msecs, const QByteArray &msg); static void randomSleep(); void addAppData(const QString &data); QStringList appData() const;