diff options
-rw-r--r-- | src/components/channels.cpp | 28 | ||||
-rw-r--r-- | src/components/channels.hpp | 3 | ||||
-rw-r--r-- | src/components/channelscellrenderer.cpp | 8 | ||||
-rw-r--r-- | src/components/channelscellrenderer.hpp | 7 | ||||
-rw-r--r-- | src/components/unreadrenderer.cpp | 15 | ||||
-rw-r--r-- | src/components/unreadrenderer.hpp | 9 | ||||
-rw-r--r-- | src/discord/discord.cpp | 57 | ||||
-rw-r--r-- | src/discord/discord.hpp | 11 | ||||
-rw-r--r-- | src/discord/objects.cpp | 19 | ||||
-rw-r--r-- | src/discord/objects.hpp | 27 |
10 files changed, 174 insertions, 10 deletions
diff --git a/src/components/channels.cpp b/src/components/channels.cpp index 455d3b1..f61abd2 100644 --- a/src/components/channels.cpp +++ b/src/components/channels.cpp @@ -1,21 +1,21 @@ #include "channels.hpp" -#include <algorithm> -#include <map> -#include <unordered_map> #include "abaddon.hpp" #include "imgmanager.hpp" #include "util.hpp" #include "statusindicator.hpp" +#include <algorithm> +#include <map> +#include <unordered_map> ChannelList::ChannelList() : Glib::ObjectBase(typeid(ChannelList)) - , Gtk::ScrolledWindow() , m_model(Gtk::TreeStore::create(m_columns)) , m_menu_guild_copy_id("_Copy ID", true) , m_menu_guild_settings("View _Settings", true) , m_menu_guild_leave("_Leave", true) , m_menu_category_copy_id("_Copy ID", true) , m_menu_channel_copy_id("_Copy ID", true) + , m_menu_channel_mark_as_read("Mark as _Read", true) , m_menu_dm_copy_id("_Copy ID", true) , m_menu_dm_close("") // changes depending on if group or not , m_menu_thread_copy_id("_Copy ID", true) @@ -24,6 +24,7 @@ ChannelList::ChannelList() , m_menu_thread_unarchive("_Unarchive", true) { get_style_context()->add_class("channel-list"); + // todo: move to method const auto cb = [this](const Gtk::TreeModel::Path &path, Gtk::TreeViewColumn *column) { auto row = *m_model->get_iter(path); const auto type = row[m_columns.m_type]; @@ -40,7 +41,9 @@ ChannelList::ChannelList() } if (type == RenderType::TextChannel || type == RenderType::DM || type == RenderType::Thread) { - m_signal_action_channel_item_select.emit(static_cast<Snowflake>(row[m_columns.m_id])); + const auto id = static_cast<Snowflake>(row[m_columns.m_id]); + m_signal_action_channel_item_select.emit(id); + Abaddon::Get().GetDiscordClient().MarkAsRead(id, [](...) {}); } }; m_view.signal_row_activated().connect(cb, false); @@ -77,6 +80,7 @@ ChannelList::ChannelList() column->add_attribute(renderer->property_icon(), m_columns.m_icon); column->add_attribute(renderer->property_icon_animation(), m_columns.m_icon_anim); column->add_attribute(renderer->property_name(), m_columns.m_name); + column->add_attribute(renderer->property_id(), m_columns.m_id); column->add_attribute(renderer->property_expanded(), m_columns.m_expanded); column->add_attribute(renderer->property_nsfw(), m_columns.m_nsfw); m_view.append_column(*column); @@ -98,13 +102,18 @@ ChannelList::ChannelList() m_menu_category_copy_id.signal_activate().connect([this] { Gtk::Clipboard::get()->set_text(std::to_string((*m_model->get_iter(m_path_for_menu))[m_columns.m_id])); }); + m_menu_category.append(m_menu_category_copy_id); m_menu_category.show_all(); m_menu_channel_copy_id.signal_activate().connect([this] { Gtk::Clipboard::get()->set_text(std::to_string((*m_model->get_iter(m_path_for_menu))[m_columns.m_id])); }); + m_menu_channel_mark_as_read.signal_activate().connect([this] { + Abaddon::Get().GetDiscordClient().MarkAsRead(static_cast<Snowflake>((*m_model->get_iter(m_path_for_menu))[m_columns.m_id]), [](...) {}); + }); m_menu_channel.append(m_menu_channel_copy_id); + m_menu_channel.append(m_menu_channel_mark_as_read); m_menu_channel.show_all(); m_menu_dm_copy_id.signal_activate().connect([this] { @@ -159,6 +168,7 @@ ChannelList::ChannelList() discord.signal_added_to_thread().connect(sigc::mem_fun(*this, &ChannelList::OnThreadJoined)); discord.signal_removed_from_thread().connect(sigc::mem_fun(*this, &ChannelList::OnThreadRemoved)); discord.signal_guild_update().connect(sigc::mem_fun(*this, &ChannelList::UpdateGuild)); + discord.signal_message_ack().connect(sigc::mem_fun(*this, &ChannelList::OnMessageAck)); } void ChannelList::UpdateListing() { @@ -658,7 +668,7 @@ void ChannelList::UpdateCreateDMChannel(const ChannelData &dm) { std::optional<UserData> top_recipient; const auto recipients = dm.GetDMRecipients(); - if (recipients.size() > 0) + if (!recipients.empty()) top_recipient = recipients[0]; auto iter = m_model->append(header_row->children()); @@ -682,6 +692,12 @@ void ChannelList::UpdateCreateDMChannel(const ChannelData &dm) { } } +void ChannelList::OnMessageAck(const MessageAckData &data) { + // trick renderer into redrawing + auto iter = GetIteratorForChannelFromID(data.ChannelID); + if (iter) m_model->row_changed(m_model->get_path(iter), iter); +} + void ChannelList::OnMessageCreate(const Message &msg) { const auto channel = Abaddon::Get().GetDiscordClient().GetChannel(msg.ChannelID); if (!channel.has_value()) return; diff --git a/src/components/channels.hpp b/src/components/channels.hpp index 6ab8174..99eff5f 100644 --- a/src/components/channels.hpp +++ b/src/components/channels.hpp @@ -89,6 +89,8 @@ protected: void AddPrivateChannels(); void UpdateCreateDMChannel(const ChannelData &channel); + void OnMessageAck(const MessageAckData &data); + void OnMessageCreate(const Message &msg); Gtk::TreeModel::Path m_path_for_menu; @@ -105,6 +107,7 @@ protected: Gtk::Menu m_menu_channel; Gtk::MenuItem m_menu_channel_copy_id; + Gtk::MenuItem m_menu_channel_mark_as_read; Gtk::Menu m_menu_dm; Gtk::MenuItem m_menu_dm_copy_id; diff --git a/src/components/channelscellrenderer.cpp b/src/components/channelscellrenderer.cpp index 2526753..f4cd33c 100644 --- a/src/components/channelscellrenderer.cpp +++ b/src/components/channelscellrenderer.cpp @@ -1,11 +1,13 @@ #include "channelscellrenderer.hpp" #include "abaddon.hpp" #include <gtkmm.h> +#include "unreadrenderer.hpp" CellRendererChannels::CellRendererChannels() : Glib::ObjectBase(typeid(CellRendererChannels)) , Gtk::CellRenderer() , m_property_type(*this, "render-type") + , m_property_id(*this, "id") , m_property_name(*this, "name") , m_property_pixbuf(*this, "pixbuf") , m_property_pixbuf_animation(*this, "pixbuf-animation") @@ -26,6 +28,10 @@ Glib::PropertyProxy<RenderType> CellRendererChannels::property_type() { return m_property_type.get_proxy(); } +Glib::PropertyProxy<uint64_t> CellRendererChannels::property_id() { + return m_property_id.get_proxy(); +} + Glib::PropertyProxy<Glib::ustring> CellRendererChannels::property_name() { return m_property_name.get_proxy(); } @@ -328,6 +334,8 @@ void CellRendererChannels::render_vfunc_channel(const Cairo::RefPtr<Cairo::Conte // setting property_foreground_rgba() sets this to true which makes non-nsfw cells use the property too which is bad // so unset it m_renderer_text.property_foreground_set() = false; + + UnreadRenderer::RenderUnreadOnChannel(m_property_id.get_value(), cr, background_area, cell_area); } // thread diff --git a/src/components/channelscellrenderer.hpp b/src/components/channelscellrenderer.hpp index ce8da54..4a9d428 100644 --- a/src/components/channelscellrenderer.hpp +++ b/src/components/channelscellrenderer.hpp @@ -3,6 +3,7 @@ #include <gdkmm/pixbufanimation.h> #include <glibmm/property.h> #include <map> +#include "discord/snowflake.hpp" enum class RenderType : uint8_t { Guild, @@ -20,6 +21,7 @@ public: virtual ~CellRendererChannels(); Glib::PropertyProxy<RenderType> property_type(); + Glib::PropertyProxy<uint64_t> property_id(); Glib::PropertyProxy<Glib::ustring> property_name(); Glib::PropertyProxy<Glib::RefPtr<Gdk::Pixbuf>> property_icon(); Glib::PropertyProxy<Glib::RefPtr<Gdk::PixbufAnimation>> property_icon_animation(); @@ -106,8 +108,9 @@ protected: private: Gtk::CellRendererText m_renderer_text; - Glib::Property<RenderType> m_property_type; // all - Glib::Property<Glib::ustring> m_property_name; // all + Glib::Property<RenderType> m_property_type; // all + Glib::Property<Glib::ustring> m_property_name; // all + Glib::Property<uint64_t> m_property_id; Glib::Property<Glib::RefPtr<Gdk::Pixbuf>> m_property_pixbuf; // guild, dm Glib::Property<Glib::RefPtr<Gdk::PixbufAnimation>> m_property_pixbuf_animation; // guild Glib::Property<bool> m_property_expanded; // category diff --git a/src/components/unreadrenderer.cpp b/src/components/unreadrenderer.cpp new file mode 100644 index 0000000..4e508fc --- /dev/null +++ b/src/components/unreadrenderer.cpp @@ -0,0 +1,15 @@ +#include "unreadrenderer.hpp" +#include "abaddon.hpp" + +void UnreadRenderer::RenderUnreadOnChannel(Snowflake id, const Cairo::RefPtr<Cairo::Context> &cr, const Gdk::Rectangle &background_area, const Gdk::Rectangle &cell_area) { + const auto state = Abaddon::Get().GetDiscordClient().GetUnreadStateForChannel(id); + if (state >= 0) { + cr->set_source_rgb(1.0, 1.0, 1.0); + const auto x = cell_area.get_x() + 1; + const auto y = cell_area.get_y(); + const auto w = cell_area.get_width(); + const auto h = cell_area.get_height(); + cr->rectangle(x, y, 3, h); + cr->fill(); + } +} diff --git a/src/components/unreadrenderer.hpp b/src/components/unreadrenderer.hpp new file mode 100644 index 0000000..e333543 --- /dev/null +++ b/src/components/unreadrenderer.hpp @@ -0,0 +1,9 @@ +#pragma once +#include <cairomm/context.h> +#include <gdkmm/rectangle.h> +#include "discord/snowflake.hpp" + +class UnreadRenderer { +public: + static void RenderUnreadOnChannel(Snowflake id, const Cairo::RefPtr<Cairo::Context> &cr, const Gdk::Rectangle &background_area, const Gdk::Rectangle &cell_area); +}; diff --git a/src/discord/discord.cpp b/src/discord/discord.cpp index b678de0..5d888f4 100644 --- a/src/discord/discord.cpp +++ b/src/discord/discord.cpp @@ -1,8 +1,8 @@ #include "discord.hpp" -#include <cassert> -#include <cinttypes> #include "util.hpp" #include "abaddon.hpp" +#include <cassert> +#include <cinttypes> DiscordClient::DiscordClient(bool mem_store) : m_decompress_buf(InflateChunkSize) @@ -874,6 +874,17 @@ void DiscordClient::UnArchiveThread(Snowflake channel_id, sigc::slot<void(Discor }); } +void DiscordClient::MarkAsRead(Snowflake channel_id, sigc::slot<void(DiscordError code)> callback) { + const auto iter = m_last_message_id.find(channel_id); + if (iter == m_last_message_id.end()) return; + m_http.MakePOST("/channels/" + std::to_string(channel_id) + "/messages/" + std::to_string(iter->second) + "/ack", "{\"token\":null}", [this, callback](const http::response_type &response) { + if (CheckCode(response)) + callback(DiscordError::NONE); + else + callback(GetCodeFromResponse(response)); + }); +} + void DiscordClient::FetchPinned(Snowflake id, sigc::slot<void(std::vector<Message>, DiscordError code)> callback) { // return from db if we know the pins have already been requested if (m_channels_pinned_requested.find(id) != m_channels_pinned_requested.end()) { @@ -1060,6 +1071,12 @@ void DiscordClient::SetUserAgent(std::string agent) { m_websocket.SetUserAgent(agent); } +int DiscordClient::GetUnreadStateForChannel(Snowflake id) const noexcept { + const auto iter = m_unread.find(id); + if (iter == m_unread.end()) return -1; // todo: no magic number + return iter->second; +} + PresenceStatus DiscordClient::GetUserStatus(Snowflake id) const { auto it = m_user_to_status.find(id); if (it != m_user_to_status.end()) @@ -1290,6 +1307,9 @@ void DiscordClient::HandleGatewayMessage(std::string str) { case GatewayEvent::THREAD_MEMBER_LIST_UPDATE: { HandleGatewayThreadMemberListUpdate(m); } break; + case GatewayEvent::MESSAGE_ACK: { + HandleGatewayMessageAck(m); + } break; } } break; default: @@ -1411,6 +1431,9 @@ void DiscordClient::HandleGatewayReady(const GatewayMessage &msg) { m_session_id = data.SessionID; m_user_data = data.SelfUser; m_user_settings = data.Settings; + + HandleReadyReadState(data); + m_signal_gateway_ready.emit(); } @@ -1419,6 +1442,7 @@ void DiscordClient::HandleGatewayMessageCreate(const GatewayMessage &msg) { StoreMessageData(data); if (data.GuildID.has_value()) AddUserToGuild(data.Author.ID, *data.GuildID); + m_last_message_id[data.ChannelID] = data.ID; m_signal_message_create.emit(data); } @@ -1778,6 +1802,12 @@ void DiscordClient::HandleGatewayThreadMemberListUpdate(const GatewayMessage &ms m_signal_thread_member_list_update.emit(data); } +void DiscordClient::HandleGatewayMessageAck(const GatewayMessage &msg) { + MessageAckData data = msg.Data; + m_unread.erase(data.ChannelID); + m_signal_message_ack.emit(data); +} + void DiscordClient::HandleGatewayReadySupplemental(const GatewayMessage &msg) { ReadySupplementalData data = msg.Data; for (const auto &p : data.MergedPresences.Friends) { @@ -2105,6 +2135,24 @@ void DiscordClient::StoreMessageData(Message &msg) { StoreMessageData(**msg.ReferencedMessage); } +// some notes for myself +// a read channel is determined by checking if the channel object's last message id is equal to the read state's last message id +// here the absence of an entry in m_unread indicates a read channel and the value is only the mention count since the message doesnt matter +// no entry.id cannot be a guild even though sometimes it looks like it +void DiscordClient::HandleReadyReadState(const ReadyEventData &data) { + for (const auto &guild : data.Guilds) + for (const auto &channel : *guild.Channels) + if (channel.Type == ChannelType::GUILD_TEXT || channel.Type == ChannelType::GUILD_NEWS && channel.LastMessageID.has_value()) + m_last_message_id[channel.ID] = *channel.LastMessageID; + + for (const auto &entry : data.ReadState.Entries) { + const auto it = m_last_message_id.find(entry.ID); + if (it == m_last_message_id.end()) continue; + if (it->second > entry.LastMessageID) + m_unread[entry.ID] = entry.MentionCount; + } +} + void DiscordClient::LoadEventMap() { m_event_map["READY"] = GatewayEvent::READY; m_event_map["MESSAGE_CREATE"] = GatewayEvent::MESSAGE_CREATE; @@ -2147,6 +2195,7 @@ void DiscordClient::LoadEventMap() { m_event_map["THREAD_MEMBER_UPDATE"] = GatewayEvent::THREAD_MEMBER_UPDATE; m_event_map["THREAD_UPDATE"] = GatewayEvent::THREAD_UPDATE; m_event_map["THREAD_MEMBER_LIST_UPDATE"] = GatewayEvent::THREAD_MEMBER_LIST_UPDATE; + m_event_map["MESSAGE_ACK"] = GatewayEvent::MESSAGE_ACK; } DiscordClient::type_signal_gateway_ready DiscordClient::signal_gateway_ready() { @@ -2309,6 +2358,10 @@ DiscordClient::type_signal_thread_member_list_update DiscordClient::signal_threa return m_signal_thread_member_list_update; } +DiscordClient::type_signal_message_ack DiscordClient::signal_message_ack() { + return m_signal_message_ack; +} + DiscordClient::type_signal_added_to_thread DiscordClient::signal_added_to_thread() { return m_signal_added_to_thread; } diff --git a/src/discord/discord.hpp b/src/discord/discord.hpp index 4010977..06aa698 100644 --- a/src/discord/discord.hpp +++ b/src/discord/discord.hpp @@ -138,6 +138,7 @@ public: void LeaveThread(Snowflake channel_id, const std::string &location, sigc::slot<void(DiscordError code)> callback); void ArchiveThread(Snowflake channel_id, sigc::slot<void(DiscordError code)> callback); void UnArchiveThread(Snowflake channel_id, sigc::slot<void(DiscordError code)> callback); + void MarkAsRead(Snowflake channel_id, sigc::slot<void(DiscordError code)> callback); bool CanModifyRole(Snowflake guild_id, Snowflake role_id) const; bool CanModifyRole(Snowflake guild_id, Snowflake role_id, Snowflake user_id) const; @@ -182,6 +183,8 @@ public: void UpdateToken(std::string token); void SetUserAgent(std::string agent); + int GetUnreadStateForChannel(Snowflake id) const noexcept; + PresenceStatus GetUserStatus(Snowflake id) const; std::map<Snowflake, RelationshipType> GetRelationships() const; @@ -244,6 +247,7 @@ private: void HandleGatewayThreadMemberUpdate(const GatewayMessage &msg); void HandleGatewayThreadUpdate(const GatewayMessage &msg); void HandleGatewayThreadMemberListUpdate(const GatewayMessage &msg); + void HandleGatewayMessageAck(const GatewayMessage &msg); void HandleGatewayReadySupplemental(const GatewayMessage &msg); void HandleGatewayReconnect(const GatewayMessage &msg); void HandleGatewayInvalidSession(const GatewayMessage &msg); @@ -259,6 +263,8 @@ private: void StoreMessageData(Message &msg); + void HandleReadyReadState(const ReadyEventData &data); + std::string m_token; void AddUserToGuild(Snowflake user_id, Snowflake guild_id); @@ -269,6 +275,8 @@ private: std::map<Snowflake, RelationshipType> m_user_relationships; std::set<Snowflake> m_joined_threads; std::map<Snowflake, std::vector<Snowflake>> m_thread_members; + std::map<Snowflake, Snowflake> m_last_message_id; + std::unordered_map<Snowflake, int> m_unread; UserData m_user_data; UserSettings m_user_settings; @@ -343,6 +351,7 @@ public: typedef sigc::signal<void, ThreadMembersUpdateData> type_signal_thread_members_update; typedef sigc::signal<void, ThreadUpdateData> type_signal_thread_update; typedef sigc::signal<void, ThreadMemberListUpdateData> type_signal_thread_member_list_update; + typedef sigc::signal<void, MessageAckData> type_signal_message_ack; // not discord dispatch events typedef sigc::signal<void, Snowflake> type_signal_added_to_thread; @@ -393,6 +402,7 @@ public: type_signal_thread_members_update signal_thread_members_update(); type_signal_thread_update signal_thread_update(); type_signal_thread_member_list_update signal_thread_member_list_update(); + type_signal_message_ack signal_message_ack(); type_signal_added_to_thread signal_added_to_thread(); type_signal_removed_from_thread signal_removed_from_thread(); @@ -440,6 +450,7 @@ protected: type_signal_thread_members_update m_signal_thread_members_update; type_signal_thread_update m_signal_thread_update; type_signal_thread_member_list_update m_signal_thread_member_list_update; + type_signal_message_ack m_signal_message_ack; type_signal_removed_from_thread m_signal_removed_from_thread; type_signal_added_to_thread m_signal_added_to_thread; diff --git a/src/discord/objects.cpp b/src/discord/objects.cpp index c6de2ce..88d3f30 100644 --- a/src/discord/objects.cpp +++ b/src/discord/objects.cpp @@ -119,6 +119,18 @@ void to_json(nlohmann::json &j, const UpdateStatusMessage &m) { } } +void from_json(const nlohmann::json &j, ReadStateEntry &m) { + JS_ON("mention_count", m.MentionCount); + JS_ON("last_message_id", m.LastMessageID); + JS_D("id", m.ID); +} + +void from_json(const nlohmann::json &j, ReadStateData &m) { + JS_ON("version", m.Version); + JS_ON("partial", m.IsPartial); + JS_ON("entries", m.Entries); +} + void from_json(const nlohmann::json &j, ReadyEventData &m) { JS_D("v", m.GatewayVersion); JS_D("user", m.SelfUser); @@ -132,6 +144,7 @@ void from_json(const nlohmann::json &j, ReadyEventData &m) { JS_ON("merged_members", m.MergedMembers); JS_O("relationships", m.Relationships); JS_O("guild_join_requests", m.GuildJoinRequests); + JS_O("read_state", m.ReadState); } void from_json(const nlohmann::json &j, MergedPresence &m) { @@ -532,3 +545,9 @@ void to_json(nlohmann::json &j, const ModifyChannelObject &m) { JS_IF("archived", m.Archived); JS_IF("locked", m.Locked); } + +void from_json(const nlohmann::json &j, MessageAckData &m) { + // JS_D("version", m.Version); + JS_D("message_id", m.MessageID); + JS_D("channel_id", m.ChannelID); +} diff --git a/src/discord/objects.hpp b/src/discord/objects.hpp index 7084efb..008fe98 100644 --- a/src/discord/objects.hpp +++ b/src/discord/objects.hpp @@ -78,6 +78,7 @@ enum class GatewayEvent : int { THREAD_MEMBER_UPDATE, THREAD_MEMBERS_UPDATE, THREAD_MEMBER_LIST_UPDATE, + MESSAGE_ACK, }; enum class GatewayCloseCode : uint16_t { @@ -224,6 +225,23 @@ struct UpdateStatusMessage { friend void to_json(nlohmann::json &j, const UpdateStatusMessage &m); }; +struct ReadStateEntry { + int MentionCount; + Snowflake LastMessageID; + Snowflake ID; + // std::string LastPinTimestamp; iso + + friend void from_json(const nlohmann::json &j, ReadStateEntry &m); +}; + +struct ReadStateData { + int Version; + bool IsPartial; + std::vector<ReadStateEntry> Entries; + + friend void from_json(const nlohmann::json &j, ReadStateData &m); +}; + struct ReadyEventData { int GatewayVersion; UserData SelfUser; @@ -239,6 +257,7 @@ struct ReadyEventData { std::optional<std::vector<std::vector<GuildMember>>> MergedMembers; std::optional<std::vector<RelationshipData>> Relationships; std::optional<std::vector<GuildApplicationData>> GuildJoinRequests; + ReadStateData ReadState; // std::vector<Unknown> ConnectedAccounts; // opt // std::map<std::string, Unknown> Consents; // opt // std::vector<Unknown> Experiments; // opt @@ -745,3 +764,11 @@ struct ModifyChannelObject { friend void to_json(nlohmann::json &j, const ModifyChannelObject &m); }; + +struct MessageAckData { + // int Version; // what is this ?!?!?!!? + Snowflake MessageID; + Snowflake ChannelID; + + friend void from_json(const nlohmann::json &j, MessageAckData &m); +}; |