show pending/failed messages

css changes:
- added .failed
- added .pending
This commit is contained in:
ouwou 2021-04-03 02:40:37 -04:00
parent 88c91495c3
commit 77b43f0f24
16 changed files with 175 additions and 33 deletions

View File

@ -92,6 +92,8 @@ Or, do steps 1 and 2, and open CMakeLists.txt in Visual Studio if `vcpkg integra
.message-container-avatar - Avatar for a user in a message
.message-container-extra - Label containing BOT/Webhook
.message-text - The text of a user message
.pending - Extra class of .message-text for messages pending to be sent
.failed - Extra class of .message-text for messages that failed to be sent
.message-attachment-box - Contains attachment info
.message-reply - Container for the replied-to message in a reply (these elements will also have .message-text set)
.message-input - Applied to the chat input container

View File

@ -42,6 +42,7 @@ Abaddon::Abaddon()
m_discord.signal_reaction_add().connect(sigc::mem_fun(*this, &Abaddon::DiscordOnReactionAdd));
m_discord.signal_reaction_remove().connect(sigc::mem_fun(*this, &Abaddon::DiscordOnReactionRemove));
m_discord.signal_guild_join_request_create().connect(sigc::mem_fun(*this, &Abaddon::DiscordOnGuildJoinRequestCreate));
m_discord.signal_message_sent().connect(sigc::mem_fun(*this, &Abaddon::DiscordOnMessageSent));
m_discord.signal_disconnected().connect(sigc::mem_fun(*this, &Abaddon::DiscordOnDisconnect));
if (m_settings.GetPrefetch())
m_discord.signal_message_create().connect([this](Snowflake id) {
@ -226,6 +227,10 @@ void Abaddon::DiscordOnGuildJoinRequestCreate(const GuildJoinRequestCreateData &
}
}
void Abaddon::DiscordOnMessageSent(const Message &data) {
m_main_window->UpdateChatNewMessage(data.ID);
}
void Abaddon::DiscordOnDisconnect(bool is_reconnecting, GatewayCloseCode close_code) {
m_main_window->UpdateComponents();
if (close_code == GatewayCloseCode::AuthenticationFailed) {

View File

@ -75,6 +75,7 @@ public:
void DiscordOnReactionAdd(Snowflake message_id, const Glib::ustring &param);
void DiscordOnReactionRemove(Snowflake message_id, const Glib::ustring &param);
void DiscordOnGuildJoinRequestCreate(const GuildJoinRequestCreateData &data);
void DiscordOnMessageSent(const Message &data);
void DiscordOnDisconnect(bool is_reconnecting, GatewayCloseCode close_code);
const SettingsManager &GetSettings() const;

View File

@ -51,6 +51,9 @@ ChatMessageItemContainer *ChatMessageItemContainer::FromMessage(Snowflake id) {
container->ID = data->ID;
container->ChannelID = data->ChannelID;
if (data->Nonce.has_value())
container->Nonce = *data->Nonce;
if (data->Content.size() > 0 || data->Type != MessageType::DEFAULT) {
container->m_text_component = container->CreateTextComponent(&*data);
container->AttachEventHandlers(*container->m_text_component);
@ -139,6 +142,11 @@ void ChatMessageItemContainer::UpdateReactions() {
}
}
void ChatMessageItemContainer::SetFailed() {
m_text_component->get_style_context()->remove_class("pending");
m_text_component->get_style_context()->add_class("failed");
}
void ChatMessageItemContainer::UpdateAttributes() {
const auto data = Abaddon::Get().GetDiscordClient().GetMessage(ID);
if (!data.has_value()) return;
@ -176,6 +184,8 @@ void ChatMessageItemContainer::AddClickHandler(Gtk::Widget *widget, std::string
Gtk::TextView *ChatMessageItemContainer::CreateTextComponent(const Message *data) {
auto *tv = Gtk::manage(new Gtk::TextView);
if (data->IsPending)
tv->get_style_context()->add_class("pending");
tv->get_style_context()->add_class("message-text");
tv->set_can_focus(false);
tv->set_editable(false);

View File

@ -7,6 +7,8 @@ public:
Snowflake ID;
Snowflake ChannelID;
std::string Nonce;
ChatMessageItemContainer();
static ChatMessageItemContainer *FromMessage(Snowflake id);
@ -14,6 +16,7 @@ public:
void UpdateAttributes();
void UpdateContent();
void UpdateReactions();
void SetFailed();
protected:
void AddClickHandler(Gtk::Widget *widget, std::string);

View File

@ -5,6 +5,8 @@
#include "chatinput.hpp"
ChatWindow::ChatWindow() {
Abaddon::Get().GetDiscordClient().signal_message_send_fail().connect(sigc::mem_fun(*this, &ChatWindow::OnMessageSendFail));
m_main = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_VERTICAL));
m_list = Gtk::manage(new Gtk::ListBox);
m_scroll = Gtk::manage(new Gtk::ScrolledWindow);
@ -197,6 +199,19 @@ ChatMessageItemContainer *ChatWindow::CreateMessageComponent(Snowflake id) {
return container;
}
void ChatWindow::RemoveMessageAndHeader(Gtk::Widget *widget) {
ChatMessageHeader *header = dynamic_cast<ChatMessageHeader *>(widget->get_ancestor(Gtk::ListBoxRow::get_type()));
if (header != nullptr) {
if (header->GetChildContent().size() == 1) {
m_num_rows--;
delete header;
} else
delete widget;
} else
delete widget;
m_num_messages--;
}
constexpr static int MaxMessagesForCull = 50; // this has to be 50 cuz that magic number is used in a couple other places and i dont feel like replacing them
void ChatWindow::ProcessNewMessage(Snowflake id, bool prepend) {
const auto &client = Abaddon::Get().GetDiscordClient();
@ -204,6 +219,16 @@ void ChatWindow::ProcessNewMessage(Snowflake id, bool prepend) {
const auto data = client.GetMessage(id);
if (!data.has_value()) return;
if (!data->IsPending && data->Nonce.has_value() && data->Author.ID == client.GetUserData().ID) {
for (auto [id, widget] : m_id_to_widget) {
if (dynamic_cast<ChatMessageItemContainer *>(widget)->Nonce == *data->Nonce) {
RemoveMessageAndHeader(widget);
m_id_to_widget.erase(id);
break;
}
}
}
ChatMessageHeader *last_row = nullptr;
bool should_attach = false;
if (m_num_rows > 0) {
@ -222,17 +247,8 @@ void ChatWindow::ProcessNewMessage(Snowflake id, bool prepend) {
if (m_should_scroll_to_bottom && !prepend)
while (m_num_messages > MaxMessagesForCull) {
auto first_it = m_id_to_widget.begin();
ChatMessageHeader *header = dynamic_cast<ChatMessageHeader *>(first_it->second->get_ancestor(Gtk::ListBoxRow::get_type()));
if (header != nullptr) {
if (header->GetChildContent().size() == 1)
delete header;
else
delete first_it->second;
} else
delete first_it->second;
RemoveMessageAndHeader(first_it->second);
m_id_to_widget.erase(first_it);
m_num_rows--;
m_num_messages--;
}
ChatMessageHeader *header;
@ -261,22 +277,24 @@ void ChatWindow::ProcessNewMessage(Snowflake id, bool prepend) {
header->AddContent(content, prepend);
m_id_to_widget[id] = content;
content->signal_action_delete().connect([this, id] {
m_signal_action_message_delete.emit(m_active_channel, id);
});
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_action_channel_click().connect([this](const Snowflake &id) {
m_signal_action_channel_click.emit(id);
});
content->signal_action_reply_to().connect(sigc::mem_fun(*this, &ChatWindow::StartReplying));
if (!data->IsPending) {
content->signal_action_delete().connect([this, id] {
m_signal_action_message_delete.emit(m_active_channel, id);
});
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_action_channel_click().connect([this](const Snowflake &id) {
m_signal_action_channel_click.emit(id);
});
content->signal_action_reply_to().connect(sigc::mem_fun(*this, &ChatWindow::StartReplying));
}
}
header->set_margin_left(5);
@ -321,6 +339,15 @@ void ChatWindow::ScrollToBottom() {
x->set_value(x->get_upper());
}
void ChatWindow::OnMessageSendFail(const std::string &nonce) {
for (auto [id, widget] : m_id_to_widget) {
if (auto *container = dynamic_cast<ChatMessageItemContainer *>(widget); container->Nonce == nonce) {
container->SetFailed();
break;
}
}
}
ChatWindow::type_signal_action_message_delete ChatWindow::signal_action_message_delete() {
return m_signal_action_message_delete;
}

View File

@ -47,9 +47,13 @@ protected:
bool OnKeyPressEvent(GdkEventKey *e);
void OnScrollEdgeOvershot(Gtk::PositionType pos);
void RemoveMessageAndHeader(Gtk::Widget *widget);
void ScrollToBottom();
bool m_should_scroll_to_bottom = true;
void OnMessageSendFail(const std::string &nonce);
Gtk::Box *m_main;
Gtk::ListBox *m_list;
Gtk::ScrolledWindow *m_scroll;

View File

@ -75,10 +75,18 @@
padding-top: 5px;
}
.message-text text, .message-reply {
.message-text:not(.failed) text, .message-reply {
color: @text_color;
}
.message-text.pending text {
color: shade(@text_color, 0.5);
}
.message-text.failed text {
color: #b72d4f;
}
.message-reply {
border-left: 2px solid gray;
padding-left: 20px;

View File

@ -345,18 +345,57 @@ bool DiscordClient::CanManageMember(Snowflake guild_id, Snowflake actor, Snowfla
return actor_highest->Position > target_highest->Position;
}
void DiscordClient::ChatMessageCallback(std::string nonce, const http::response_type &response) {
if (!CheckCode(response)) {
m_signal_message_send_fail.emit(nonce);
}
}
void DiscordClient::SendChatMessage(const std::string &content, Snowflake channel) {
// @([^@#]{1,32})#(\\d{4})
const auto nonce = std::to_string(Snowflake::FromNow());
CreateMessageObject obj;
obj.Content = content;
m_http.MakePOST("/channels/" + std::to_string(channel) + "/messages", nlohmann::json(obj).dump(), [](auto) {});
obj.Nonce = nonce;
m_http.MakePOST("/channels/" + std::to_string(channel) + "/messages", nlohmann::json(obj).dump(), sigc::bind<0>(sigc::mem_fun(*this, &DiscordClient::ChatMessageCallback), nonce));
// dummy data so the content can be shown while waiting for MESSAGE_CREATE
Message tmp;
tmp.Content = content;
tmp.ID = nonce;
tmp.ChannelID = channel;
tmp.Author = GetUserData();
tmp.IsTTS = false;
tmp.DoesMentionEveryone = false;
tmp.Type = MessageType::DEFAULT;
tmp.IsPinned = false;
tmp.Timestamp = "2000-01-01T00:00:00.000000+00:00";
tmp.Nonce = obj.Nonce;
tmp.IsPending = true;
m_store.SetMessage(tmp.ID, tmp);
m_signal_message_sent.emit(tmp);
}
void DiscordClient::SendChatMessage(const std::string &content, Snowflake channel, Snowflake referenced_message) {
const auto nonce = std::to_string(Snowflake::FromNow());
CreateMessageObject obj;
obj.Content = content;
obj.Nonce = nonce;
obj.MessageReference.emplace().MessageID = referenced_message;
m_http.MakePOST("/channels/" + std::to_string(channel) + "/messages", nlohmann::json(obj).dump(), [](auto) {});
m_http.MakePOST("/channels/" + std::to_string(channel) + "/messages", nlohmann::json(obj).dump(), sigc::bind<0>(sigc::mem_fun(*this, &DiscordClient::ChatMessageCallback), nonce));
Message tmp;
tmp.Content = content;
tmp.ID = nonce;
tmp.ChannelID = channel;
tmp.Author = GetUserData();
tmp.IsTTS = false;
tmp.DoesMentionEveryone = false;
tmp.Type = MessageType::DEFAULT;
tmp.IsPinned = false;
tmp.Timestamp = "2000-01-01T00:00:00.000000+00:00";
tmp.Nonce = obj.Nonce;
tmp.IsPending = true;
m_store.SetMessage(tmp.ID, tmp);
m_signal_message_sent.emit(tmp);
}
void DiscordClient::DeleteMessage(Snowflake channel_id, Snowflake id) {
@ -1861,3 +1900,11 @@ DiscordClient::type_signal_guild_join_request_update DiscordClient::signal_guild
DiscordClient::type_signal_guild_join_request_delete DiscordClient::signal_guild_join_request_delete() {
return m_signal_guild_join_request_delete;
}
DiscordClient::type_signal_message_sent DiscordClient::signal_message_sent() {
return m_signal_message_sent;
}
DiscordClient::type_signal_message_send_fail DiscordClient::signal_message_send_fail() {
return m_signal_message_send_fail;
}

View File

@ -97,6 +97,8 @@ public:
Permission ComputeOverwrites(Permission base, Snowflake member_id, Snowflake channel_id) const;
bool CanManageMember(Snowflake guild_id, Snowflake actor, Snowflake target) const; // kick, ban, edit nickname (cant think of a better name)
void ChatMessageCallback(std::string nonce, const http::response_type &response);
void SendChatMessage(const std::string &content, Snowflake channel);
void SendChatMessage(const std::string &content, Snowflake channel, Snowflake referenced_message);
void DeleteMessage(Snowflake channel_id, Snowflake id);
@ -306,6 +308,8 @@ public:
typedef sigc::signal<void, GuildJoinRequestCreateData> type_signal_guild_join_request_create;
typedef sigc::signal<void, GuildJoinRequestUpdateData> type_signal_guild_join_request_update;
typedef sigc::signal<void, GuildJoinRequestDeleteData> type_signal_guild_join_request_delete;
typedef sigc::signal<void, Message> type_signal_message_sent;
typedef sigc::signal<void, std::string> type_signal_message_send_fail;
typedef sigc::signal<void, bool, GatewayCloseCode> type_signal_disconnected; // bool true if reconnecting
typedef sigc::signal<void> type_signal_connected;
@ -337,6 +341,8 @@ public:
type_signal_guild_join_request_create signal_guild_join_request_create();
type_signal_guild_join_request_update signal_guild_join_request_update();
type_signal_guild_join_request_delete signal_guild_join_request_delete();
type_signal_message_sent signal_message_sent();
type_signal_message_send_fail signal_message_send_fail();
type_signal_disconnected signal_disconnected();
type_signal_connected signal_connected();
@ -369,6 +375,8 @@ protected:
type_signal_guild_join_request_create m_signal_guild_join_request_create;
type_signal_guild_join_request_update m_signal_guild_join_request_update;
type_signal_guild_join_request_delete m_signal_guild_join_request_delete;
type_signal_message_sent m_signal_message_sent;
type_signal_message_send_fail m_signal_message_send_fail;
type_signal_disconnected m_signal_disconnected;
type_signal_connected m_signal_connected;
};

View File

@ -202,6 +202,8 @@ struct Message {
void from_json_edited(const nlohmann::json &j); // for MESSAGE_UPDATE
// custom fields to track changes
bool IsPending = false; // for user-sent messages yet to be received in a MESSAGE_CREATE
void SetDeleted();
void SetEdited();
bool IsDeleted() const;

View File

@ -191,6 +191,7 @@ void to_json(nlohmann::json &j, const HeartbeatMessage &m) {
void to_json(nlohmann::json &j, const CreateMessageObject &m) {
j["content"] = m.Content;
JS_IF("message_reference", m.MessageReference);
JS_IF("nonce", m.Nonce);
}
void to_json(nlohmann::json &j, const MessageEditObject &m) {

View File

@ -291,6 +291,7 @@ struct HeartbeatMessage : GatewayMessage {
struct CreateMessageObject {
std::string Content;
std::optional<MessageReferenceData> MessageReference;
std::optional<std::string> Nonce;
friend void to_json(nlohmann::json &j, const CreateMessageObject &m);
};

View File

@ -1,6 +1,9 @@
#include "snowflake.hpp"
#include <ctime>
#include <iomanip>
#include <chrono>
constexpr static uint64_t DiscordEpochSeconds = 1420070400;
Snowflake::Snowflake()
: m_num(Invalid) {}
@ -21,6 +24,18 @@ Snowflake::Snowflake(const Glib::ustring &str) {
m_num = Invalid;
};
Snowflake Snowflake::FromNow() {
using namespace std::chrono;
// not guaranteed to work but it probably will anyway
static uint64_t counter = 0;
const auto millis_since_epoch = static_cast<uint64_t>(duration_cast<milliseconds>(system_clock::now().time_since_epoch()).count());
const auto epoch = millis_since_epoch - DiscordEpochSeconds * 1000;
uint64_t snowflake = epoch << 22;
// worker id and process id would be OR'd in here but there's no point
snowflake |= counter++ % 4096;
return snowflake;
}
bool Snowflake::IsValid() const {
return m_num != Invalid;
}

View File

@ -9,6 +9,8 @@ struct Snowflake {
Snowflake(const std::string &str);
Snowflake(const Glib::ustring &str);
static Snowflake FromNow(); // not thread safe
bool IsValid() const;
std::string GetLocalTimestamp() const;

View File

@ -259,6 +259,8 @@ void Store::SetMessage(Snowflake id, const Message &message) {
Bind(m_set_msg_stmt, 20, nullptr);
Bind(m_set_msg_stmt, 21, message.IsDeleted());
Bind(m_set_msg_stmt, 22, message.IsEdited());
Bind(m_set_msg_stmt, 23, message.IsPending);
Bind(m_set_msg_stmt, 24, message.Nonce); // sorry
if (!RunInsert(m_set_msg_stmt))
fprintf(stderr, "message insert failed: %s\n", sqlite3_errstr(m_db_err));
@ -567,6 +569,9 @@ std::optional<Message> Store::GetMessage(Snowflake id) const {
Get(m_get_msg_stmt, 21, tmpb);
if (tmpb) ret.SetEdited();
Get(m_get_msg_stmt, 22, ret.IsPending);
Get(m_get_msg_stmt, 23, ret.Nonce);
Reset(m_get_msg_stmt);
if (ret.MessageReference.has_value() && ret.MessageReference->MessageID.has_value()) {
@ -749,9 +754,10 @@ bool Store::CreateTables() {
flags INTEGER,
stickers TEXT, /* json */
reactions TEXT, /* json */
/* extra */
deleted BOOL,
edited BOOL
deleted BOOL, /* extra */
edited BOOL, /* extra */
pending BOOL, /* extra */
nonce TEXT
)
)";
@ -951,7 +957,7 @@ bool Store::CreateStatements() {
constexpr const char *set_msg = R"(
REPLACE INTO messages VALUES (
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
)
)";