display reactions + click to add/remove

This commit is contained in:
ouwou 2020-12-15 01:51:49 -05:00
parent 0c313ce5e8
commit 2667a4b30d
20 changed files with 622 additions and 201 deletions

View File

@ -35,6 +35,8 @@ Abaddon::Abaddon()
m_discord.signal_channel_update().connect(sigc::mem_fun(*this, &Abaddon::DiscordOnChannelUpdate));
m_discord.signal_channel_create().connect(sigc::mem_fun(*this, &Abaddon::DiscordOnChannelCreate));
m_discord.signal_guild_update().connect(sigc::mem_fun(*this, &Abaddon::DiscordOnGuildUpdate));
m_discord.signal_reaction_add().connect(sigc::mem_fun(*this, &Abaddon::DiscordOnReactionAdd));
m_discord.signal_reaction_remove().connect(sigc::mem_fun(*this, &Abaddon::DiscordOnReactionRemove));
}
Abaddon::~Abaddon() {
@ -98,6 +100,8 @@ int Abaddon::StartGTK() {
m_main_window->GetChatWindow()->signal_action_chat_load_history().connect(sigc::mem_fun(*this, &Abaddon::ActionChatLoadHistory));
m_main_window->GetChatWindow()->signal_action_channel_click().connect(sigc::mem_fun(*this, &Abaddon::ActionChannelOpened));
m_main_window->GetChatWindow()->signal_action_insert_mention().connect(sigc::mem_fun(*this, &Abaddon::ActionInsertMention));
m_main_window->GetChatWindow()->signal_action_reaction_add().connect(sigc::mem_fun(*this, &Abaddon::ActionReactionAdd));
m_main_window->GetChatWindow()->signal_action_reaction_remove().connect(sigc::mem_fun(*this, &Abaddon::ActionReactionRemove));
ActionReloadCSS();
@ -203,6 +207,14 @@ void Abaddon::DiscordOnGuildUpdate(Snowflake guild_id) {
m_main_window->UpdateChannelsUpdateGuild(guild_id);
}
void Abaddon::DiscordOnReactionAdd(Snowflake message_id, const Glib::ustring &param) {
m_main_window->UpdateChatReactionAdd(message_id, param);
}
void Abaddon::DiscordOnReactionRemove(Snowflake message_id, const Glib::ustring &param) {
m_main_window->UpdateChatReactionAdd(message_id, param);
}
const SettingsManager &Abaddon::GetSettings() const {
return m_settings;
}
@ -423,6 +435,14 @@ void Abaddon::ActionSetStatus() {
m_discord.UpdateStatus(status, false, activity);
}
void Abaddon::ActionReactionAdd(Snowflake id, const Glib::ustring &param) {
m_discord.AddReaction(id, param);
}
void Abaddon::ActionReactionRemove(Snowflake id, const Glib::ustring &param) {
m_discord.RemoveReaction(id, param);
}
void Abaddon::ActionReloadCSS() {
try {
Gtk::StyleContext::remove_provider_for_screen(Gdk::Screen::get_default(), m_css_provider);

View File

@ -43,6 +43,8 @@ public:
void ActionKickMember(Snowflake user_id, Snowflake guild_id);
void ActionBanMember(Snowflake user_id, Snowflake guild_id);
void ActionSetStatus();
void ActionReactionAdd(Snowflake id, const Glib::ustring &param);
void ActionReactionRemove(Snowflake id, const Glib::ustring &param);
void ActionReloadCSS();
@ -65,6 +67,8 @@ public:
void DiscordOnChannelUpdate(Snowflake channel_id);
void DiscordOnChannelCreate(Snowflake channel_id);
void DiscordOnGuildUpdate(Snowflake guild_id);
void DiscordOnReactionAdd(Snowflake message_id, const Glib::ustring &param);
void DiscordOnReactionRemove(Snowflake message_id, const Glib::ustring &param);
const SettingsManager &GetSettings() const;

View File

@ -76,6 +76,11 @@ ChatMessageItemContainer *ChatMessageItemContainer::FromMessage(Snowflake id) {
}
}
if (data->Reactions.has_value() && data->Reactions->size() > 0) {
container->m_reactions_component = container->CreateReactionsComponent(&*data);
container->m_main->add(*container->m_reactions_component);
}
container->UpdateAttributes();
return container;
@ -125,6 +130,18 @@ void ChatMessageItemContainer::UpdateImage(std::string url, Glib::RefPtr<Gdk::Pi
}
}
void ChatMessageItemContainer::UpdateReactions() {
if (m_reactions_component != nullptr)
delete m_reactions_component;
const auto data = Abaddon::Get().GetDiscordClient().GetMessage(ID);
if (data->Reactions.has_value() && data->Reactions->size() > 0) {
m_reactions_component = CreateReactionsComponent(&*data);
m_reactions_component->show_all();
m_main->add(*m_reactions_component);
}
}
void ChatMessageItemContainer::UpdateAttributes() {
const auto data = Abaddon::Get().GetDiscordClient().GetMessage(ID);
if (!data.has_value()) return;
@ -395,6 +412,89 @@ Gtk::Widget *ChatMessageItemContainer::CreateStickerComponent(const Sticker &dat
return box;
}
Gtk::Widget *ChatMessageItemContainer::CreateReactionsComponent(const Message *data) {
auto *flow = Gtk::manage(new Gtk::FlowBox);
flow->set_orientation(Gtk::ORIENTATION_HORIZONTAL);
flow->set_min_children_per_line(5);
flow->set_max_children_per_line(20);
flow->set_halign(Gtk::ALIGN_START);
flow->set_hexpand(false);
flow->set_column_spacing(2);
flow->set_selection_mode(Gtk::SELECTION_NONE);
auto &imgr = Abaddon::Get().GetImageManager();
auto &emojis = Abaddon::Get().GetEmojis();
const auto &placeholder = imgr.GetPlaceholder(16);
for (const auto &reaction : *data->Reactions) {
auto *ev = Gtk::manage(new Gtk::EventBox);
auto *box = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL));
box->get_style_context()->add_class("reaction-box");
ev->add(*box);
flow->add(*ev);
bool is_stock = !reaction.Emoji.ID.IsValid();
bool has_reacted = reaction.HasReactedWith;
if (has_reacted)
box->get_style_context()->add_class("reacted");
ev->signal_button_press_event().connect([this, has_reacted, is_stock, reaction](GdkEventButton *event) -> bool {
if (event->type == GDK_BUTTON_PRESS && event->button == GDK_BUTTON_PRIMARY) {
Glib::ustring param; // escaped in client
if (is_stock)
param = reaction.Emoji.Name;
else
param = std::to_string(reaction.Emoji.ID);
if (has_reacted)
m_signal_action_reaction_remove.emit(param);
else
m_signal_action_reaction_add.emit(param);
return true;
}
return false;
});
ev->signal_realize().connect([ev]() {
auto window = ev->get_window();
auto display = window->get_display();
auto cursor = Gdk::Cursor::create(display, "pointer");
window->set_cursor(cursor);
});
// image
if (is_stock) { // unicode/stock
const auto &pb = emojis.GetPixBuf(reaction.Emoji.Name);
auto *img = Gtk::manage(new Gtk::Image(pb->scale_simple(16, 16, Gdk::INTERP_BILINEAR)));
img->set_can_focus(false);
box->add(*img);
} else { // custom
const auto &pb = imgr.GetFromURLIfCached(reaction.Emoji.GetURL());
Gtk::Image *img;
if (pb) {
img = Gtk::manage(new Gtk::Image(pb->scale_simple(16, 16, Gdk::INTERP_BILINEAR)));
} else {
img = Gtk::manage(new Gtk::Image(placeholder));
// can track_obj PLEASE work ???
imgr.LoadFromURL(reaction.Emoji.GetURL(), sigc::bind<0>(sigc::mem_fun(*this, &ChatMessageItemContainer::ReactionUpdateImage), img));
}
img->set_can_focus(false);
box->add(*img);
}
auto *lbl = Gtk::manage(new Gtk::Label(std::to_string(reaction.Count)));
lbl->set_margin_left(5);
lbl->get_style_context()->add_class("reaction-count");
box->add(*lbl);
}
return flow;
}
void ChatMessageItemContainer::ReactionUpdateImage(Gtk::Image *img, const Glib::RefPtr<Gdk::Pixbuf> &pb) {
img->property_pixbuf() = pb->scale_simple(16, 16, Gdk::INTERP_BILINEAR);
}
void ChatMessageItemContainer::HandleImage(const AttachmentData &data, Gtk::Image *img, std::string url) {
m_img_loadmap[url] = std::make_pair(img, data);
// ask the chatwindow to call UpdateImage because dealing with lifetimes sucks
@ -715,6 +815,14 @@ ChatMessageItemContainer::type_signal_channel_click ChatMessageItemContainer::si
return m_signal_action_channel_click;
}
ChatMessageItemContainer::type_signal_action_reaction_add ChatMessageItemContainer::signal_action_reaction_add() {
return m_signal_action_reaction_add;
}
ChatMessageItemContainer::type_signal_action_reaction_remove ChatMessageItemContainer::signal_action_reaction_remove() {
return m_signal_action_reaction_remove;
}
ChatMessageItemContainer::type_signal_image_load ChatMessageItemContainer::signal_image_load() {
return m_signal_image_load;
}

View File

@ -14,6 +14,7 @@ public:
void UpdateAttributes();
void UpdateContent();
void UpdateImage(std::string url, Glib::RefPtr<Gdk::Pixbuf> buf);
void UpdateReactions();
protected:
bool EmitImageLoad(std::string url);
@ -25,6 +26,8 @@ protected:
Gtk::Widget *CreateImageComponent(const AttachmentData &data);
Gtk::Widget *CreateAttachmentComponent(const AttachmentData &data); // non-image attachments
Gtk::Widget *CreateStickerComponent(const Sticker &data);
Gtk::Widget *CreateReactionsComponent(const Message *data);
void ReactionUpdateImage(Gtk::Image *img, const Glib::RefPtr<Gdk::Pixbuf> &pb);
void HandleImage(const AttachmentData &data, Gtk::Image *img, std::string url);
void OnEmbedImageLoad(const Glib::RefPtr<Gdk::Pixbuf> &pixbuf);
@ -75,6 +78,7 @@ protected:
Gtk::TextView *m_text_component = nullptr;
Gtk::Widget *m_embed_component = nullptr;
Gtk::Widget *m_reactions_component = nullptr;
public:
typedef sigc::signal<void, std::string> type_signal_image_load;
@ -82,10 +86,14 @@ public:
typedef sigc::signal<void> type_signal_action_delete;
typedef sigc::signal<void> type_signal_action_edit;
typedef sigc::signal<void, Snowflake> type_signal_channel_click;
typedef sigc::signal<void, Glib::ustring> type_signal_action_reaction_add;
typedef sigc::signal<void, Glib::ustring> type_signal_action_reaction_remove;
type_signal_action_delete signal_action_delete();
type_signal_action_edit signal_action_edit();
type_signal_channel_click signal_action_channel_click();
type_signal_action_reaction_add signal_action_reaction_add();
type_signal_action_reaction_remove signal_action_reaction_remove();
type_signal_image_load signal_image_load();
@ -93,6 +101,8 @@ private:
type_signal_action_delete m_signal_action_delete;
type_signal_action_edit m_signal_action_edit;
type_signal_channel_click m_signal_action_channel_click;
type_signal_action_reaction_add m_signal_action_reaction_add;
type_signal_action_reaction_remove m_signal_action_reaction_remove;
type_signal_image_load m_signal_image_load;
};

View File

@ -128,6 +128,14 @@ Snowflake ChatWindow::GetOldestListedMessage() {
return m;
}
void ChatWindow::UpdateReactions(Snowflake id) {
auto it = m_id_to_widget.find(id);
if (it == m_id_to_widget.end()) return;
auto *widget = dynamic_cast<ChatMessageItemContainer *>(it->second);
if (widget == nullptr) return;
widget->UpdateReactions();
}
Snowflake ChatWindow::GetActiveChannel() const {
return m_active_channel;
}
@ -206,6 +214,12 @@ void ChatWindow::ProcessNewMessage(Snowflake id, bool prepend) {
content->signal_action_edit().connect([this, id] {
m_signal_action_message_edit.emit(m_active_channel, id);
});
content->signal_action_reaction_add().connect([this, id](const Glib::ustring &param) {
m_signal_action_reaction_add.emit(id, param);
});
content->signal_action_reaction_remove().connect([this, id](const Glib::ustring &param) {
m_signal_action_reaction_remove.emit(id, param);
});
content->signal_image_load().connect([this, id](std::string url) {
auto &mgr = Abaddon::Get().GetImageManager();
mgr.LoadFromURL(url, [this, id, url](Glib::RefPtr<Gdk::Pixbuf> buf) {
@ -339,3 +353,11 @@ ChatWindow::type_signal_action_insert_mention ChatWindow::signal_action_insert_m
ChatWindow::type_signal_action_open_user_menu ChatWindow::signal_action_open_user_menu() {
return m_signal_action_open_user_menu;
}
ChatWindow::type_signal_action_reaction_add ChatWindow::signal_action_reaction_add() {
return m_signal_action_reaction_add;
}
ChatWindow::type_signal_action_reaction_remove ChatWindow::signal_action_reaction_remove() {
return m_signal_action_reaction_remove;
}

View File

@ -23,6 +23,7 @@ public:
void AddNewHistory(const std::vector<Snowflake> &id); // prepend messages
void InsertChatInput(std::string text);
Snowflake GetOldestListedMessage(); // oldest message that is currently in the ListBox
void UpdateReactions(Snowflake id);
protected:
ChatMessageItemContainer *CreateMessageComponent(Snowflake id); // to be inserted into header's content box
@ -72,6 +73,8 @@ public:
typedef sigc::signal<void, Snowflake> type_signal_action_channel_click;
typedef sigc::signal<void, Snowflake> type_signal_action_insert_mention;
typedef sigc::signal<void, const GdkEvent *, Snowflake, Snowflake> type_signal_action_open_user_menu;
typedef sigc::signal<void, Snowflake, Glib::ustring> type_signal_action_reaction_add;
typedef sigc::signal<void, Snowflake, Glib::ustring> type_signal_action_reaction_remove;
type_signal_action_message_delete signal_action_message_delete();
type_signal_action_message_edit signal_action_message_edit();
@ -80,6 +83,8 @@ public:
type_signal_action_channel_click signal_action_channel_click();
type_signal_action_insert_mention signal_action_insert_mention();
type_signal_action_open_user_menu signal_action_open_user_menu();
type_signal_action_reaction_add signal_action_reaction_add();
type_signal_action_reaction_remove signal_action_reaction_remove();
private:
type_signal_action_message_delete m_signal_action_message_delete;
@ -89,4 +94,6 @@ private:
type_signal_action_channel_click m_signal_action_channel_click;
type_signal_action_insert_mention m_signal_action_insert_mention;
type_signal_action_open_user_menu m_signal_action_open_user_menu;
type_signal_action_reaction_add m_signal_action_reaction_add;
type_signal_action_reaction_remove m_signal_action_reaction_remove;
};

View File

@ -105,6 +105,22 @@
margin: 5px;
}
.reaction-box {
padding: 2px 5px 2px 5px;
margin: 0px 0px 0px 0px;
background-color: rgba(0.4, 0.4, 0.4, 0.4);
border-radius: 5px;
border: 1px solid transparent;
}
.reaction-box.reacted {
border: 1px solid white;
}
.reaction-count {
color: #cfd8dc;
}
paned separator {
background:#37474f;
}

View File

@ -411,6 +411,34 @@ std::optional<Snowflake> DiscordClient::FindDM(Snowflake user_id) {
return std::nullopt;
}
void DiscordClient::AddReaction(Snowflake id, Glib::ustring param) {
if (!param.is_ascii()) // means unicode param
param = Glib::uri_escape_string(param, "", false);
else {
const auto &tmp = m_store.GetEmoji(param);
if (tmp.has_value())
param = tmp->Name + ":" + std::to_string(tmp->ID);
else
return;
}
Snowflake channel_id = m_store.GetMessage(id)->ChannelID;
m_http.MakePUT("/channels/" + std::to_string(channel_id) + "/messages/" + std::to_string(id) + "/reactions/" + param + "/@me", "", [](auto) {});
}
void DiscordClient::RemoveReaction(Snowflake id, Glib::ustring param) {
if (!param.is_ascii()) // means unicode param
param = Glib::uri_escape_string(param, "", false);
else {
const auto &tmp = m_store.GetEmoji(param);
if (tmp.has_value())
param = tmp->Name + ":" + std::to_string(tmp->ID);
else
return;
}
Snowflake channel_id = m_store.GetMessage(id)->ChannelID;
m_http.MakeDELETE("/channels/" + std::to_string(channel_id) + "/messages/" + std::to_string(id) + "/reactions/" + param + "/@me", [](auto) {});
}
void DiscordClient::UpdateToken(std::string token) {
if (!IsStarted()) {
m_token = token;
@ -552,6 +580,12 @@ void DiscordClient::HandleGatewayMessage(std::string str) {
case GatewayEvent::GUILD_ROLE_DELETE: {
HandleGatewayGuildRoleDelete(m);
} break;
case GatewayEvent::MESSAGE_REACTION_ADD: {
HandleGatewayMessageReactionAdd(m);
} break;
case GatewayEvent::MESSAGE_REACTION_REMOVE: {
HandleGatewayMessageReactionRemove(m);
} break;
}
} break;
default:
@ -735,6 +769,80 @@ void DiscordClient::HandleGatewayGuildRoleDelete(const GatewayMessage &msg) {
m_signal_role_delete.emit(data.RoleID);
}
void DiscordClient::HandleGatewayMessageReactionAdd(const GatewayMessage &msg) {
MessageReactionAddObject data = msg.Data;
auto to = m_store.GetMessage(data.MessageID);
if (!to.has_value()) return;
if (!to->Reactions.has_value()) to->Reactions.emplace();
// add if present
bool stock;
auto it = std::find_if(to->Reactions->begin(), to->Reactions->end(), [&](const ReactionData &x) {
if (data.Emoji.ID.IsValid() && x.Emoji.ID.IsValid()) {
stock = false;
return data.Emoji.ID == x.Emoji.ID;
} else {
stock = true;
return data.Emoji.Name == x.Emoji.Name;
}
});
if (it != to->Reactions->end()) {
it->Count++;
if (data.UserID == GetUserData().ID)
it->HasReactedWith = true;
m_store.SetMessage(data.MessageID, *to);
if (stock)
m_signal_reaction_add.emit(data.MessageID, data.Emoji.Name);
else
m_signal_reaction_add.emit(data.MessageID, std::to_string(data.Emoji.ID));
return;
}
// create new
auto &rdata = to->Reactions->emplace_back();
rdata.Count = 1;
rdata.Emoji = data.Emoji;
rdata.HasReactedWith = data.UserID == GetUserData().ID;
m_store.SetMessage(data.MessageID, *to);
if (stock)
m_signal_reaction_add.emit(data.MessageID, data.Emoji.Name);
else
m_signal_reaction_add.emit(data.MessageID, std::to_string(data.Emoji.ID));
}
void DiscordClient::HandleGatewayMessageReactionRemove(const GatewayMessage &msg) {
MessageReactionRemoveObject data = msg.Data;
auto to = m_store.GetMessage(data.MessageID);
if (!to.has_value()) return;
if (!to->Reactions.has_value()) return;
bool stock;
auto it = std::find_if(to->Reactions->begin(), to->Reactions->end(), [&](const ReactionData &x) {
if (data.Emoji.ID.IsValid() && x.Emoji.ID.IsValid()) {
stock = false;
return data.Emoji.ID == x.Emoji.ID;
} else {
stock = true;
return data.Emoji.Name == x.Emoji.Name;
}
});
if (it == to->Reactions->end()) return;
if (it->Count == 1)
to->Reactions->erase(it);
else {
if (data.UserID == GetUserData().ID)
it->HasReactedWith = false;
it->Count--;
}
m_store.SetMessage(data.MessageID, *to);
if (stock)
m_signal_reaction_remove.emit(data.MessageID, data.Emoji.Name);
else
m_signal_reaction_remove.emit(data.MessageID, std::to_string(data.Emoji.ID));
}
void DiscordClient::HandleGatewayReconnect(const GatewayMessage &msg) {
m_signal_disconnected.emit(true);
inflateEnd(&m_zstream);
@ -928,6 +1036,8 @@ void DiscordClient::LoadEventMap() {
m_event_map["GUILD_ROLE_UPDATE"] = GatewayEvent::GUILD_ROLE_UPDATE;
m_event_map["GUILD_ROLE_CREATE"] = GatewayEvent::GUILD_ROLE_CREATE;
m_event_map["GUILD_ROLE_DELETE"] = GatewayEvent::GUILD_ROLE_DELETE;
m_event_map["MESSAGE_REACTION_ADD"] = GatewayEvent::MESSAGE_REACTION_ADD;
m_event_map["MESSAGE_REACTION_REMOVE"] = GatewayEvent::MESSAGE_REACTION_REMOVE;
}
DiscordClient::type_signal_gateway_ready DiscordClient::signal_gateway_ready() {
@ -993,3 +1103,11 @@ DiscordClient::type_signal_role_create DiscordClient::signal_role_create() {
DiscordClient::type_signal_role_delete DiscordClient::signal_role_delete() {
return m_signal_role_delete;
}
DiscordClient::type_signal_reaction_add DiscordClient::signal_reaction_add() {
return m_signal_reaction_add;
}
DiscordClient::type_signal_reaction_remove DiscordClient::signal_reaction_remove() {
return m_signal_reaction_remove;
}

View File

@ -108,6 +108,8 @@ public:
void UpdateStatus(const std::string &status, bool is_afk, const Activity &obj);
void CreateDM(Snowflake user_id);
std::optional<Snowflake> FindDM(Snowflake user_id); // wont find group dms
void AddReaction(Snowflake id, Glib::ustring param);
void RemoveReaction(Snowflake id, Glib::ustring param);
void UpdateToken(std::string token);
void SetUserAgent(std::string agent);
@ -140,6 +142,8 @@ private:
void HandleGatewayGuildRoleUpdate(const GatewayMessage &msg);
void HandleGatewayGuildRoleCreate(const GatewayMessage &msg);
void HandleGatewayGuildRoleDelete(const GatewayMessage &msg);
void HandleGatewayMessageReactionAdd(const GatewayMessage &msg);
void HandleGatewayMessageReactionRemove(const GatewayMessage &msg);
void HandleGatewayReconnect(const GatewayMessage &msg);
void HeartbeatThread();
void SendIdentify();
@ -202,6 +206,8 @@ public:
typedef sigc::signal<void, Snowflake> type_signal_role_update;
typedef sigc::signal<void, Snowflake> type_signal_role_create;
typedef sigc::signal<void, Snowflake> type_signal_role_delete;
typedef sigc::signal<void, Snowflake, Glib::ustring> type_signal_reaction_add;
typedef sigc::signal<void, Snowflake, Glib::ustring> type_signal_reaction_remove;
typedef sigc::signal<void, bool> type_signal_disconnected; // bool true if reconnecting
typedef sigc::signal<void> type_signal_connected;
@ -219,6 +225,8 @@ public:
type_signal_role_update signal_role_update();
type_signal_role_create signal_role_create();
type_signal_role_delete signal_role_delete();
type_signal_reaction_add signal_reaction_add();
type_signal_reaction_remove signal_reaction_remove();
type_signal_disconnected signal_disconnected();
type_signal_connected signal_connected();
@ -237,6 +245,8 @@ protected:
type_signal_role_update m_signal_role_update;
type_signal_role_create m_signal_role_create;
type_signal_role_delete m_signal_role_delete;
type_signal_reaction_add m_signal_reaction_add;
type_signal_reaction_remove m_signal_reaction_remove;
type_signal_disconnected m_signal_disconnected;
type_signal_connected m_signal_connected;
};

View File

@ -11,6 +11,23 @@ void from_json(const nlohmann::json &j, Emoji &m) {
JS_O("available", m.IsAvailable);
}
void to_json(nlohmann::json &j, const Emoji &m) {
if (m.ID.IsValid())
j["id"] = m.ID;
else
j["id"] = nullptr;
if (m.Name != "")
j["name"] = m.Name;
else
j["name"] = nullptr;
JS_IF("roles", m.Roles);
JS_IF("user", m.Creator);
JS_IF("require_colons", m.NeedsColons);
JS_IF("managed", m.IsManaged);
JS_IF("animated", m.IsAnimated);
JS_IF("available", m.IsAvailable);
}
std::string Emoji::GetURL() const {
return "https://cdn.discordapp.com/emojis/" + std::to_string(ID) + ".png";
}

View File

@ -16,6 +16,7 @@ struct Emoji {
std::optional<bool> IsAvailable;
friend void from_json(const nlohmann::json &j, Emoji &m);
friend void to_json(nlohmann::json &j, const Emoji &m);
std::string GetURL() const;
static std::string URLFromID(std::string emoji_id);

View File

@ -152,6 +152,18 @@ void to_json(nlohmann::json &j, const MessageReferenceData &m) {
JS_IF("guild_id", m.GuildID);
}
void from_json(const nlohmann::json &j, ReactionData &m) {
JS_D("count", m.Count);
JS_D("me", m.HasReactedWith);
JS_D("emoji", m.Emoji);
}
void to_json(nlohmann::json &j, const ReactionData &m) {
j["count"] = m.Count;
j["me"] = m.HasReactedWith;
j["emoji"] = m.Emoji;
}
void from_json(const nlohmann::json &j, Message &m) {
JS_D("id", m.ID);
JS_D("channel_id", m.ChannelID);
@ -170,7 +182,7 @@ void from_json(const nlohmann::json &j, Message &m) {
// JS_O("mention_channels", m.MentionChannels);
JS_D("attachments", m.Attachments);
JS_D("embeds", m.Embeds);
// JS_O("reactions", m.Reactions);
JS_O("reactions", m.Reactions);
JS_O("nonce", m.Nonce);
JS_D("pinned", m.IsPinned);
JS_O("webhook_id", m.WebhookID);

View File

@ -3,6 +3,7 @@
#include "json.hpp"
#include "user.hpp"
#include "sticker.hpp"
#include "emoji.hpp"
#include <string>
#include <vector>
@ -140,6 +141,15 @@ struct MessageReferenceData {
friend void to_json(nlohmann::json &j, const MessageReferenceData &m);
};
struct ReactionData {
int Count;
bool HasReactedWith;
Emoji Emoji;
friend void from_json(const nlohmann::json &j, ReactionData &m);
friend void to_json(nlohmann::json &j, const ReactionData &m);
};
struct Message {
Snowflake ID;
Snowflake ChannelID;
@ -156,7 +166,7 @@ struct Message {
// std::optional<std::vector<ChannelMentionData>> MentionChannels;
std::vector<AttachmentData> Attachments;
std::vector<EmbedData> Embeds;
// std::optional<std::vector<ReactionData>> Reactions;
std::optional<std::vector<ReactionData>> Reactions;
std::optional<std::string> Nonce;
bool IsPinned;
std::optional<Snowflake> WebhookID;

View File

@ -216,3 +216,20 @@ void from_json(const nlohmann::json &j, GuildRoleDeleteObject &m) {
JS_D("guild_id", m.GuildID);
JS_D("role_id", m.RoleID);
}
void from_json(const nlohmann::json &j, MessageReactionAddObject &m) {
JS_D("user_id", m.UserID);
JS_D("channel_id", m.ChannelID);
JS_D("message_id", m.MessageID);
JS_O("guild_id", m.GuildID);
JS_O("member", m.Member);
JS_D("emoji", m.Emoji);
}
void from_json(const nlohmann::json &j, MessageReactionRemoveObject &m) {
JS_D("user_id", m.UserID);
JS_D("channel_id", m.ChannelID);
JS_D("message_id", m.MessageID);
JS_O("guild_id", m.GuildID);
JS_D("emoji", m.Emoji);
}

View File

@ -49,6 +49,8 @@ enum class GatewayEvent : int {
GUILD_ROLE_UPDATE,
GUILD_ROLE_CREATE,
GUILD_ROLE_DELETE,
MESSAGE_REACTION_ADD,
MESSAGE_REACTION_REMOVE,
};
struct GatewayMessage {
@ -298,3 +300,24 @@ struct GuildRoleDeleteObject {
friend void from_json(const nlohmann::json &j, GuildRoleDeleteObject &m);
};
struct MessageReactionAddObject {
Snowflake UserID;
Snowflake ChannelID;
Snowflake MessageID;
std::optional<Snowflake> GuildID;
std::optional<GuildMember> Member;
Emoji Emoji;
friend void from_json(const nlohmann::json &j, MessageReactionAddObject &m);
};
struct MessageReactionRemoveObject {
Snowflake UserID;
Snowflake ChannelID;
Snowflake MessageID;
std::optional<Snowflake> GuildID;
Emoji Emoji;
friend void from_json(const nlohmann::json &j, MessageReactionRemoveObject &m);
};

View File

@ -219,9 +219,13 @@ void Store::SetMessage(Snowflake id, const Message &message) {
Bind(m_set_msg_stmt, 18, tmp);
} else
Bind(m_set_msg_stmt, 18, nullptr);
Bind(m_set_msg_stmt, 19, message.IsDeleted());
Bind(m_set_msg_stmt, 20, message.IsEdited());
if (message.Reactions.has_value()) {
std::string tmp = nlohmann::json(*message.Reactions).dump();
Bind(m_set_msg_stmt, 19, tmp);
} else
Bind(m_set_msg_stmt, 19, nullptr);
Bind(m_set_msg_stmt, 20, message.IsDeleted());
Bind(m_set_msg_stmt, 21, message.IsEdited());
if (!RunInsert(m_set_msg_stmt))
fprintf(stderr, "message insert failed: %s\n", sqlite3_errstr(m_db_err));
@ -468,10 +472,13 @@ std::optional<Message> Store::GetMessage(Snowflake id) const {
Get(m_get_msg_stmt, 17, tmps);
if (tmps != "")
ret.Stickers = nlohmann::json::parse(tmps).get<std::vector<Sticker>>();
Get(m_get_msg_stmt, 18, tmps);
if (tmps != "")
ret.Reactions = nlohmann::json::parse(tmps).get<std::vector<ReactionData>>();
bool tmpb = false;
Get(m_get_msg_stmt, 18, tmpb);
if (tmpb) ret.SetDeleted();
Get(m_get_msg_stmt, 19, tmpb);
if (tmpb) ret.SetDeleted();
Get(m_get_msg_stmt, 20, tmpb);
if (tmpb) ret.SetEdited();
Reset(m_get_msg_stmt);
@ -589,164 +596,165 @@ void Store::EndTransaction() {
bool Store::CreateTables() {
constexpr const char *create_users = R"(
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY,
username TEXT NOT NULL,
discriminator TEXT NOT NULL,
avatar TEXT,
bot BOOL,
system BOOL,
mfa BOOL,
locale TEXT,
verified BOOl,
email TEXT,
flags INTEGER,
premium INTEGER,
pubflags INTEGER
)
)";
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY,
username TEXT NOT NULL,
discriminator TEXT NOT NULL,
avatar TEXT,
bot BOOL,
system BOOL,
mfa BOOL,
locale TEXT,
verified BOOl,
email TEXT,
flags INTEGER,
premium INTEGER,
pubflags INTEGER
)
)";
constexpr const char *create_permissions = R"(
CREATE TABLE IF NOT EXISTS permissions (
id INTEGER NOT NULL,
channel_id INTEGER NOT NULL,
type INTEGER NOT NULL,
allow INTEGER NOT NULL,
deny INTEGER NOT NULL
)
)";
CREATE TABLE IF NOT EXISTS permissions (
id INTEGER NOT NULL,
channel_id INTEGER NOT NULL,
type INTEGER NOT NULL,
allow INTEGER NOT NULL,
deny INTEGER NOT NULL
)
)";
constexpr const char *create_messages = R"(
CREATE TABLE IF NOT EXISTS messages (
id INTEGER PRIMARY KEY,
channel_id INTEGER NOT NULL,
guild_id INTEGER,
author_id INTEGER NOT NULL,
content TEXT NOT NULL,
timestamp TEXT NOT NULL,
edited_timestamp TEXT,
tts BOOL NOT NULL,
everyone BOOL NOT NULL,
mentions TEXT NOT NULL, /* json */
attachments TEXT NOT NULL, /* json */
embeds TEXT NOT NULL, /* json */
pinned BOOL,
webhook_id INTEGER,
type INTEGER,
reference TEXT, /* json */
flags INTEGER,
stickers TEXT, /* json */
/* extra */
deleted BOOL,
edited BOOL
)
)";
CREATE TABLE IF NOT EXISTS messages (
id INTEGER PRIMARY KEY,
channel_id INTEGER NOT NULL,
guild_id INTEGER,
author_id INTEGER NOT NULL,
content TEXT NOT NULL,
timestamp TEXT NOT NULL,
edited_timestamp TEXT,
tts BOOL NOT NULL,
everyone BOOL NOT NULL,
mentions TEXT NOT NULL, /* json */
attachments TEXT NOT NULL, /* json */
embeds TEXT NOT NULL, /* json */
pinned BOOL,
webhook_id INTEGER,
type INTEGER,
reference TEXT, /* json */
flags INTEGER,
stickers TEXT, /* json */
reactions TEXT, /* json */
/* extra */
deleted BOOL,
edited BOOL
)
)";
constexpr const char *create_roles = R"(
CREATE TABLE IF NOT EXISTS roles (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
color INTEGER NOT NULL,
hoisted BOOL NOT NULL,
position INTEGER NOT NULL,
permissions INTEGER NOT NULL,
managed BOOL NOT NULL,
mentionable BOOL NOT NULL
)
)";
CREATE TABLE IF NOT EXISTS roles (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
color INTEGER NOT NULL,
hoisted BOOL NOT NULL,
position INTEGER NOT NULL,
permissions INTEGER NOT NULL,
managed BOOL NOT NULL,
mentionable BOOL NOT NULL
)
)";
constexpr const char *create_emojis = R"(
CREATE TABLE IF NOT EXISTS emojis (
id INTEGER PRIMARY KEY, /*though nullable, only custom emojis (with non-null ids) are stored*/
name TEXT NOT NULL, /*same as id*/
roles TEXT, /* json */
creator_id INTEGER,
colons BOOL,
managed BOOL,
animated BOOL,
available BOOL
)
)";
CREATE TABLE IF NOT EXISTS emojis (
id INTEGER PRIMARY KEY, /*though nullable, only custom emojis (with non-null ids) are stored*/
name TEXT NOT NULL, /*same as id*/
roles TEXT, /* json */
creator_id INTEGER,
colons BOOL,
managed BOOL,
animated BOOL,
available BOOL
)
)";
constexpr const char *create_members = R"(
CREATE TABLE IF NOT EXISTS members (
user_id INTEGER PRIMARY KEY,
guild_id INTEGER NOT NULL,
nickname TEXT,
roles TEXT NOT NULL, /* json */
joined_at TEXT NOT NULL,
premium_since TEXT,
deaf BOOL NOT NULL,
mute BOOL NOT NULL
)
)";
CREATE TABLE IF NOT EXISTS members (
user_id INTEGER PRIMARY KEY,
guild_id INTEGER NOT NULL,
nickname TEXT,
roles TEXT NOT NULL, /* json */
joined_at TEXT NOT NULL,
premium_since TEXT,
deaf BOOL NOT NULL,
mute BOOL NOT NULL
)
)";
constexpr char *create_guilds = R"(
CREATE TABLE IF NOT EXISTS guilds (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
icon TEXT NOT NULL,
splash TEXT,
owner BOOL,
owner_id INTEGER NOT NULL,
permissions INTEGER, /* new */
voice_region TEXT,
afk_id INTEGER,
afk_timeout INTEGER NOT NULL,
verification INTEGER NOT NULL,
notifications INTEGER NOT NULL,
roles TEXT NOT NULL, /* json */
emojis TEXT NOT NULL, /* json */
features TEXT NOT NULL, /* json */
mfa INTEGER NOT NULL,
application INTEGER,
widget BOOL,
widget_channel INTEGER,
system_flags INTEGER NOT NULL,
rules_channel INTEGER,
joined_at TEXT,
large BOOL,
unavailable BOOL,
member_count INTEGER,
channels TEXT NOT NULL, /* json */
max_presences INTEGER,
max_members INTEGER,
vanity TEXT,
description TEXT,
banner_hash TEXT,
premium_tier INTEGER NOT NULL,
premium_count INTEGER,
locale TEXT NOT NULL,
public_updates_id INTEGER,
max_video_users INTEGER,
approx_members INTEGER,
approx_presences INTEGER,
lazy BOOL
)
)";
constexpr const char *create_guilds = R"(
CREATE TABLE IF NOT EXISTS guilds (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
icon TEXT NOT NULL,
splash TEXT,
owner BOOL,
owner_id INTEGER NOT NULL,
permissions INTEGER, /* new */
voice_region TEXT,
afk_id INTEGER,
afk_timeout INTEGER NOT NULL,
verification INTEGER NOT NULL,
notifications INTEGER NOT NULL,
roles TEXT NOT NULL, /* json */
emojis TEXT NOT NULL, /* json */
features TEXT NOT NULL, /* json */
mfa INTEGER NOT NULL,
application INTEGER,
widget BOOL,
widget_channel INTEGER,
system_flags INTEGER NOT NULL,
rules_channel INTEGER,
joined_at TEXT,
large BOOL,
unavailable BOOL,
member_count INTEGER,
channels TEXT NOT NULL, /* json */
max_presences INTEGER,
max_members INTEGER,
vanity TEXT,
description TEXT,
banner_hash TEXT,
premium_tier INTEGER NOT NULL,
premium_count INTEGER,
locale TEXT NOT NULL,
public_updates_id INTEGER,
max_video_users INTEGER,
approx_members INTEGER,
approx_presences INTEGER,
lazy BOOL
)
)";
constexpr char *create_channels = R"(
CREATE TABLE IF NOT EXISTS channels (
id INTEGER PRIMARY KEY,
type INTEGER NOT NULL,
guild_id INTEGER,
position INTEGER,
overwrites TEXT, /* json */
name TEXT,
topic TEXT,
is_nsfw BOOL,
last_message_id INTEGER,
bitrate INTEGER,
user_limit INTEGER,
rate_limit INTEGER,
recipients TEXT, /* json */
icon TEXT,
owner_id INTEGER,
application_id INTEGER,
parent_id INTEGER,
last_pin_timestamp TEXT
)
)";
constexpr const char *create_channels = R"(
CREATE TABLE IF NOT EXISTS channels (
id INTEGER PRIMARY KEY,
type INTEGER NOT NULL,
guild_id INTEGER,
position INTEGER,
overwrites TEXT, /* json */
name TEXT,
topic TEXT,
is_nsfw BOOL,
last_message_id INTEGER,
bitrate INTEGER,
user_limit INTEGER,
rate_limit INTEGER,
recipients TEXT, /* json */
icon TEXT,
owner_id INTEGER,
application_id INTEGER,
parent_id INTEGER,
last_pin_timestamp TEXT
)
)";
m_db_err = sqlite3_exec(m_db, create_users, nullptr, nullptr, nullptr);
if (m_db_err != SQLITE_OK) {
@ -801,84 +809,84 @@ last_pin_timestamp TEXT
bool Store::CreateStatements() {
constexpr const char *set_user = R"(
REPLACE INTO users VALUES (
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
)
)";
REPLACE INTO users VALUES (
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
)
)";
constexpr const char *get_user = R"(
SELECT * FROM users WHERE id = ?
)";
SELECT * FROM users WHERE id = ?
)";
constexpr const char *set_perm = R"(
REPLACE INTO permissions VALUES (
?, ?, ?, ?, ?
)
)";
REPLACE INTO permissions VALUES (
?, ?, ?, ?, ?
)
)";
constexpr const char *get_perm = R"(
SELECT * FROM permissions WHERE id = ? AND channel_id = ?
)";
SELECT * FROM permissions WHERE id = ? AND channel_id = ?
)";
constexpr const char *set_msg = R"(
REPLACE INTO messages VALUES (
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
)
)";
REPLACE INTO messages VALUES (
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
)
)";
constexpr const char *get_msg = R"(
SELECT * FROM messages WHERE id = ?
)";
SELECT * FROM messages WHERE id = ?
)";
constexpr const char *set_role = R"(
REPLACE INTO roles VALUES (
?, ?, ?, ?, ?, ?, ?, ?
)
)";
REPLACE INTO roles VALUES (
?, ?, ?, ?, ?, ?, ?, ?
)
)";
constexpr const char *get_role = R"(
SELECT * FROM roles WHERE id = ?
)";
SELECT * FROM roles WHERE id = ?
)";
constexpr const char *set_emoji = R"(
REPLACE INTO emojis VALUES (
?, ?, ?, ?, ?, ?, ?, ?
)
)";
REPLACE INTO emojis VALUES (
?, ?, ?, ?, ?, ?, ?, ?
)
)";
constexpr const char *get_emoji = R"(
SELECT * FROM emojis WHERE id = ?
)";
SELECT * FROM emojis WHERE id = ?
)";
constexpr const char *set_member = R"(
REPLACE INTO members VALUES (
?, ?, ?, ?, ?, ?, ?, ?
)
)";
REPLACE INTO members VALUES (
?, ?, ?, ?, ?, ?, ?, ?
)
)";
constexpr const char *get_member = R"(
SELECT * FROM members WHERE user_id = ? AND guild_id = ?
)";
SELECT * FROM members WHERE user_id = ? AND guild_id = ?
)";
constexpr const char *set_guild = R"(
REPLACE INTO guilds VALUES (
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
)
)";
REPLACE INTO guilds VALUES (
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
)
)";
constexpr const char *get_guild = R"(
SELECT * FROM guilds WHERE id = ?
)";
SELECT * FROM guilds WHERE id = ?
)";
constexpr const char *set_chan = R"(
REPLACE INTO channels VALUES (
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
)
)";
REPLACE INTO channels VALUES (
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
)
)";
constexpr const char *get_chan = R"(
SELECT * FROM channels WHERE id = ?
)";
SELECT * FROM channels WHERE id = ?
)";
m_db_err = sqlite3_prepare_v2(m_db, set_user, -1, &m_set_user_stmt, nullptr);
if (m_db_err != SQLITE_OK) {

View File

@ -33,6 +33,13 @@ void from_json(const nlohmann::json &j, User &m) {
JS_ON("phone", m.Phone);
}
void to_json(nlohmann::json &j, const User &m) {
j["id"] = m.ID;
j["username"] = m.Username;
j["avatar"] = m.Avatar;
// rest of stuff as needed im too lazy and its probably not necessary
}
void User::update_from_json(const nlohmann::json &j, User &m) {
JS_RD("username", m.Username);
JS_RD("discriminator", m.Discriminator);

View File

@ -25,6 +25,7 @@ struct User {
std::string Phone; // null?
friend void from_json(const nlohmann::json &j, User &m);
friend void to_json(nlohmann::json &j, const User &m);
static void update_from_json(const nlohmann::json &j, User &m);
bool HasAvatar() const;

View File

@ -211,6 +211,14 @@ Snowflake MainWindow::GetChatOldestListedMessage() {
return m_chat.GetOldestListedMessage();
}
void MainWindow::UpdateChatReactionAdd(Snowflake id, const Glib::ustring &param) {
m_chat.UpdateReactions(id);
}
void MainWindow::UpdateChatReactionRemove(Snowflake id, const Glib::ustring &param) {
m_chat.UpdateReactions(id);
}
ChannelList *MainWindow::GetChannelList() {
return &m_channel_list;
}

View File

@ -26,6 +26,8 @@ public:
void UpdateChatPrependHistory(const std::vector<Snowflake> &msgs);
void InsertChatInput(std::string text);
Snowflake GetChatOldestListedMessage();
void UpdateChatReactionAdd(Snowflake id, const Glib::ustring &param);
void UpdateChatReactionRemove(Snowflake id, const Glib::ustring &param);
ChannelList *GetChannelList();
ChatWindow *GetChatWindow();