20 Commits

Author SHA1 Message Date
ouwou
190118bb58 try optimizing user fetch 2022-12-18 19:59:48 -05:00
ouwou
6926b12a50 fix build 2022-12-18 17:57:43 -05:00
ouwou
fb010b5ac5 Merge branch 'master' into member-list 2022-12-18 17:54:19 -05:00
ouwou
acb03642c2 Merge pull request #124 from uowuo/keychain
store token in keychain
2022-12-18 20:52:19 +00:00
ouwou
ba2aea86f9 Merge branch 'master' into keychain 2022-12-16 19:28:39 -05:00
abdalrzag eisa
c704703b14 doc: Display CSS selectors and Settings as tables (#126) 2022-12-03 23:18:14 +00:00
Altoids1
33a329f16a Fixes a few minor typos in the README (#127) 2022-12-02 20:59:40 +00:00
abdalrzag eisa
f0df06a795 doc: list deps for building on Arch Linux (#125) 2022-12-02 08:57:29 +00:00
ouwou
9ae41b7335 Merge branch 'master' into keychain 2022-12-01 20:00:44 -05:00
ouwou
b9fee0f6c9 Merge branch 'master' of https://github.com/uowuo/abaddon 2022-12-01 02:25:35 -05:00
ouwou
92273829bb update ci run to latest nlohmann/json release 2022-12-01 01:33:10 -05:00
abdalrzag eisa
86f6f81d9b add missing setting (#123) 2022-12-01 00:56:22 +00:00
ouwou
573a619191 Merge branch 'master' into keychain 2022-11-29 15:53:02 -05:00
abdalrzag eisa
c5807a3463 Make README.md more readable (#120)
* More noticeable warnings
* Make CSS selectors stand out more from their description
* Make Settings options stand out more from their description, and make the default value easy to see
2022-11-15 07:47:16 +00:00
ouwou
b0370ee489 Merge branch 'master' into member-list 2022-11-03 16:50:30 -04:00
ouwou
2a9f49a148 add menu item + shortcuts to hide channel and member lists (closes #118) 2022-11-03 00:45:45 -04:00
ouwou
00fe6642a9 basic functionality 2022-09-11 02:46:30 -04:00
ouwou
77dd9fabfa change service clear user 2022-08-09 02:06:24 -04:00
ouwou
ee67037a3f store token in keychain 2022-08-08 23:25:34 -04:00
ouwou
b46cf53be5 add hrantzsch/keychain and link 2022-08-08 22:50:27 -04:00
16 changed files with 629 additions and 334 deletions

View File

@@ -160,7 +160,7 @@ jobs:
cd deps
git clone https://github.com/nlohmann/json
cd json
git checkout db78ac1d7716f56fc9f1b030b715f872f93964e4
git checkout bc889afb4c5bf1c0d8ee29ef35eaaf4c8bef8a5d
mkdir build
cd build
cmake ..

3
.gitmodules vendored
View File

@@ -4,3 +4,6 @@
[submodule "subprojects/ixwebsocket"]
path = subprojects/ixwebsocket
url = https://github.com/machinezone/ixwebsocket
[submodule "subprojects/keychain"]
path = subprojects/keychain
url = https://github.com/hrantzsch/keychain

View File

@@ -8,6 +8,7 @@ set(CMAKE_CXX_STANDARD 17)
set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${CMAKE_SOURCE_DIR}/cmake/")
option(USE_LIBHANDY "Enable features that require libhandy (default)" ON)
option(USE_KEYCHAIN "Store the token in the keychain (default)" ON)
find_package(nlohmann_json REQUIRED)
find_package(CURL)
@@ -106,3 +107,13 @@ if (USE_LIBHANDY)
target_compile_definitions(abaddon PRIVATE WITH_LIBHANDY)
endif ()
endif ()
if (USE_KEYCHAIN)
find_package(keychain QUIET)
if (NOT keychain_FOUND)
message("keychain was not found and will be included as a submodule")
add_subdirectory(subprojects/keychain)
target_link_libraries(abaddon keychain)
target_compile_definitions(abaddon PRIVATE WITH_KEYCHAIN)
endif ()
endif ()

245
README.md
View File

@@ -75,6 +75,10 @@ the result of fundamental issues with Discord's thread implementation.
```Shell
$ sudo apt install g++ cmake libgtkmm-3.0-dev libcurl4-gnutls-dev libsqlite3-dev libssl-dev nlohmann-json3-dev
```
* On Arch Linux
```Shell
$ sudo pacman -S gcc cmake gtkmm3 libcurl-gnutls lib32-sqlite lib32-openssl nlohmann-json libhandy
```
2. `git clone https://github.com/uowuo/abaddon --recurse-submodules="subprojects" && cd abaddon`
3. `mkdir build && cd build`
4. `cmake ..`
@@ -92,7 +96,7 @@ Latest release version: https://github.com/uowuo/abaddon/releases/latest
- Linux: [here](https://nightly.link/uowuo/abaddon/workflows/ci/master/build-linux-MinSizeRel.zip) unpackaged (for now),
requires gtkmm3. built on Ubuntu 18.04 + gcc9
⚠️ If you use Windows, make sure to start from the `bin` directory
> **Warning**: If you use Windows, make sure to start from the `bin` directory
On Linux, `css` and `res` can also be loaded from `~/.local/share/abaddon` or `/usr/share/abaddon`
@@ -135,145 +139,158 @@ spam filter's wrath:
#### CSS selectors
.app-window - Applied to all windows. This means the main window and all popups
.app-popup - Additional class for `.app-window`s when the window is not the main window
| Selector | Description |
|--------------------------------|---------------------------------------------------------------------------------------------------|
| `.app-window` | Applied to all windows. This means the main window and all popups |
| `.app-popup` | Additional class for `.app-window`s when the window is not the main window |
| `.channel-list` | Container of the channel list |
| `.messages` | Container of user messages |
| `.message-container` | The container which holds a user's messages |
| `.message-container-author` | The author label for a message container |
| `.message-container-timestamp` | The timestamp label for a message container |
| `.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 |
| `.replying` | Extra class for chat input container when a reply is currently being created |
| `.reaction-box` | Contains a reaction image and the count |
| `.reacted` | Additional class for reaction-box when the user has reacted with a particular reaction |
| `.reaction-count` | Contains the count for reaction |
| `.completer` | Container for the message completer |
| `.completer-entry` | Container for a single entry in the completer |
| `.completer-entry-label` | Contains the label for an entry in the completer |
| `.completer-entry-image` | Contains the image for an entry in the completer |
| `.embed` | Container for a message embed |
| `.embed-author` | The author of an embed |
| `.embed-title` | The title of an embed |
| `.embed-description` | The description of an embed |
| `.embed-field-title` | The title of an embed field |
| `.embed-field-value` | The value of an embed field |
| `.embed-footer` | The footer of an embed |
| `.members` | Container of the member list |
| `.members-row` | All rows within the members container |
| `.members-row-label` | All labels in the members container |
| `.members-row-member` | Rows containing a member |
| `.members-row-role` | Rows containing a role |
| `.members-row-avatar` | Contains the avatar for a row in the member list |
| `.status-indicator` | The status indicator |
| `.online` | Applied to status indicators when the associated user is online |
| `.idle` | Applied to status indicators when the associated user is away |
| `.dnd` | Applied to status indicators when the associated user is on do not disturb |
| `.offline` | Applied to status indicators when the associated user is offline |
| `.typing-indicator` | The typing indicator (also used for replies) |
.channel-list - Container of the channel list
Used in reorderable list implementation:
| Selector |
|----------------------|
| `.drag-icon` |
| `.drag-hover-top` |
| `.drag-hover-bottom` |
.messages - Container of user messages
.message-container - The container which holds a user's messages
.message-container-author - The author label for a message container
.message-container-timestamp - The timestamp label for a message container
.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
.replying - Extra class for chat input container when a reply is currently being created
.reaction-box - Contains a reaction image and the count
.reacted - Additional class for reaction-box when the user has reacted with a particular reaction
.reaction-count - Contains the count for reaction
Used in guild settings popup:
.completer - Container for the message completer
.completer-entry - Container for a single entry in the completer
.completer-entry-label - Contains the label for an entry in the completer
.completer-entry-image - Contains the image for an entry in the completer
| Selector | Description |
|----------------------------|---------------------------------------------------|
| `.guild-settings-window` | Container for list of members in the members pane |
| `.guild-members-pane-list` | |
| `.guild-members-pane-info` | Container for member info |
| `.guild-roles-pane-list` | Container for list of roles in the roles pane |
.embed - Container for a message embed
.embed-author - The author of an embed
.embed-title - The title of an embed
.embed-description - The description of an embed
.embed-field-title - The title of an embed field
.embed-field-value - The value of an embed field
.embed-footer - The footer of an embed
Used in profile popup:
.members - Container of the member list
.members-row - All rows within the members container
.members-row-label - All labels in the members container
.members-row-member - Rows containing a member
.members-row-role - Rows containing a role
.members-row-avatar - Contains the avatar for a row in the member list
.status-indicator - The status indicator
.online - Applied to status indicators when the associated user is online
.idle - Applied to status indicators when the associated user is away
.dnd - Applied to status indicators when the associated user is on do not disturb
.offline - Applied to status indicators when the associated user is offline
.typing-indicator - The typing indicator (also used for replies)
Used in reorderable list implementation:
.drag-icon .drag-hover-top .drag-hover-bottom
Used in guild settings popup:
.guild-settings-window
.guild-members-pane-list - Container for list of members in the members pane
.guild-members-pane-info - Container for member info
.guild-roles-pane-list - Container for list of roles in the roles pane
Used in profile popup:
.mutual-friend-item - Applied to every item in the mutual friends list
.mutual-friend-item-name - Name in mutual friend item
.mutual-friend-item-avatar - Avatar in mutual friend item
.mutual-guild-item - Applied to every item in the mutual guilds list
.mutual-guild-item-name - Name in mutual guild item
.mutual-guild-item-icon - Icon in mutual guild item
.mutual-guild-item-nick - User nickname in mutual guild item
.profile-connection - Applied to every item in the user connections list
.profile-connection-label - Label in profile connection item
.profile-connection-check - Checkmark in verified profile connection items
.profile-connections - Container for profile connections
.profile-notes - Container for notes in profile window
.profile-notes-label - Label that says "NOTE"
.profile-notes-text - Actual note text
.profile-info-pane - Applied to container for info section of profile popup
.profile-info-created - Label for creation date of profile
.user-profile-window
.profile-main-container - Inner container for profile
.profile-avatar
.profile-username
.profile-switcher - Buttons used to switch viewed section of profile
.profile-stack - Container for profile info that can be switched between
.profile-badges - Container for badges
.profile-badge
| Selector | Description |
|------------------------------|---------------------------------------------------------|
| `.mutual-friend-item` | Applied to every item in the mutual friends list |
| `.mutual-friend-item-name` | Name in mutual friend item |
| `.mutual-friend-item-avatar` | Avatar in mutual friend item |
| `.mutual-guild-item` | Applied to every item in the mutual guilds list |
| `.mutual-guild-item-name` | Name in mutual guild item |
| `.mutual-guild-item-icon` | Icon in mutual guild item |
| `.mutual-guild-item-nick` | User nickname in mutual guild item |
| `.profile-connection` | Applied to every item in the user connections list |
| `.profile-connection-label` | Label in profile connection item |
| `.profile-connection-check` | Checkmark in verified profile connection items |
| `.profile-connections` | Container for profile connections |
| `.profile-notes` | Container for notes in profile window |
| `.profile-notes-label` | Label that says "NOTE" |
| `.profile-notes-text` | Actual note text |
| `.profile-info-pane` | Applied to container for info section of profile popup |
| `.profile-info-created` | Label for creation date of profile |
| `.user-profile-window` | |
| `.profile-main-container` | Inner container for profile |
| `.profile-avatar` | |
| `.profile-username` | |
| `.profile-switcher` | Buttons used to switch viewed section of profile |
| `.profile-stack` | Container for profile info that can be switched between |
| `.profile-badges` | Container for badges |
| `.profile-badge` | |
### Settings
Settings are configured (for now) by editing abaddon.ini
Settings are configured (for now) by editing `abaddon.ini`.
The format is similar to the standard Windows ini format **except**:
* `#` is used to begin comments as opposed to `;`
* Section and key names are case-sensitive
You should edit these while the client is closed even though there's an option to reload while running
This listing is organized by section.
> **Warning**: You should edit these while the client is closed, even though there's an option to reload while running.
This listing is organized by section.
For example, memory_db would be set by adding `memory_db = true` under the line `[discord]`
#### discord
* gateway (string) - override url for Discord gateway. must be json format and use zlib stream compression
* api_base (string) - override base url for Discord API
* memory_db (true or false, default false) - if true, Discord data will be kept in memory as opposed to on disk
* token (string) - Discord token used to login, this can be set from the menu
* prefetch (true or false, default false) - if true, new messages will cause the avatar and image attachments to be
automatically downloaded
| Setting | Type | Default | Description |
|---------------|---------|---------|--------------------------------------------------------------------------------------------------|
| `gateway` | string | | override url for Discord gateway. must be json format and use zlib stream compression |
| `api_base` | string | | override base url for Discord API |
| `memory_db` | boolean | false | if true, Discord data will be kept in memory as opposed to on disk |
| `token` | string | | Discord token used to login, this can be set from the menu |
| `prefetch` | boolean | false | if true, new messages will cause the avatar and image attachments to be automatically downloaded |
| `autoconnect` | boolean | false | autoconnect to discord |
#### http
* user_agent (string) - sets the user-agent to use in HTTP requests to the Discord API (not including media/images)
* concurrent (int, default 20) - how many images can be concurrently retrieved
| Setting | Type | Default | Description |
|--------------|--------|---------|---------------------------------------------------------------------------------------------|
| `user_agent` | string | | sets the user-agent to use in HTTP requests to the Discord API (not including media/images) |
| `concurrent` | int | 20 | how many images can be concurrently retrieved |
#### gui
* member_list_discriminator (true or false, default true) - show user discriminators in the member list
* stock_emojis (true or false, default true) - allow abaddon to substitute unicode emojis with images from emojis.bin,
must be false to allow GTK to render emojis itself
* custom_emojis (true or false, default true) - download and use custom Discord emojis
* css (string) - path to the main CSS file
* animations (true or false, default true) - use animated images where available (e.g. server icons, emojis, avatars).
false means static images will be used
* animated_guild_hover_only (true or false, default true) - only animate guild icons when the guild is being hovered
over
* owner_crown (true or false, default true) - show a crown next to the owner
* unreads (true or false, default true) - show unread indicators and mention badges
* save_state (true or false, default true) - save the state of the gui (active channels, tabs, expanded channels)
* alt_menu (true or false, default false) - keep the menu hidden unless revealed with alt key
* hide_to_tray (true or false, default false) - hide abaddon to the system tray on window close
| Setting | Type | Default | Description |
|-----------------------------|---------|---------|----------------------------------------------------------------------------------------------------------------------------|
| `member_list_discriminator` | boolean | true | show user discriminators in the member list |
| `stock_emojis` | boolean | true | allow abaddon to substitute unicode emojis with images from emojis.bin, must be false to allow GTK to render emojis itself |
| `custom_emojis` | boolean | true | download and use custom Discord emojis |
| `css` | string | | path to the main CSS file |
| `animations` | boolean | true | use animated images where available (e.g. server icons, emojis, avatars). false means static images will be used |
| `animated_guild_hover_only` | boolean | true | only animate guild icons when the guild is being hovered over |
| `owner_crown` | boolean | true | show a crown next to the owner |
| `unreads` | boolean | true | show unread indicators and mention badges |
| `save_state` | boolean | true | save the state of the gui (active channels, tabs, expanded channels) |
| `alt_menu` | boolean | false | keep the menu hidden unless revealed with alt key |
| `hide_to_tray` | boolean | false | hide abaddon to the system tray on window close |
#### style
* linkcolor (string) - color to use for links in messages
* expandercolor (string) - color to use for the expander in the channel list
* nsfwchannelcolor (string) - color to use for NSFW channels in the channel list
* channelcolor (string) - color to use for SFW channels in the channel list
* mentionbadgecolor (string) - background color for mention badges
* mentionbadgetextcolor (string) - color to use for number displayed on mention badges
* unreadcolor (string) - color to use for the unread indicator
| Setting | Type | Description |
|-------------------------|--------|-----------------------------------------------------|
| `linkcolor` | string | color to use for links in messages |
| `expandercolor` | string | color to use for the expander in the channel list |
| `nsfwchannelcolor` | string | color to use for NSFW channels in the channel list |
| `channelcolor` | string | color to use for SFW channels in the channel list |
| `mentionbadgecolor` | string | background color for mention badges |
| `mentionbadgetextcolor` | string | color to use for number displayed on mention badges |
| `unreadcolor` | string | color to use for the unread indicator |
### Environment variables
* ABADDON_NO_FC (Windows only) - don't use custom font config
* ABADDON_CONFIG - change path of configuration file to use. relative to cwd or can be absolute
| variable | Description |
|------------------|------------------------------------------------------------------------------|
| `ABADDON_NO_FC` | (Windows only) don't use custom font config |
| `ABADDON_CONFIG` | change path of configuration file to use. relative to cwd or can be absolute |

View File

@@ -1,85 +1,32 @@
#include "memberlist.hpp"
#include "abaddon.hpp"
#include "util.hpp"
#include "lazyimage.hpp"
#include "statusindicator.hpp"
constexpr static const int MaxMemberListRows = 200;
MemberList::MemberList()
: m_model(Gtk::TreeStore::create(m_columns)) {
add(m_view);
show_all_children();
MemberListUserRow::MemberListUserRow(const std::optional<GuildData> &guild, const UserData &data) {
ID = data.ID;
m_ev = Gtk::manage(new Gtk::EventBox);
m_box = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL));
m_label = Gtk::manage(new Gtk::Label);
m_avatar = Gtk::manage(new LazyImage(16, 16));
m_status_indicator = Gtk::manage(new StatusIndicator(ID));
m_view.set_activate_on_single_click(true);
m_view.set_hexpand(true);
m_view.set_vexpand(true);
if (Abaddon::Get().GetSettings().ShowOwnerCrown && guild.has_value() && guild->OwnerID == data.ID) {
try {
const static auto crown_path = Abaddon::GetResPath("/crown.png");
auto pixbuf = Gdk::Pixbuf::create_from_file(crown_path, 12, 12);
m_crown = Gtk::manage(new Gtk::Image(pixbuf));
m_crown->set_valign(Gtk::ALIGN_CENTER);
m_crown->set_margin_end(8);
} catch (...) {}
}
m_view.set_show_expanders(false);
m_view.set_enable_search(false);
m_view.set_headers_visible(false);
m_view.set_model(m_model);
m_status_indicator->set_margin_start(3);
m_model->set_sort_column(m_columns.m_sort, Gtk::SORT_DESCENDING);
if (guild.has_value())
m_avatar->SetURL(data.GetAvatarURL(guild->ID, "png"));
else
m_avatar->SetURL(data.GetAvatarURL("png"));
auto *column = Gtk::make_managed<Gtk::TreeView::Column>("display");
auto *renderer = Gtk::make_managed<CellRendererMemberList>();
column->pack_start(*renderer);
column->add_attribute(renderer->property_type(), m_columns.m_type);
column->add_attribute(renderer->property_id(), m_columns.m_id);
column->add_attribute(renderer->property_markup(), m_columns.m_markup);
column->add_attribute(renderer->property_icon(), m_columns.m_icon);
m_view.append_column(*column);
get_style_context()->add_class("members-row");
get_style_context()->add_class("members-row-member");
m_label->get_style_context()->add_class("members-row-label");
m_avatar->get_style_context()->add_class("members-row-avatar");
m_label->set_single_line_mode(true);
m_label->set_ellipsize(Pango::ELLIPSIZE_END);
std::string display = data.Username;
if (Abaddon::Get().GetSettings().ShowMemberListDiscriminators)
display += "#" + data.Discriminator;
if (guild.has_value()) {
if (const auto col_id = data.GetHoistedRole(guild->ID, true); col_id.IsValid()) {
auto color = Abaddon::Get().GetDiscordClient().GetRole(col_id)->Color;
m_label->set_use_markup(true);
m_label->set_markup("<span color='#" + IntToCSSColor(color) + "'>" + Glib::Markup::escape_text(display) + "</span>");
} else {
m_label->set_text(display);
}
} else {
m_label->set_text(display);
}
m_label->set_halign(Gtk::ALIGN_START);
m_box->add(*m_avatar);
m_box->add(*m_status_indicator);
m_box->add(*m_label);
if (m_crown != nullptr)
m_box->add(*m_crown);
m_ev->add(*m_box);
add(*m_ev);
show_all();
}
MemberList::MemberList() {
m_main = Gtk::manage(new Gtk::ScrolledWindow);
m_listbox = Gtk::manage(new Gtk::ListBox);
m_listbox->get_style_context()->add_class("members");
m_listbox->set_selection_mode(Gtk::SELECTION_NONE);
m_main->set_policy(Gtk::POLICY_NEVER, Gtk::POLICY_AUTOMATIC);
m_main->add(*m_listbox);
m_main->show_all();
}
Gtk::Widget *MemberList::GetRoot() const {
return m_main;
m_view.expand_all();
}
void MemberList::Clear() {
@@ -97,131 +44,110 @@ void MemberList::SetActiveChannel(Snowflake id) {
}
void MemberList::UpdateMemberList() {
m_id_to_row.clear();
auto children = m_listbox->get_children();
auto it = children.begin();
while (it != children.end()) {
delete *it;
it++;
}
if (!Abaddon::Get().GetDiscordClient().IsStarted()) return;
if (!m_chan_id.IsValid()) return;
m_model->clear();
auto &discord = Abaddon::Get().GetDiscordClient();
const auto chan = discord.GetChannel(m_chan_id);
if (!chan.has_value()) return;
if (chan->Type == ChannelType::DM || chan->Type == ChannelType::GROUP_DM) {
int num_rows = 0;
for (const auto &user : chan->GetDMRecipients()) {
if (num_rows++ > MaxMemberListRows) break;
auto *row = Gtk::manage(new MemberListUserRow(std::nullopt, user));
m_id_to_row[user.ID] = row;
AttachUserMenuHandler(row, user.ID);
m_listbox->add(*row);
if (!discord.IsStarted()) return;
const auto channel = discord.GetChannel(m_chan_id);
if (!channel.has_value()) return;
// dm
if (channel->IsDM()) {
// todo eliminate for dm
auto everyone_row = *m_model->append();
everyone_row[m_columns.m_type] = MemberListRenderType::Role;
everyone_row[m_columns.m_id] = Snowflake::Invalid;
everyone_row[m_columns.m_markup] = "<b>Users</b>";
for (const auto &user : channel->GetDMRecipients()) {
auto row = *m_model->append(everyone_row.children());
row[m_columns.m_type] = MemberListRenderType::Member;
row[m_columns.m_id] = user.ID;
row[m_columns.m_markup] = Glib::Markup::escape_text(user.Username + "#" + user.Discriminator);
row[m_columns.m_icon] = Abaddon::Get().GetImageManager().GetPlaceholder(16);
}
return;
}
std::set<Snowflake> ids;
if (chan->IsThread()) {
const auto x = discord.GetUsersInThread(m_chan_id);
ids = { x.begin(), x.end() };
} else
ids = discord.GetUsersInGuild(m_guild_id);
std::vector<UserData> users;
if (channel->IsThread()) {
// auto x = discord.GetUsersInThread(m_chan_id);
// ids = { x.begin(), x.end() };
} else {
users = discord.GetUserDataInGuildBulk(m_guild_id);
}
// process all the shit first so its in proper order
std::map<int, RoleData> pos_to_role;
std::map<int, std::vector<UserData>> pos_to_users;
std::unordered_map<Snowflake, int> user_to_color;
// std::unordered_map<Snowflake, int> user_to_color;
std::vector<Snowflake> roleless_users;
for (const auto &id : ids) {
auto user = discord.GetUser(id);
if (!user.has_value() || user->IsDeleted())
for (const auto &user : users) {
if (user.IsDeleted())
continue;
auto pos_role_id = discord.GetMemberHoistedRole(m_guild_id, id); // role for positioning
auto col_role_id = discord.GetMemberHoistedRole(m_guild_id, id, true); // role for color
auto pos_role_id = discord.GetMemberHoistedRole(m_guild_id, user.ID); // role for positioning
// auto col_role_id = discord.GetMemberHoistedRole(m_guild_id, id, true); // role for color
auto pos_role = discord.GetRole(pos_role_id);
auto col_role = discord.GetRole(col_role_id);
// auto col_role = discord.GetRole(col_role_id);
if (!pos_role.has_value()) {
roleless_users.push_back(id);
roleless_users.push_back(user.ID);
continue;
}
pos_to_role[pos_role->Position] = *pos_role;
pos_to_users[pos_role->Position].push_back(std::move(*user));
if (col_role.has_value())
user_to_color[id] = col_role->Color;
pos_to_users[pos_role->Position].push_back(user);
// if (col_role.has_value())
// user_to_color[id] = col_role->Color;
}
int num_rows = 0;
const auto guild = discord.GetGuild(m_guild_id);
if (!guild.has_value()) return;
auto add_user = [this, &num_rows, guild](const UserData &data) -> bool {
if (num_rows++ > MaxMemberListRows) return false;
auto *row = Gtk::manage(new MemberListUserRow(*guild, data));
m_id_to_row[data.ID] = row;
AttachUserMenuHandler(row, data.ID);
m_listbox->add(*row);
auto add_user = [this](const UserData &user, const Gtk::TreeNodeChildren &node) -> bool {
auto row = *m_model->append(node);
row[m_columns.m_type] = MemberListRenderType::Member;
row[m_columns.m_id] = user.ID;
row[m_columns.m_markup] = user.GetEscapedName() + "#" + user.Discriminator;
row[m_columns.m_sort] = static_cast<int>(user.ID);
row[m_columns.m_icon] = Abaddon::Get().GetImageManager().GetPlaceholder(16);
// come on
Gtk::TreeRowReference ref(m_model, m_model->get_path(row));
Abaddon::Get().GetImageManager().LoadFromURL(user.GetAvatarURL(), [this, ref = std::move(ref)](const Glib::RefPtr<Gdk::Pixbuf> &pb) {
if (ref.is_valid()) {
auto row = *m_model->get_iter(ref.get_path());
row[m_columns.m_icon] = pb->scale_simple(16, 16, Gdk::INTERP_BILINEAR);
}
});
return true;
};
auto add_role = [this](const std::string &name) {
auto *role_row = Gtk::manage(new Gtk::ListBoxRow);
auto *role_lbl = Gtk::manage(new Gtk::Label);
role_row->get_style_context()->add_class("members-row");
role_row->get_style_context()->add_class("members-row-role");
role_lbl->get_style_context()->add_class("members-row-label");
role_lbl->set_single_line_mode(true);
role_lbl->set_ellipsize(Pango::ELLIPSIZE_END);
role_lbl->set_use_markup(true);
role_lbl->set_markup("<b>" + Glib::Markup::escape_text(name) + "</b>");
role_lbl->set_halign(Gtk::ALIGN_START);
role_row->add(*role_lbl);
role_row->show_all();
m_listbox->add(*role_row);
auto add_role = [this](const RoleData &role) -> Gtk::TreeRow {
auto row = *m_model->append();
row[m_columns.m_type] = MemberListRenderType::Role;
row[m_columns.m_id] = role.ID;
row[m_columns.m_markup] = "<b>" + role.GetEscapedName() + "</b>";
row[m_columns.m_sort] = role.Position;
return row;
};
for (auto it = pos_to_role.crbegin(); it != pos_to_role.crend(); it++) {
auto pos = it->first;
const auto &role = it->second;
add_role(role.Name);
if (pos_to_users.find(pos) == pos_to_users.end()) continue;
auto &users = pos_to_users.at(pos);
AlphabeticalSort(users.begin(), users.end(), [](const auto &e) { return e.Username; });
for (const auto &data : users)
if (!add_user(data)) return;
}
if (chan->Type == ChannelType::DM || chan->Type == ChannelType::GROUP_DM)
add_role("Users");
else
add_role("@everyone");
for (const auto &id : roleless_users) {
const auto user = discord.GetUser(id);
if (user.has_value())
if (!add_user(*user)) return;
}
}
void MemberList::AttachUserMenuHandler(Gtk::ListBoxRow *row, Snowflake id) {
row->signal_button_press_event().connect([this, id](GdkEventButton *e) -> bool {
if (e->type == GDK_BUTTON_PRESS && e->button == GDK_BUTTON_SECONDARY) {
Abaddon::Get().ShowUserMenu(reinterpret_cast<const GdkEvent *>(e), id, m_guild_id);
return true;
for (auto &[pos, role] : pos_to_role) {
auto role_children = add_role(role).children();
if (auto it = pos_to_users.find(pos); it != pos_to_users.end()) {
for (const auto &user : it->second) {
if (!add_user(user, role_children)) break;
}
}
}
return false;
});
m_view.expand_all();
}
MemberList::ModelColumns::ModelColumns() {
add(m_type);
add(m_id);
add(m_markup);
add(m_icon);
add(m_sort);
}

View File

@@ -1,44 +1,37 @@
#pragma once
#include <gtkmm.h>
#include <mutex>
#include <unordered_map>
#include <optional>
#include "discord/discord.hpp"
#include <variant>
#include <gtkmm/scrolledwindow.h>
#include <gtkmm/treemodel.h>
#include <gtkmm/treestore.h>
#include <gtkmm/treeview.h>
#include "discord/snowflake.hpp"
#include "memberlistcellrenderer.hpp"
class LazyImage;
class StatusIndicator;
class MemberListUserRow : public Gtk::ListBoxRow {
public:
MemberListUserRow(const std::optional<GuildData> &guild, const UserData &data);
Snowflake ID;
private:
Gtk::EventBox *m_ev;
Gtk::Box *m_box;
LazyImage *m_avatar;
StatusIndicator *m_status_indicator;
Gtk::Label *m_label;
Gtk::Image *m_crown = nullptr;
};
class MemberList {
class MemberList : public Gtk::ScrolledWindow {
public:
MemberList();
Gtk::Widget *GetRoot() const;
void UpdateMemberList();
void Clear();
void SetActiveChannel(Snowflake id);
private:
void AttachUserMenuHandler(Gtk::ListBoxRow *row, Snowflake id);
class ModelColumns : public Gtk::TreeModel::ColumnRecord {
public:
ModelColumns();
Gtk::ScrolledWindow *m_main;
Gtk::ListBox *m_listbox;
Gtk::TreeModelColumn<MemberListRenderType> m_type;
Gtk::TreeModelColumn<uint64_t> m_id;
Gtk::TreeModelColumn<Glib::ustring> m_markup;
Gtk::TreeModelColumn<Glib::RefPtr<Gdk::Pixbuf>> m_icon;
Gtk::TreeModelColumn<int> m_sort;
};
ModelColumns m_columns;
Glib::RefPtr<Gtk::TreeStore> m_model;
Gtk::TreeView m_view;
Snowflake m_guild_id;
Snowflake m_chan_id;
std::unordered_map<Snowflake, Gtk::ListBoxRow *> m_id_to_row;
};

View File

@@ -0,0 +1,177 @@
#include "memberlistcellrenderer.hpp"
#include <gdkmm/general.h>
CellRendererMemberList::CellRendererMemberList()
: Glib::ObjectBase(typeid(CellRendererMemberList))
, m_property_type(*this, "render-type")
, m_property_id(*this, "id")
, m_property_markup(*this, "markup")
, m_property_icon(*this, "pixbuf") {
m_renderer_text.property_ellipsize() = Pango::ELLIPSIZE_END;
m_property_markup.get_proxy().signal_changed().connect([this]() {
m_renderer_text.property_markup() = m_property_markup;
});
}
Glib::PropertyProxy<MemberListRenderType> CellRendererMemberList::property_type() {
return m_property_type.get_proxy();
}
Glib::PropertyProxy<uint64_t> CellRendererMemberList::property_id() {
return m_property_id.get_proxy();
}
Glib::PropertyProxy<Glib::ustring> CellRendererMemberList::property_markup() {
return m_property_markup.get_proxy();
}
Glib::PropertyProxy<Glib::RefPtr<Gdk::Pixbuf>> CellRendererMemberList::property_icon() {
return m_property_icon.get_proxy();
}
void CellRendererMemberList::get_preferred_width_vfunc(Gtk::Widget &widget, int &minimum_width, int &natural_width) const {
switch (m_property_type.get_value()) {
case MemberListRenderType::Member:
return get_preferred_width_vfunc_member(widget, minimum_width, natural_width);
case MemberListRenderType::Role:
return get_preferred_width_vfunc_role(widget, minimum_width, natural_width);
}
}
void CellRendererMemberList::get_preferred_width_for_height_vfunc(Gtk::Widget &widget, int height, int &minimum_width, int &natural_width) const {
switch (m_property_type.get_value()) {
case MemberListRenderType::Member:
return get_preferred_width_for_height_vfunc_member(widget, height, minimum_width, natural_width);
case MemberListRenderType::Role:
return get_preferred_width_for_height_vfunc_role(widget, height, minimum_width, natural_width);
}
}
void CellRendererMemberList::get_preferred_height_vfunc(Gtk::Widget &widget, int &minimum_height, int &natural_height) const {
switch (m_property_type.get_value()) {
case MemberListRenderType::Member:
return get_preferred_height_vfunc_member(widget, minimum_height, natural_height);
case MemberListRenderType::Role:
return get_preferred_height_vfunc_role(widget, minimum_height, natural_height);
}
}
void CellRendererMemberList::get_preferred_height_for_width_vfunc(Gtk::Widget &widget, int width, int &minimum_height, int &natural_height) const {
switch (m_property_type.get_value()) {
case MemberListRenderType::Member:
return get_preferred_height_for_width_vfunc_member(widget, width, minimum_height, natural_height);
case MemberListRenderType::Role:
return get_preferred_height_for_width_vfunc_role(widget, width, minimum_height, natural_height);
}
}
void CellRendererMemberList::render_vfunc(const Cairo::RefPtr<Cairo::Context> &cr, Gtk::Widget &widget, const Gdk::Rectangle &background_area, const Gdk::Rectangle &cell_area, Gtk::CellRendererState flags) {
switch (m_property_type.get_value()) {
case MemberListRenderType::Member:
return render_vfunc_member(cr, widget, background_area, cell_area, flags);
case MemberListRenderType::Role:
return render_vfunc_role(cr, widget, background_area, cell_area, flags);
}
}
void CellRendererMemberList::get_preferred_width_vfunc_role(Gtk::Widget &widget, int &minimum_width, int &natural_width) const {
int text_min, text_nat;
m_renderer_text.get_preferred_width(widget, text_min, text_nat);
int xpad, ypad;
get_padding(xpad, ypad);
minimum_width = text_min + xpad * 2;
natural_width = text_nat + xpad * 2;
}
void CellRendererMemberList::get_preferred_width_for_height_vfunc_role(Gtk::Widget &widget, int height, int &minimum_width, int &natural_width) const {
get_preferred_width_vfunc_role(widget, minimum_width, natural_width);
}
void CellRendererMemberList::get_preferred_height_vfunc_role(Gtk::Widget &widget, int &minimum_height, int &natural_height) const {
int text_min, text_nat;
m_renderer_text.get_preferred_height(widget, text_min, text_nat);
int xpad, ypad;
get_padding(xpad, ypad);
minimum_height = text_min + ypad * 2;
natural_height = text_nat + ypad * 2;
}
void CellRendererMemberList::get_preferred_height_for_width_vfunc_role(Gtk::Widget &widget, int width, int &minimum_height, int &natural_height) const {
get_preferred_height_vfunc_role(widget, minimum_height, natural_height);
}
void CellRendererMemberList::render_vfunc_role(const Cairo::RefPtr<Cairo::Context> &cr, Gtk::Widget &widget, const Gdk::Rectangle &background_area, const Gdk::Rectangle &cell_area, Gtk::CellRendererState flags) {
Gtk::Requisition text_min, text_nat, min, nat;
m_renderer_text.get_preferred_size(widget, text_min, text_nat);
get_preferred_size(widget, min, nat);
int x = background_area.get_x() + 5;
int y = background_area.get_y() + background_area.get_height() / 2.0 - text_nat.height / 2.0;
int w = text_nat.width;
int h = text_nat.height;
Gdk::Rectangle text_cell_area(x, y, w, h);
m_renderer_text.render(cr, widget, background_area, text_cell_area, flags);
}
void CellRendererMemberList::get_preferred_width_vfunc_member(Gtk::Widget &widget, int &minimum_width, int &natural_width) const {
int text_min, text_nat;
m_renderer_text.get_preferred_width(widget, text_min, text_nat);
int xpad, ypad;
get_padding(xpad, ypad);
minimum_width = text_min + xpad * 2;
natural_width = text_nat + xpad * 2;
}
void CellRendererMemberList::get_preferred_width_for_height_vfunc_member(Gtk::Widget &widget, int height, int &minimum_width, int &natural_width) const {
get_preferred_width_vfunc_role(widget, minimum_width, natural_width);
}
void CellRendererMemberList::get_preferred_height_vfunc_member(Gtk::Widget &widget, int &minimum_height, int &natural_height) const {
int text_min, text_nat;
m_renderer_text.get_preferred_height(widget, text_min, text_nat);
int xpad, ypad;
get_padding(xpad, ypad);
minimum_height = text_min + ypad * 2;
natural_height = text_nat + ypad * 2;
}
void CellRendererMemberList::get_preferred_height_for_width_vfunc_member(Gtk::Widget &widget, int width, int &minimum_height, int &natural_height) const {
get_preferred_height_vfunc_role(widget, minimum_height, natural_height);
}
void CellRendererMemberList::render_vfunc_member(const Cairo::RefPtr<Cairo::Context> &cr, Gtk::Widget &widget, const Gdk::Rectangle &background_area, const Gdk::Rectangle &cell_area, Gtk::CellRendererState flags) {
Gtk::Requisition text_min, text_nat, min, nat;
m_renderer_text.get_preferred_size(widget, text_min, text_nat);
get_preferred_size(widget, min, nat);
int pixbuf_w = 0, pixbuf_h = 0;
if (auto pixbuf = m_property_icon.get_value()) {
pixbuf_w = pixbuf->get_width();
pixbuf_h = pixbuf->get_height();
}
const double icon_w = pixbuf_w;
const double icon_h = pixbuf_h;
const double icon_x = background_area.get_x() + 3.0;
const double icon_y = background_area.get_y() + background_area.get_height() / 2.0 - icon_h / 2.0;
const double x = icon_x + icon_w + 5;
const double y = background_area.get_y() + background_area.get_height() / 2.0 - text_nat.height / 2.0;
const double w = text_nat.width;
const double h = text_nat.height;
if (auto pixbuf = m_property_icon.get_value()) {
Gdk::Cairo::set_source_pixbuf(cr, pixbuf, icon_x, icon_y);
cr->rectangle(icon_x, icon_y, icon_w, icon_h);
cr->fill();
}
Gdk::Rectangle text_cell_area(
static_cast<int>(x),
static_cast<int>(y),
static_cast<int>(w),
static_cast<int>(h));
m_renderer_text.render(cr, widget, background_area, text_cell_area, flags);
}

View File

@@ -0,0 +1,60 @@
#pragma once
#include <gdkmm/pixbuf.h>
#include <glibmm/property.h>
#include <gtkmm/cellrenderertext.h>
enum class MemberListRenderType {
Role,
Member,
};
class CellRendererMemberList : public Gtk::CellRenderer {
public:
CellRendererMemberList();
~CellRendererMemberList() = default;
Glib::PropertyProxy<MemberListRenderType> property_type();
Glib::PropertyProxy<uint64_t> property_id();
Glib::PropertyProxy<Glib::ustring> property_markup();
Glib::PropertyProxy<Glib::RefPtr<Gdk::Pixbuf>> property_icon();
private:
void get_preferred_width_vfunc(Gtk::Widget &widget, int &minimum_width, int &natural_width) const override;
void get_preferred_width_for_height_vfunc(Gtk::Widget &widget, int height, int &minimum_width, int &natural_width) const override;
void get_preferred_height_vfunc(Gtk::Widget &widget, int &minimum_height, int &natural_height) const override;
void get_preferred_height_for_width_vfunc(Gtk::Widget &widget, int width, int &minimum_height, int &natural_height) const override;
void render_vfunc(const Cairo::RefPtr<Cairo::Context> &cr,
Gtk::Widget &widget,
const Gdk::Rectangle &background_area,
const Gdk::Rectangle &cell_area,
Gtk::CellRendererState flags) override;
// role
void get_preferred_width_vfunc_role(Gtk::Widget &widget, int &minimum_width, int &natural_width) const;
void get_preferred_width_for_height_vfunc_role(Gtk::Widget &widget, int height, int &minimum_width, int &natural_width) const;
void get_preferred_height_vfunc_role(Gtk::Widget &widget, int &minimum_height, int &natural_height) const;
void get_preferred_height_for_width_vfunc_role(Gtk::Widget &widget, int width, int &minimum_height, int &natural_height) const;
void render_vfunc_role(const Cairo::RefPtr<Cairo::Context> &cr,
Gtk::Widget &widget,
const Gdk::Rectangle &background_area,
const Gdk::Rectangle &cell_area,
Gtk::CellRendererState flags);
// member
void get_preferred_width_vfunc_member(Gtk::Widget &widget, int &minimum_width, int &natural_width) const;
void get_preferred_width_for_height_vfunc_member(Gtk::Widget &widget, int height, int &minimum_width, int &natural_width) const;
void get_preferred_height_vfunc_member(Gtk::Widget &widget, int &minimum_height, int &natural_height) const;
void get_preferred_height_for_width_vfunc_member(Gtk::Widget &widget, int width, int &minimum_height, int &natural_height) const;
void render_vfunc_member(const Cairo::RefPtr<Cairo::Context> &cr,
Gtk::Widget &widget,
const Gdk::Rectangle &background_area,
const Gdk::Rectangle &cell_area,
Gtk::CellRendererState flags);
Gtk::CellRendererText m_renderer_text;
Glib::Property<MemberListRenderType> m_property_type;
Glib::Property<uint64_t> m_property_id;
Glib::Property<Glib::ustring> m_property_markup;
Glib::Property<Glib::RefPtr<Gdk::Pixbuf>> m_property_icon;
};

View File

@@ -262,6 +262,12 @@ std::set<Snowflake> DiscordClient::GetUsersInGuild(Snowflake id) const {
return {};
}
std::vector<UserData> DiscordClient::GetUserDataInGuildBulk(Snowflake id) {
const auto ids = GetUsersInGuild(id);
std::vector<Snowflake> test;
return m_store.GetUserDataBulk(ids.begin(), ids.end());
}
std::set<Snowflake> DiscordClient::GetChannelsInGuild(Snowflake id) const {
auto it = m_guild_to_channels.find(id);
if (it != m_guild_to_channels.end())

View File

@@ -76,6 +76,7 @@ public:
Snowflake GetMemberHoistedRole(Snowflake guild_id, Snowflake user_id, bool with_color = false) const;
std::optional<RoleData> GetMemberHighestRole(Snowflake guild_id, Snowflake user_id) const;
std::set<Snowflake> GetUsersInGuild(Snowflake id) const;
std::vector<UserData> GetUserDataInGuildBulk(Snowflake id);
std::set<Snowflake> GetChannelsInGuild(Snowflake id) const;
std::vector<Snowflake> GetUsersInThread(Snowflake id) const;
std::vector<ChannelData> GetActiveThreads(Snowflake channel_id) const;

View File

@@ -1033,6 +1033,22 @@ RoleData Store::GetRoleBound(std::unique_ptr<Statement> &s) {
return r;
}
UserData Store::GetUserBound(Statement *s) const {
UserData r;
s->Get(0, r.ID);
s->Get(1, r.Username);
s->Get(2, r.Discriminator);
s->Get(3, r.Avatar);
s->Get(4, r.IsBot);
s->Get(5, r.IsSystem);
s->Get(6, r.IsMFAEnabled);
s->Get(7, r.PremiumType);
s->Get(8, r.PublicFlags);
return r;
}
std::optional<UserData> Store::GetUser(Snowflake id) const {
auto &s = m_stmt_get_user;
s->Bind(1, id);
@@ -1043,17 +1059,7 @@ std::optional<UserData> Store::GetUser(Snowflake id) const {
return {};
}
UserData r;
r.ID = id;
s->Get(1, r.Username);
s->Get(2, r.Discriminator);
s->Get(3, r.Avatar);
s->Get(4, r.IsBot);
s->Get(5, r.IsSystem);
s->Get(6, r.IsMFAEnabled);
s->Get(7, r.PremiumType);
s->Get(8, r.PublicFlags);
auto r = GetUserBound(s.get());
s->Reset();

View File

@@ -39,6 +39,36 @@ public:
std::optional<BanData> GetBan(Snowflake guild_id, Snowflake user_id) const;
std::vector<BanData> GetBans(Snowflake guild_id) const;
template<typename Iter>
std::vector<UserData> GetUserDataBulk(Iter start, Iter end) {
std::string query = "SELECT * FROM users WHERE id IN (";
for (Iter it = start; it != end; it++) {
query += "?,";
}
query.pop_back();
query += ")";
Statement stmt(m_db, query.c_str());
if (!stmt.OK()) {
printf("failed to prepare GetUserDataBulk: %s\n", m_db.ErrStr());
}
int i = 0;
for (Iter it = start; it != end; it++) {
i++;
if (stmt.Bind(i, *it) != SQLITE_OK) {
printf("failed to bind GetUserDataBulk: %s\n", m_db.ErrStr());
}
}
std::vector<UserData> r;
while (stmt.FetchOne()) {
r.push_back(GetUserBound(&stmt));
}
return r;
}
std::vector<Message> GetLastMessages(Snowflake id, size_t num) const;
std::vector<Message> GetMessagesBefore(Snowflake channel_id, Snowflake message_id, size_t limit) const;
std::vector<Message> GetPinnedMessages(Snowflake channel_id) const;
@@ -240,6 +270,7 @@ private:
Message GetMessageBound(std::unique_ptr<Statement> &stmt) const;
static RoleData GetRoleBound(std::unique_ptr<Statement> &stmt);
UserData GetUserBound(Statement *stmt) const;
void SetMessageInteractionPair(Snowflake message_id, const MessageInteractionData &interaction);

View File

@@ -1,6 +1,15 @@
#include "settings.hpp"
#include <filesystem>
#include <fstream>
#include <glibmm/miscutils.h>
#ifdef WITH_KEYCHAIN
#include <keychain/keychain.h>
#endif
const std::string KeychainPackage = "com.github.uowuo.abaddon";
const std::string KeychainService = "abaddon-client-token";
const std::string KeychainUser = "";
SettingsManager::SettingsManager(const std::string &filename)
: m_filename(filename) {
@@ -36,7 +45,6 @@ void SettingsManager::ReadSettings() {
SMSTR("discord", "api_base", APIBaseURL);
SMSTR("discord", "gateway", GatewayURL);
SMSTR("discord", "token", DiscordToken);
SMBOOL("discord", "memory_db", UseMemoryDB);
SMBOOL("discord", "prefetch", Prefetch);
SMBOOL("discord", "autoconnect", Autoconnect);
@@ -61,6 +69,32 @@ void SettingsManager::ReadSettings() {
SMSTR("style", "mentionbadgetextcolor", MentionBadgeTextColor);
SMSTR("style", "unreadcolor", UnreadIndicatorColor);
#ifdef WITH_KEYCHAIN
keychain::Error error {};
// convert to keychain if present in normal settings
SMSTR("discord", "token", DiscordToken);
if (!m_settings.DiscordToken.empty()) {
keychain::Error set_error {};
keychain::setPassword(KeychainPackage, KeychainService, KeychainUser, m_settings.DiscordToken, set_error);
if (set_error) {
printf("keychain error setting token: %s\n", set_error.message.c_str());
} else {
m_file.remove_key("discord", "token");
}
}
m_settings.DiscordToken = keychain::getPassword(KeychainPackage, KeychainService, KeychainUser, error);
if (error && error.type != keychain::ErrorType::NotFound) {
printf("keychain error reading token: %s (%d)\n", error.message.c_str(), error.code);
}
#else
SMSTR("discord", "token", DiscordToken);
#endif
#undef SMBOOL
#undef SMSTR
#undef SMINT
@@ -92,7 +126,6 @@ void SettingsManager::Close() {
SMSTR("discord", "api_base", APIBaseURL);
SMSTR("discord", "gateway", GatewayURL);
SMSTR("discord", "token", DiscordToken);
SMBOOL("discord", "memory_db", UseMemoryDB);
SMBOOL("discord", "prefetch", Prefetch);
SMBOOL("discord", "autoconnect", Autoconnect);
@@ -117,6 +150,17 @@ void SettingsManager::Close() {
SMSTR("style", "mentionbadgetextcolor", MentionBadgeTextColor);
SMSTR("style", "unreadcolor", UnreadIndicatorColor);
#ifdef WITH_KEYCHAIN
keychain::Error error {};
keychain::setPassword(KeychainPackage, KeychainService, KeychainUser, m_settings.DiscordToken, error);
if (error) {
printf("keychain error setting token: %s\n", error.message.c_str());
}
#else
SMSTR("discord", "token", DiscordToken);
#endif
#undef SMSTR
#undef SMBOOL
#undef SMINT

View File

@@ -1,6 +1,8 @@
#include "mainwindow.hpp"
#include "abaddon.hpp"
#include "components/memberlist.hpp" // TMP!!!!
MainWindow::MainWindow()
: m_main_box(Gtk::ORIENTATION_VERTICAL)
, m_content_box(Gtk::ORIENTATION_HORIZONTAL)
@@ -20,7 +22,6 @@ MainWindow::MainWindow()
m_main_box.add(m_content_box);
m_main_box.show();
auto *member_list = m_members.GetRoot();
auto *chat = m_chat.GetRoot();
chat->set_vexpand(true);
@@ -37,8 +38,8 @@ MainWindow::MainWindow()
m_channel_list.set_size_request(-1, -1);
m_channel_list.show();
member_list->set_vexpand(true);
member_list->show();
m_members.set_vexpand(true);
m_members.show();
m_friends.set_vexpand(true);
m_friends.set_hexpand(true);
@@ -63,11 +64,11 @@ MainWindow::MainWindow()
m_channel_list.UsePanedHack(m_chan_content_paned);
m_content_members_paned.pack1(m_content_stack);
m_content_members_paned.pack2(*member_list);
m_content_members_paned.pack2(m_members);
m_content_members_paned.child_property_shrink(m_content_stack) = true;
m_content_members_paned.child_property_resize(m_content_stack) = true;
m_content_members_paned.child_property_shrink(*member_list) = true;
m_content_members_paned.child_property_resize(*member_list) = true;
m_content_members_paned.child_property_shrink(m_members) = true;
m_content_members_paned.child_property_resize(m_members) = true;
int w, h;
get_default_size(w, h); // :s
m_content_members_paned.set_position(w - m_chan_content_paned.get_position() - 150);
@@ -265,6 +266,12 @@ void MainWindow::SetupMenu() {
m_menu_view_threads.set_label("Threads");
m_menu_view_mark_guild_as_read.set_label("Mark Server as Read");
m_menu_view_mark_guild_as_read.add_accelerator("activate", m_accels, GDK_KEY_Escape, Gdk::SHIFT_MASK, Gtk::ACCEL_VISIBLE);
m_menu_view_channels.set_label("Channels");
m_menu_view_channels.add_accelerator("activate", m_accels, GDK_KEY_L, Gdk::CONTROL_MASK, Gtk::ACCEL_VISIBLE);
m_menu_view_channels.set_active(true);
m_menu_view_members.set_label("Members");
m_menu_view_members.add_accelerator("activate", m_accels, GDK_KEY_M, Gdk::CONTROL_MASK, Gtk::ACCEL_VISIBLE);
m_menu_view_members.set_active(true);
#ifdef WITH_LIBHANDY
m_menu_view_go_back.set_label("Go Back");
m_menu_view_go_forward.set_label("Go Forward");
@@ -275,6 +282,8 @@ void MainWindow::SetupMenu() {
m_menu_view_sub.append(m_menu_view_pins);
m_menu_view_sub.append(m_menu_view_threads);
m_menu_view_sub.append(m_menu_view_mark_guild_as_read);
m_menu_view_sub.append(m_menu_view_channels);
m_menu_view_sub.append(m_menu_view_members);
#ifdef WITH_LIBHANDY
m_menu_view_sub.append(m_menu_view_go_back);
m_menu_view_sub.append(m_menu_view_go_forward);
@@ -354,6 +363,14 @@ void MainWindow::SetupMenu() {
}
});
m_menu_view_channels.signal_activate().connect([this]() {
m_channel_list.set_visible(m_menu_view_channels.get_active());
});
m_menu_view_members.signal_activate().connect([this]() {
m_members.set_visible(m_menu_view_members.get_active());
});
#ifdef WITH_LIBHANDY
m_menu_view_go_back.signal_activate().connect([this] {
GoBack();

View File

@@ -1,8 +1,8 @@
#pragma once
#include "components/channels.hpp"
#include "components/chatwindow.hpp"
#include "components/memberlist.hpp"
#include "components/friendslist.hpp"
#include "components/memberlist.hpp"
#include <gtkmm.h>
class MainWindow : public Gtk::Window {
@@ -79,6 +79,8 @@ private:
Gtk::MenuItem m_menu_view_pins;
Gtk::MenuItem m_menu_view_threads;
Gtk::MenuItem m_menu_view_mark_guild_as_read;
Gtk::CheckMenuItem m_menu_view_channels;
Gtk::CheckMenuItem m_menu_view_members;
#ifdef WITH_LIBHANDY
Gtk::MenuItem m_menu_view_go_back;
Gtk::MenuItem m_menu_view_go_forward;

1
subprojects/keychain Submodule

Submodule subprojects/keychain added at 44b517d096