23 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
64245bf745 add option to autoconnect (closes #114) 2022-10-23 18:23:11 -04:00
ouwou
772598996c Add option to hide the menu bar behind alt key (#115) 2022-10-23 02:56:07 +00:00
Addison Snelling
ccb82c1676 readme: list exact depedency package names for Ubuntu (#109)
Co-authored-by: Addison Snelling <fwd+gpg@ext.asnell.io>
2022-10-18 05:48:45 +00: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
18 changed files with 694 additions and 343 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 ()

262
README.md
View File

@@ -25,13 +25,15 @@ Current features:
* Thread support<sup>3</sup>
* Animated avatars, server icons, emojis (can be turned off)
1 - Abaddon tries its best (though is not perfect) to make Discord think it's a legitimate web client. Some of the things done to do this
1 - Abaddon tries its best (though is not perfect) to make Discord think it's a legitimate web client. Some of the
things done to do this
include: using a browser user agent, sending the same IDENTIFY message that the official web client does, using API v9
endpoints in all cases, and not using endpoints the web client does not normally use. There are still a few smaller
inconsistencies, however. For example the web client sends lots of telemetry via the `/science` endpoint (uBlock origin
stops this) as well as in the headers of all requests.<br>
**See [here](#the-spam-filter)** for things you might want to avoid if you are worried about being caught in the spam filter.
**See [here](#the-spam-filter)** for things you might want to avoid if you are worried about being caught in the spam
filter.
2 - Unicode emojis are substituted manually as opposed to rendered by GTK on non-Windows platforms. This can be changed
with the `stock_emojis` setting as shown at the bottom of this README. A CBDT-based font using Twemoji is provided to
@@ -68,8 +70,15 @@ the result of fundamental issues with Discord's thread implementation.
#### Linux:
1. Install dependencies: `libgtkmm-3.0-dev`, `libcurl4-gnutls-dev`,
and [nlohmann-json](https://github.com/nlohmann/json)
1. Install dependencies
* On Ubuntu 20.04 (Focal) and newer:
```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 ..`
@@ -87,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`
@@ -96,7 +105,12 @@ no `abaddon.ini` in the working directory
#### The Spam Filter
Discord likes disabling accounts/forcing them to reset their passwords if they think the user is a spam bot or potentially had their account compromised. While the official client still often gets users caught in the spam filter, third party clients tend to upset the spam filter more often. If you get caught by it, you can usually [appeal](https://support.discord.com/hc/en-us/requests/new?ticket_form_id=360000029731) it and get it restored. Here are some things you might want to do with the official client instead if you are particularly afraid of evoking the spam filter's wrath:
Discord likes disabling accounts/forcing them to reset their passwords if they think the user is a spam bot or
potentially had their account compromised. While the official client still often gets users caught in the spam filter,
third party clients tend to upset the spam filter more often. If you get caught by it, you can
usually [appeal](https://support.discord.com/hc/en-us/requests/new?ticket_form_id=360000029731) it and get it restored.
Here are some things you might want to do with the official client instead if you are particularly afraid of evoking the
spam filter's wrath:
* Joining or leaving servers (usually main cause of getting caught)
* Frequently disconnecting and reconnecting
@@ -125,142 +139,158 @@ Discord likes disabling accounts/forcing them to reset their passwords if they t
#### 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
| 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

@@ -68,14 +68,13 @@ Abaddon &Abaddon::Get() {
return instance;
}
#ifdef WITH_LIBHANDY
#ifdef _WIN32
#ifdef _WIN32
constexpr static guint BUTTON_BACK = 4;
constexpr static guint BUTTON_FORWARD = 5;
#else
#else
constexpr static guint BUTTON_BACK = 8;
constexpr static guint BUTTON_FORWARD = 9;
#endif
#endif
static bool HandleButtonEvents(GdkEvent *event, MainWindow *main_window) {
if (event->type != GDK_BUTTON_PRESS) return false;
@@ -85,6 +84,7 @@ static bool HandleButtonEvents(GdkEvent *event, MainWindow *main_window) {
auto *window = gtk_widget_get_toplevel(widget);
if (static_cast<void *>(window) != static_cast<void *>(main_window->gobj())) return false; // is this the right way???
#ifdef WITH_LIBHANDY
switch (event->button.button) {
case BUTTON_BACK:
main_window->GoBack();
@@ -93,6 +93,7 @@ static bool HandleButtonEvents(GdkEvent *event, MainWindow *main_window) {
main_window->GoForward();
break;
}
#endif
return false;
}
@@ -108,6 +109,15 @@ static bool HandleKeyEvents(GdkEvent *event, MainWindow *main_window) {
const bool ctrl = (event->key.state & GDK_CONTROL_MASK) == GDK_CONTROL_MASK;
const bool shft = (event->key.state & GDK_SHIFT_MASK) == GDK_SHIFT_MASK;
constexpr static guint EXCLUDE_STATES = GDK_CONTROL_MASK | GDK_SHIFT_MASK;
if (!(event->key.state & EXCLUDE_STATES) && event->key.keyval == GDK_KEY_Alt_L) {
if (Abaddon::Get().GetSettings().AltMenu) {
main_window->ToggleMenuVisibility();
}
}
#ifdef WITH_LIBHANDY
if (ctrl) {
switch (event->key.keyval) {
case GDK_KEY_Tab:
@@ -134,6 +144,7 @@ static bool HandleKeyEvents(GdkEvent *event, MainWindow *main_window) {
return true;
}
}
#endif
return false;
}
@@ -143,7 +154,6 @@ static void MainEventHandler(GdkEvent *event, void *main_window) {
if (HandleKeyEvents(event, static_cast<MainWindow *>(main_window))) return;
gtk_main_do_event(event);
}
#endif
int Abaddon::StartGTK() {
m_gtk_app = Gtk::Application::create("com.github.uowuo.abaddon");
@@ -485,6 +495,11 @@ void Abaddon::RunFirstTimeDiscordStartup() {
confirm.SetAcceptOnly(true);
confirm.run();
}
// autoconnect
if (cookie.has_value() && build_number.has_value() && GetSettings().Autoconnect && !GetDiscordToken().empty()) {
ActionConnect();
}
});
}

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,9 +45,9 @@ 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);
SMSTR("gui", "css", MainCSS);
SMBOOL("gui", "animated_guild_hover_only", AnimatedGuildHoverOnly);
SMBOOL("gui", "animations", ShowAnimations);
@@ -48,6 +57,7 @@ void SettingsManager::ReadSettings() {
SMBOOL("gui", "save_state", SaveState);
SMBOOL("gui", "stock_emojis", ShowStockEmojis);
SMBOOL("gui", "unreads", Unreads);
SMBOOL("gui", "alt_menu", AltMenu);
SMBOOL("gui", "hide_to_tray", HideToTray);
SMINT("http", "concurrent", CacheHTTPConcurrency);
SMSTR("http", "user_agent", UserAgent);
@@ -59,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
@@ -90,9 +126,9 @@ 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);
SMSTR("gui", "css", MainCSS);
SMBOOL("gui", "animated_guild_hover_only", AnimatedGuildHoverOnly);
SMBOOL("gui", "animations", ShowAnimations);
@@ -102,6 +138,7 @@ void SettingsManager::Close() {
SMBOOL("gui", "save_state", SaveState);
SMBOOL("gui", "stock_emojis", ShowStockEmojis);
SMBOOL("gui", "unreads", Unreads);
SMBOOL("gui", "alt_menu", AltMenu);
SMBOOL("gui", "hide_to_tray", HideToTray);
SMINT("http", "concurrent", CacheHTTPConcurrency);
SMSTR("http", "user_agent", UserAgent);
@@ -113,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

@@ -12,6 +12,7 @@ public:
std::string DiscordToken;
bool UseMemoryDB { false };
bool Prefetch { false };
bool Autoconnect { false };
// [gui]
std::string MainCSS { "main.css" };
@@ -27,7 +28,7 @@ public:
bool ShowStockEmojis { true };
#endif
bool Unreads { true };
bool AltMenu { false };
bool HideToTray { false };
// [http]

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);
@@ -158,6 +159,10 @@ void MainWindow::UpdateMenus() {
OnViewSubmenuPopup();
}
void MainWindow::ToggleMenuVisibility() {
m_menu_bar.set_visible(!m_menu_bar.get_visible());
}
#ifdef WITH_LIBHANDY
void MainWindow::GoBack() {
m_chat.GoBack();
@@ -261,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");
@@ -271,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);
@@ -279,7 +292,25 @@ void MainWindow::SetupMenu() {
m_menu_bar.append(m_menu_file);
m_menu_bar.append(m_menu_discord);
m_menu_bar.append(m_menu_view);
m_menu_bar.show_all();
if (Abaddon::Get().GetSettings().AltMenu) {
auto set_hide_cb = [this](Gtk::Menu &menu) {
for (auto *child : menu.get_children()) {
auto *item = dynamic_cast<Gtk::MenuItem *>(child);
if (item != nullptr) {
item->signal_activate().connect([this]() {
m_menu_bar.hide();
});
}
}
};
set_hide_cb(m_menu_discord_sub);
set_hide_cb(m_menu_file_sub);
set_hide_cb(m_menu_view_sub);
m_menu_bar.show_all_children();
} else {
m_menu_bar.show_all();
}
m_menu_discord_connect.signal_activate().connect([this] {
m_signal_action_connect.emit();
@@ -332,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 {
@@ -24,6 +24,7 @@ public:
void UpdateChatReactionAdd(Snowflake id, const Glib::ustring &param);
void UpdateChatReactionRemove(Snowflake id, const Glib::ustring &param);
void UpdateMenus();
void ToggleMenuVisibility();
#ifdef WITH_LIBHANDY
void GoBack();
@@ -78,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