Merge branch 'master' into voice

This commit is contained in:
ouwou
2023-02-07 15:03:03 -05:00
107 changed files with 695 additions and 373 deletions

View File

@@ -63,26 +63,26 @@ jobs:
- name: Build (1) - name: Build (1)
uses: haya14busa/action-cond@v1 uses: haya14busa/action-cond@v1
id: buildcmd id: build
with: with:
cond: ${{ matrix.mindeps == true }} cond: ${{ matrix.mindeps == true }}
if_true: | if_true: |
cmake -GNinja -Bbuild -DUSE_LIBHANDY=OFF -DENABLE_VOICE=OFF -DCMAKE_BUILD_TYPE=${{ matrix.buildtype }} cmake -GNinja -Bbuild -DCMAKE_BUILD_TYPE=${{ matrix.buildtype }} -DUSE_LIBHANDY=OFF -DENABLE_VOICE=OFF
cmake --build build cmake --build build
if_false: | if_false: |
cmake -GNinja -Bbuild -DCMAKE_BUILD_TYPE=${{ matrix.buildtype }} cmake -GNinja -Bbuild -DCMAKE_BUILD_TYPE=${{ matrix.buildtype }}
cmake --build build cmake --build build
- name: Build (2) - name: Build (2) }}
run: ${{ steps.buildcmd.outputs.value }} run: ${{ steps.build.outputs.value }}
- name: Setup Artifact - name: Setup Artifact
run: | run: |
mkdir -p build/artifactdir/bin build/artifactdir/ssl/certs build/artifactdir/lib build/artifactdir/share/glib-2.0/schemas mkdir -p build/artifactdir/bin build/artifactdir/etc/ssl/certs build/artifactdir/lib build/artifactdir/share/glib-2.0/schemas
cd build cd build
cp *.exe artifactdir/bin cp *.exe artifactdir/bin
cd .. cd ..
cp /mingw64/ssl/certs/ca-bundle.crt build/artifactdir/ssl/certs cp /mingw64/etc/ssl/certs/ca-bundle.crt build/artifactdir/etc/ssl/certs
cp -r /mingw64/lib/gdk-pixbuf-2.0 build/artifactdir/lib cp -r /mingw64/lib/gdk-pixbuf-2.0 build/artifactdir/lib
cp -r res/css res/res res/fonts build/artifactdir/bin cp -r res/css res/res res/fonts build/artifactdir/bin
cp /mingw64/share/glib-2.0/schemas/gschemas.compiled build/artifactdir/share/glib-2.0/schemas cp /mingw64/share/glib-2.0/schemas/gschemas.compiled build/artifactdir/share/glib-2.0/schemas

3
.gitmodules vendored
View File

@@ -7,3 +7,6 @@
[submodule "subprojects/miniaudio"] [submodule "subprojects/miniaudio"]
path = subprojects/miniaudio path = subprojects/miniaudio
url = https://github.com/mackron/miniaudio url = https://github.com/mackron/miniaudio
[submodule "subprojects/keychain"]
path = subprojects/keychain
url = https://github.com/hrantzsch/keychain

View File

@@ -9,6 +9,7 @@ set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${CMAKE_SOURCE_DIR}/cmake/")
option(USE_LIBHANDY "Enable features that require libhandy (default)" ON) option(USE_LIBHANDY "Enable features that require libhandy (default)" ON)
option(ENABLE_VOICE "Enable voice suppport" ON) option(ENABLE_VOICE "Enable voice suppport" ON)
option(USE_KEYCHAIN "Store the token in the keychain (default)" ON)
find_package(nlohmann_json REQUIRED) find_package(nlohmann_json REQUIRED)
find_package(CURL) find_package(CURL)
@@ -56,6 +57,8 @@ target_include_directories(abaddon PUBLIC ${ZLIB_INCLUDE_DIRS})
target_include_directories(abaddon PUBLIC ${SQLite3_INCLUDE_DIRS}) target_include_directories(abaddon PUBLIC ${SQLite3_INCLUDE_DIRS})
target_include_directories(abaddon PUBLIC ${NLOHMANN_JSON_INCLUDE_DIRS}) target_include_directories(abaddon PUBLIC ${NLOHMANN_JSON_INCLUDE_DIRS})
target_precompile_headers(abaddon PRIVATE <gtkmm.h> src/abaddon.hpp src/util.hpp)
if ((CMAKE_CXX_COMPILER_ID STREQUAL "GNU") OR if ((CMAKE_CXX_COMPILER_ID STREQUAL "GNU") OR
(CMAKE_CXX_COMPILER_ID STREQUAL "Clang" AND (CMAKE_CXX_COMPILER_ID STREQUAL "Clang" AND
((CMAKE_SYSTEM_NAME STREQUAL "Linux") OR (CMAKE_CXX_COMPILER_VERSION LESS 9)))) ((CMAKE_SYSTEM_NAME STREQUAL "Linux") OR (CMAKE_CXX_COMPILER_VERSION LESS 9))))
@@ -100,14 +103,19 @@ target_link_libraries(abaddon ${ZLIB_LIBRARY})
target_link_libraries(abaddon ${NLOHMANN_JSON_LIBRARIES}) target_link_libraries(abaddon ${NLOHMANN_JSON_LIBRARIES})
if (USE_LIBHANDY) if (USE_LIBHANDY)
find_package(libhandy) find_package(libhandy REQUIRED)
if (NOT libhandy_FOUND) target_include_directories(abaddon PUBLIC ${libhandy_INCLUDE_DIRS})
message("libhandy could not be found. features requiring it have been disabled") target_link_libraries(abaddon ${libhandy_LIBRARIES})
set(USE_LIBHANDY OFF) target_compile_definitions(abaddon PRIVATE WITH_LIBHANDY)
else () endif ()
target_include_directories(abaddon PUBLIC ${libhandy_INCLUDE_DIRS})
target_link_libraries(abaddon ${libhandy_LIBRARIES}) if (USE_KEYCHAIN)
target_compile_definitions(abaddon PRIVATE WITH_LIBHANDY) 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 ()
endif () endif ()

249
README.md
View File

@@ -75,6 +75,14 @@ the result of fundamental issues with Discord's thread implementation.
```Shell ```Shell
$ sudo apt install g++ cmake libgtkmm-3.0-dev libcurl4-gnutls-dev libsqlite3-dev libssl-dev nlohmann-json3-dev $ 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
```
* On Fedora Linux:
```Shell
$ sudo dnf install g++ cmake gtkmm3.0-devel libcurl-devel sqlite-devel openssl-devel json-devel libsecret-devel libhandy-devel
```
2. `git clone https://github.com/uowuo/abaddon --recurse-submodules="subprojects" && cd abaddon` 2. `git clone https://github.com/uowuo/abaddon --recurse-submodules="subprojects" && cd abaddon`
3. `mkdir build && cd build` 3. `mkdir build && cd build`
4. `cmake ..` 4. `cmake ..`
@@ -92,7 +100,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), - 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 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` On Linux, `css` and `res` can also be loaded from `~/.local/share/abaddon` or `/usr/share/abaddon`
@@ -135,145 +143,158 @@ spam filter's wrath:
#### CSS selectors #### CSS selectors
.app-window - Applied to all windows. This means the main window and all popups | Selector | Description |
.app-popup - Additional class for `.app-window`s when the window is not the main window |--------------------------------|---------------------------------------------------------------------------------------------------|
| `.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 Used in guild settings popup:
.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 | Selector | Description |
.completer-entry - Container for a single entry in the completer |----------------------------|---------------------------------------------------|
.completer-entry-label - Contains the label for an entry in the completer | `.guild-settings-window` | Container for list of members in the members pane |
.completer-entry-image - Contains the image for an entry in the completer | `.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 Used in profile popup:
.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 | Selector | Description |
.members-row - All rows within the members container |------------------------------|---------------------------------------------------------|
.members-row-label - All labels in the members container | `.mutual-friend-item` | Applied to every item in the mutual friends list |
.members-row-member - Rows containing a member | `.mutual-friend-item-name` | Name in mutual friend item |
.members-row-role - Rows containing a role | `.mutual-friend-item-avatar` | Avatar in mutual friend item |
.members-row-avatar - Contains the avatar for a row in the member list | `.mutual-guild-item` | Applied to every item in the mutual guilds list |
| `.mutual-guild-item-name` | Name in mutual guild item |
.status-indicator - The status indicator | `.mutual-guild-item-icon` | Icon in mutual guild item |
.online - Applied to status indicators when the associated user is online | `.mutual-guild-item-nick` | User nickname in mutual guild item |
.idle - Applied to status indicators when the associated user is away | `.profile-connection` | Applied to every item in the user connections list |
.dnd - Applied to status indicators when the associated user is on do not disturb | `.profile-connection-label` | Label in profile connection item |
.offline - Applied to status indicators when the associated user is offline | `.profile-connection-check` | Checkmark in verified profile connection items |
| `.profile-connections` | Container for profile connections |
.typing-indicator - The typing indicator (also used for replies) | `.profile-notes` | Container for notes in profile window |
| `.profile-notes-label` | Label that says "NOTE" |
Used in reorderable list implementation: | `.profile-notes-text` | Actual note text |
.drag-icon .drag-hover-top .drag-hover-bottom | `.profile-info-pane` | Applied to container for info section of profile popup |
| `.profile-info-created` | Label for creation date of profile |
Used in guild settings popup: | `.user-profile-window` | |
.guild-settings-window | `.profile-main-container` | Inner container for profile |
.guild-members-pane-list - Container for list of members in the members pane | `.profile-avatar` | |
.guild-members-pane-info - Container for member info | `.profile-username` | |
.guild-roles-pane-list - Container for list of roles in the roles pane | `.profile-switcher` | Buttons used to switch viewed section of profile |
| `.profile-stack` | Container for profile info that can be switched between |
Used in profile popup: | `.profile-badges` | Container for badges |
.mutual-friend-item - Applied to every item in the mutual friends list | `.profile-badge` | |
.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
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**: The format is similar to the standard Windows ini format **except**:
* `#` is used to begin comments as opposed to `;` * `#` is used to begin comments as opposed to `;`
* Section and key names are case-sensitive * 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 > **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.
This listing is organized by section.
For example, memory_db would be set by adding `memory_db = true` under the line `[discord]` For example, memory_db would be set by adding `memory_db = true` under the line `[discord]`
#### discord #### discord
* gateway (string) - override url for Discord gateway. must be json format and use zlib stream compression | Setting | Type | Default | Description |
* 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 | `gateway` | string | | override url for Discord gateway. must be json format and use zlib stream compression |
* token (string) - Discord token used to login, this can be set from the menu | `api_base` | string | | override base url for Discord API |
* prefetch (true or false, default false) - if true, new messages will cause the avatar and image attachments to be | `memory_db` | boolean | false | if true, Discord data will be kept in memory as opposed to on disk |
automatically downloaded | `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 #### http
* user_agent (string) - sets the user-agent to use in HTTP requests to the Discord API (not including media/images) | Setting | Type | Default | Description |
* concurrent (int, default 20) - how many images can be concurrently retrieved |--------------|--------|---------|---------------------------------------------------------------------------------------------|
| `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 #### gui
* member_list_discriminator (true or false, default true) - show user discriminators in the member list | Setting | Type | Default | Description |
* 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 | `member_list_discriminator` | boolean | true | show user discriminators in the member list |
* custom_emojis (true or false, default true) - download and use custom Discord emojis | `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 |
* css (string) - path to the main CSS file | `custom_emojis` | boolean | true | download and use custom Discord emojis |
* animations (true or false, default true) - use animated images where available (e.g. server icons, emojis, avatars). | `css` | string | | path to the main CSS file |
false means static images will be used | `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 (true or false, default true) - only animate guild icons when the guild is being hovered | `animated_guild_hover_only` | boolean | true | only animate guild icons when the guild is being hovered over |
over | `owner_crown` | boolean | true | show a crown next to the owner |
* owner_crown (true or false, default true) - show a crown next to the owner | `unreads` | boolean | true | show unread indicators and mention badges |
* unreads (true or false, default true) - show unread indicators and mention badges | `save_state` | boolean | true | save the state of the gui (active channels, tabs, expanded channels) |
* save_state (true or false, default 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 |
* alt_menu (true or false, default false) - keep the menu hidden unless revealed with alt key | `hide_to_tray` | boolean | false | hide abaddon to the system tray on window close |
* hide_to_tray (true or false, default false) - hide abaddon to the system tray on window close
#### style #### style
* linkcolor (string) - color to use for links in messages | Setting | Type | Description |
* expandercolor (string) - color to use for the expander in the channel list |-------------------------|--------|-----------------------------------------------------|
* nsfwchannelcolor (string) - color to use for NSFW channels in the channel list | `linkcolor` | string | color to use for links in messages |
* channelcolor (string) - color to use for SFW channels in the channel list | `expandercolor` | string | color to use for the expander in the channel list |
* mentionbadgecolor (string) - background color for mention badges | `nsfwchannelcolor` | string | color to use for NSFW channels in the channel list |
* mentionbadgetextcolor (string) - color to use for number displayed on mention badges | `channelcolor` | string | color to use for SFW channels in the channel list |
* unreadcolor (string) - color to use for the unread indicator | `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 ### Environment variables
* ABADDON_NO_FC (Windows only) - don't use custom font config | variable | Description |
* ABADDON_CONFIG - change path of configuration file to use. relative to cwd or can be absolute |------------------|------------------------------------------------------------------------------|
| `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

@@ -8,7 +8,7 @@
/bin/libcairo-2.dll /bin/libcairo-2.dll
/bin/libcairo-gobject-2.dll /bin/libcairo-gobject-2.dll
/bin/libcairomm-1.0-1.dll /bin/libcairomm-1.0-1.dll
/bin/libcrypto-1_1-x64.dll /bin/libcrypto-3-x64.dll
/bin/libcurl-4.dll /bin/libcurl-4.dll
/bin/libdatrie-1.dll /bin/libdatrie-1.dll
/bin/libdeflate.dll /bin/libdeflate.dll
@@ -50,7 +50,7 @@
/bin/libspdlog.dll /bin/libspdlog.dll
/bin/libsqlite3-0.dll /bin/libsqlite3-0.dll
/bin/libssh2-1.dll /bin/libssh2-1.dll
/bin/libssl-1_1-x64.dll /bin/libssl-3-x64.dll
/bin/libstdc++-6.dll /bin/libstdc++-6.dll
/bin/libthai-0.dll /bin/libthai-0.dll
/bin/libunistring-2.dll /bin/libunistring-2.dll

View File

@@ -1,4 +1,3 @@
#include <gtkmm.h>
#include <memory> #include <memory>
#include <spdlog/spdlog.h> #include <spdlog/spdlog.h>
#include <spdlog/cfg/env.h> #include <spdlog/cfg/env.h>
@@ -15,7 +14,6 @@
#include "dialogs/friendpicker.hpp" #include "dialogs/friendpicker.hpp"
#include "dialogs/verificationgate.hpp" #include "dialogs/verificationgate.hpp"
#include "dialogs/textinput.hpp" #include "dialogs/textinput.hpp"
#include "abaddon.hpp"
#include "windows/guildsettingswindow.hpp" #include "windows/guildsettingswindow.hpp"
#include "windows/profilewindow.hpp" #include "windows/profilewindow.hpp"
#include "windows/pinnedwindow.hpp" #include "windows/pinnedwindow.hpp"
@@ -817,19 +815,7 @@ void Abaddon::ActionChannelOpened(Snowflake id, bool expand_to) {
const bool can_access = channel->IsDM() || m_discord.HasChannelPermission(m_discord.GetUserData().ID, id, Permission::VIEW_CHANNEL); const bool can_access = channel->IsDM() || m_discord.HasChannelPermission(m_discord.GetUserData().ID, id, Permission::VIEW_CHANNEL);
if (channel->Type == ChannelType::GUILD_TEXT || channel->Type == ChannelType::GUILD_NEWS) m_main_window->set_title(std::string(APP_TITLE) + " - " + channel->GetDisplayName());
m_main_window->set_title(std::string(APP_TITLE) + " - #" + *channel->Name);
else {
std::string display;
const auto recipients = channel->GetDMRecipients();
if (recipients.size() > 1)
display = std::to_string(recipients.size()) + " users";
else if (recipients.size() == 1)
display = recipients[0].Username;
else
display = "Empty group";
m_main_window->set_title(std::string(APP_TITLE) + " - " + display);
}
m_main_window->UpdateChatActiveChannel(id, expand_to); m_main_window->UpdateChatActiveChannel(id, expand_to);
if (m_channels_requested.find(id) == m_channels_requested.end()) { if (m_channels_requested.find(id) == m_channels_requested.end()) {
// dont fire requests we know will fail // dont fire requests we know will fail

View File

@@ -1,5 +1,4 @@
#pragma once #pragma once
#include <gtkmm.h>
#include <memory> #include <memory>
#include <mutex> #include <mutex>
#include <string> #include <string>

View File

@@ -1,5 +1,4 @@
#pragma once #pragma once
#include <gtkmm.h>
#include <unordered_map> #include <unordered_map>
// handles both static and animated // handles both static and animated

View File

@@ -1,8 +1,6 @@
#include "abaddon.hpp"
#include "channels.hpp" #include "channels.hpp"
#include "imgmanager.hpp" #include "imgmanager.hpp"
#include "statusindicator.hpp" #include "statusindicator.hpp"
#include "util.hpp"
#include <algorithm> #include <algorithm>
#include <map> #include <map>
#include <unordered_map> #include <unordered_map>
@@ -105,6 +103,7 @@ ChannelList::ChannelList()
column->add_attribute(renderer->property_id(), m_columns.m_id); column->add_attribute(renderer->property_id(), m_columns.m_id);
column->add_attribute(renderer->property_expanded(), m_columns.m_expanded); column->add_attribute(renderer->property_expanded(), m_columns.m_expanded);
column->add_attribute(renderer->property_nsfw(), m_columns.m_nsfw); column->add_attribute(renderer->property_nsfw(), m_columns.m_nsfw);
column->add_attribute(renderer->property_color(), m_columns.m_color);
m_view.append_column(*column); m_view.append_column(*column);
m_menu_guild_copy_id.signal_activate().connect([this] { m_menu_guild_copy_id.signal_activate().connect([this] {
@@ -309,14 +308,51 @@ void ChannelList::UpdateListing() {
auto &discord = Abaddon::Get().GetDiscordClient(); auto &discord = Abaddon::Get().GetDiscordClient();
const auto guild_ids = discord.GetUserSortedGuilds(); /*
int sortnum = 0; guild_folders looks something like this
for (const auto &guild_id : guild_ids) { "guild_folders": [
const auto guild = discord.GetGuild(guild_id); {
if (!guild.has_value()) continue; "color": null,
"guild_ids": [
"8009060___________"
],
"id": null,
"name": null
},
{
"color": null,
"guild_ids": [
"99615594__________",
"86132141__________",
"35450138__________",
"83714048__________"
],
"id": 2853066769,
"name": null
}
]
auto iter = AddGuild(*guild); so if id != null then its a folder (they can have single entries)
(*iter)[m_columns.m_sort] = sortnum++; */
int sort_value = 0;
const auto folders = discord.GetUserSettings().GuildFolders;
if (folders.empty()) {
// fallback if no organization has occurred (guild_folders will be empty)
const auto guild_ids = discord.GetUserSortedGuilds();
for (const auto &guild_id : guild_ids) {
const auto guild = discord.GetGuild(guild_id);
if (!guild.has_value()) continue;
auto iter = AddGuild(*guild, m_model->children());
if (iter) (*iter)[m_columns.m_sort] = sort_value++;
}
} else {
for (const auto &group : folders) {
auto iter = AddFolder(group);
if (iter) (*iter)[m_columns.m_sort] = sort_value++;
}
} }
m_updating_listing = false; m_updating_listing = false;
@@ -324,8 +360,9 @@ void ChannelList::UpdateListing() {
AddPrivateChannels(); AddPrivateChannels();
} }
// TODO update for folders
void ChannelList::UpdateNewGuild(const GuildData &guild) { void ChannelList::UpdateNewGuild(const GuildData &guild) {
AddGuild(guild); AddGuild(guild, m_model->children());
// update sort order // update sort order
int sortnum = 0; int sortnum = 0;
for (const auto guild_id : Abaddon::Get().GetDiscordClient().GetUserSortedGuilds()) { for (const auto guild_id : Abaddon::Get().GetDiscordClient().GetUserSortedGuilds()) {
@@ -452,6 +489,8 @@ void ChannelList::OnThreadListSync(const ThreadListSyncData &data) {
// get the threads in the guild // get the threads in the guild
std::vector<Snowflake> threads; std::vector<Snowflake> threads;
auto guild_iter = GetIteratorForGuildFromID(data.GuildID); auto guild_iter = GetIteratorForGuildFromID(data.GuildID);
if (!guild_iter) return;
std::queue<Gtk::TreeModel::iterator> queue; std::queue<Gtk::TreeModel::iterator> queue;
queue.push(guild_iter); queue.push(guild_iter);
@@ -567,22 +606,27 @@ void ChannelList::SetActiveChannel(Snowflake id, bool expand_to) {
void ChannelList::UseExpansionState(const ExpansionStateRoot &root) { void ChannelList::UseExpansionState(const ExpansionStateRoot &root) {
auto recurse = [this](auto &self, const ExpansionStateRoot &root) -> void { auto recurse = [this](auto &self, const ExpansionStateRoot &root) -> void {
// and these are only channels
for (const auto &[id, state] : root.Children) { for (const auto &[id, state] : root.Children) {
if (const auto iter = m_tmp_channel_map.find(id); iter != m_tmp_channel_map.end()) { Gtk::TreeModel::iterator row_iter;
if (const auto map_iter = m_tmp_row_map.find(id); map_iter != m_tmp_row_map.end()) {
row_iter = map_iter->second;
} else if (const auto map_iter = m_tmp_guild_row_map.find(id); map_iter != m_tmp_guild_row_map.end()) {
row_iter = map_iter->second;
}
if (row_iter) {
if (state.IsExpanded) if (state.IsExpanded)
m_view.expand_row(m_model->get_path(iter->second), false); m_view.expand_row(m_model->get_path(row_iter), false);
else else
m_view.collapse_row(m_model->get_path(iter->second)); m_view.collapse_row(m_model->get_path(row_iter));
} }
self(self, state.Children); self(self, state.Children);
} }
}; };
// top level is guild
for (const auto &[id, state] : root.Children) { for (const auto &[id, state] : root.Children) {
if (const auto iter = GetIteratorForGuildFromID(id)) { if (const auto iter = GetIteratorForTopLevelFromID(id)) {
if (state.IsExpanded) if (state.IsExpanded)
m_view.expand_row(m_model->get_path(iter), false); m_view.expand_row(m_model->get_path(iter), false);
else else
@@ -592,7 +636,7 @@ void ChannelList::UseExpansionState(const ExpansionStateRoot &root) {
recurse(recurse, state.Children); recurse(recurse, state.Children);
} }
m_tmp_channel_map.clear(); m_tmp_row_map.clear();
} }
ExpansionStateRoot ChannelList::GetExpansionState() const { ExpansionStateRoot ChannelList::GetExpansionState() const {
@@ -617,15 +661,54 @@ ExpansionStateRoot ChannelList::GetExpansionState() const {
return r; return r;
} }
Gtk::TreeModel::iterator ChannelList::AddGuild(const GuildData &guild) { Gtk::TreeModel::iterator ChannelList::AddFolder(const UserSettingsGuildFoldersEntry &folder) {
if (!folder.ID.has_value()) {
// just a guild
if (!folder.GuildIDs.empty()) {
const auto guild = Abaddon::Get().GetDiscordClient().GetGuild(folder.GuildIDs[0]);
if (guild.has_value()) {
return AddGuild(*guild, m_model->children());
}
}
} else {
auto folder_row = *m_model->append();
folder_row[m_columns.m_type] = RenderType::Folder;
folder_row[m_columns.m_id] = *folder.ID;
m_tmp_row_map[*folder.ID] = folder_row;
if (folder.Name.has_value()) {
folder_row[m_columns.m_name] = Glib::Markup::escape_text(*folder.Name);
} else {
folder_row[m_columns.m_name] = "Folder";
}
if (folder.Color.has_value()) {
folder_row[m_columns.m_color] = IntToRGBA(*folder.Color);
}
int sort_value = 0;
for (const auto &guild_id : folder.GuildIDs) {
const auto guild = Abaddon::Get().GetDiscordClient().GetGuild(guild_id);
if (guild.has_value()) {
auto guild_row = AddGuild(*guild, folder_row->children());
(*guild_row)[m_columns.m_sort] = sort_value++;
}
}
return folder_row;
}
return {};
}
Gtk::TreeModel::iterator ChannelList::AddGuild(const GuildData &guild, const Gtk::TreeNodeChildren &root) {
auto &discord = Abaddon::Get().GetDiscordClient(); auto &discord = Abaddon::Get().GetDiscordClient();
auto &img = Abaddon::Get().GetImageManager(); auto &img = Abaddon::Get().GetImageManager();
auto guild_row = *m_model->append(); auto guild_row = *m_model->append(root);
guild_row[m_columns.m_type] = RenderType::Guild; guild_row[m_columns.m_type] = RenderType::Guild;
guild_row[m_columns.m_id] = guild.ID; guild_row[m_columns.m_id] = guild.ID;
guild_row[m_columns.m_name] = "<b>" + Glib::Markup::escape_text(guild.Name) + "</b>"; guild_row[m_columns.m_name] = "<b>" + Glib::Markup::escape_text(guild.Name) + "</b>";
guild_row[m_columns.m_icon] = img.GetPlaceholder(GuildIconSize); guild_row[m_columns.m_icon] = img.GetPlaceholder(GuildIconSize);
m_tmp_guild_row_map[guild.ID] = guild_row;
if (Abaddon::Get().GetSettings().ShowAnimations && guild.HasAnimatedIcon()) { if (Abaddon::Get().GetSettings().ShowAnimations && guild.HasAnimatedIcon()) {
const auto cb = [this, id = guild.ID](const Glib::RefPtr<Gdk::PixbufAnimation> &pb) { const auto cb = [this, id = guild.ID](const Glib::RefPtr<Gdk::PixbufAnimation> &pb) {
@@ -677,7 +760,7 @@ Gtk::TreeModel::iterator ChannelList::AddGuild(const GuildData &guild) {
if (it == threads.end()) return; if (it == threads.end()) return;
for (const auto &thread : it->second) for (const auto &thread : it->second)
m_tmp_channel_map[thread.ID] = CreateThreadRow(row.children(), thread); m_tmp_row_map[thread.ID] = CreateThreadRow(row.children(), thread);
}; };
auto add_voice_participants = [this, &discord](const ChannelData &channel, const Gtk::TreeNodeChildren &root) { auto add_voice_participants = [this, &discord](const ChannelData &channel, const Gtk::TreeNodeChildren &root) {
@@ -710,7 +793,7 @@ Gtk::TreeModel::iterator ChannelList::AddGuild(const GuildData &guild) {
channel_row[m_columns.m_sort] = *channel.Position + OrphanChannelSortOffset; channel_row[m_columns.m_sort] = *channel.Position + OrphanChannelSortOffset;
channel_row[m_columns.m_nsfw] = channel.NSFW(); channel_row[m_columns.m_nsfw] = channel.NSFW();
add_threads(channel, channel_row); add_threads(channel, channel_row);
m_tmp_channel_map[channel.ID] = channel_row; m_tmp_row_map[channel.ID] = channel_row;
} }
for (const auto &[category_id, channels] : categories) { for (const auto &[category_id, channels] : categories) {
@@ -722,7 +805,7 @@ Gtk::TreeModel::iterator ChannelList::AddGuild(const GuildData &guild) {
cat_row[m_columns.m_name] = Glib::Markup::escape_text(*category->Name); cat_row[m_columns.m_name] = Glib::Markup::escape_text(*category->Name);
cat_row[m_columns.m_sort] = *category->Position; cat_row[m_columns.m_sort] = *category->Position;
cat_row[m_columns.m_expanded] = true; cat_row[m_columns.m_expanded] = true;
m_tmp_channel_map[category_id] = cat_row; m_tmp_row_map[category_id] = cat_row;
// m_view.expand_row wont work because it might not have channels // m_view.expand_row wont work because it might not have channels
for (const auto &channel : channels) { for (const auto &channel : channels) {
@@ -740,7 +823,7 @@ Gtk::TreeModel::iterator ChannelList::AddGuild(const GuildData &guild) {
channel_row[m_columns.m_sort] = *channel.Position; channel_row[m_columns.m_sort] = *channel.Position;
channel_row[m_columns.m_nsfw] = channel.NSFW(); channel_row[m_columns.m_nsfw] = channel.NSFW();
add_threads(channel, channel_row); add_threads(channel, channel_row);
m_tmp_channel_map[channel.ID] = channel_row; m_tmp_row_map[channel.ID] = channel_row;
} }
} }
@@ -781,10 +864,33 @@ void ChannelList::UpdateChannelCategory(const ChannelData &channel) {
(*iter)[m_columns.m_name] = Glib::Markup::escape_text(*channel.Name); (*iter)[m_columns.m_name] = Glib::Markup::escape_text(*channel.Name);
} }
// todo this all needs refactoring for shooore
Gtk::TreeModel::iterator ChannelList::GetIteratorForTopLevelFromID(Snowflake id) {
for (const auto &child : m_model->children()) {
if ((child[m_columns.m_type] == RenderType::Guild || child[m_columns.m_type] == RenderType::Folder) && child[m_columns.m_id] == id) {
return child;
} else if (child[m_columns.m_type] == RenderType::Folder) {
for (const auto &folder_child : child->children()) {
if (folder_child[m_columns.m_id] == id) {
return folder_child;
}
}
}
}
return {};
}
Gtk::TreeModel::iterator ChannelList::GetIteratorForGuildFromID(Snowflake id) { Gtk::TreeModel::iterator ChannelList::GetIteratorForGuildFromID(Snowflake id) {
for (const auto &child : m_model->children()) { for (const auto &child : m_model->children()) {
if (child[m_columns.m_id] == id) if (child[m_columns.m_type] == RenderType::Guild && child[m_columns.m_id] == id) {
return child; return child;
} else if (child[m_columns.m_type] == RenderType::Folder) {
for (const auto &folder_child : child->children()) {
if (folder_child[m_columns.m_id] == id) {
return folder_child;
}
}
}
} }
return {}; return {};
} }
@@ -797,7 +903,7 @@ Gtk::TreeModel::iterator ChannelList::GetIteratorForRowFromID(Snowflake id) {
while (!queue.empty()) { while (!queue.empty()) {
auto item = queue.front(); auto item = queue.front();
if ((*item)[m_columns.m_id] == id) return item; if ((*item)[m_columns.m_id] == id && (*item)[m_columns.m_type] != RenderType::Guild) return item;
for (const auto &child : item->children()) for (const auto &child : item->children())
queue.push(child); queue.push(child);
queue.pop(); queue.pop();
@@ -884,14 +990,10 @@ void ChannelList::AddPrivateChannels() {
auto row = *iter; auto row = *iter;
row[m_columns.m_type] = RenderType::DM; row[m_columns.m_type] = RenderType::DM;
row[m_columns.m_id] = dm_id; row[m_columns.m_id] = dm_id;
row[m_columns.m_name] = Glib::Markup::escape_text(dm->GetDisplayName());
row[m_columns.m_sort] = static_cast<int64_t>(-(dm->LastMessageID.has_value() ? *dm->LastMessageID : dm_id)); row[m_columns.m_sort] = static_cast<int64_t>(-(dm->LastMessageID.has_value() ? *dm->LastMessageID : dm_id));
row[m_columns.m_icon] = img.GetPlaceholder(DMIconSize); row[m_columns.m_icon] = img.GetPlaceholder(DMIconSize);
if (dm->Type == ChannelType::DM && top_recipient.has_value())
row[m_columns.m_name] = Glib::Markup::escape_text(top_recipient->Username);
else if (dm->Type == ChannelType::GROUP_DM)
row[m_columns.m_name] = std::to_string(recipients.size()) + " members";
if (dm->HasIcon()) { if (dm->HasIcon()) {
const auto cb = [this, iter](const Glib::RefPtr<Gdk::Pixbuf> &pb) { const auto cb = [this, iter](const Glib::RefPtr<Gdk::Pixbuf> &pb) {
if (iter) if (iter)
@@ -921,14 +1023,10 @@ void ChannelList::UpdateCreateDMChannel(const ChannelData &dm) {
auto row = *iter; auto row = *iter;
row[m_columns.m_type] = RenderType::DM; row[m_columns.m_type] = RenderType::DM;
row[m_columns.m_id] = dm.ID; row[m_columns.m_id] = dm.ID;
row[m_columns.m_name] = Glib::Markup::escape_text(dm.GetDisplayName());
row[m_columns.m_sort] = static_cast<int64_t>(-(dm.LastMessageID.has_value() ? *dm.LastMessageID : dm.ID)); row[m_columns.m_sort] = static_cast<int64_t>(-(dm.LastMessageID.has_value() ? *dm.LastMessageID : dm.ID));
row[m_columns.m_icon] = img.GetPlaceholder(DMIconSize); row[m_columns.m_icon] = img.GetPlaceholder(DMIconSize);
if (dm.Type == ChannelType::DM && top_recipient.has_value())
row[m_columns.m_name] = Glib::Markup::escape_text(top_recipient->Username);
else if (dm.Type == ChannelType::GROUP_DM)
row[m_columns.m_name] = std::to_string(recipients.size()) + " members";
if (top_recipient.has_value()) { if (top_recipient.has_value()) {
const auto cb = [this, iter](const Glib::RefPtr<Gdk::Pixbuf> &pb) { const auto cb = [this, iter](const Glib::RefPtr<Gdk::Pixbuf> &pb) {
if (iter) if (iter)
@@ -1025,6 +1123,7 @@ void ChannelList::MoveRow(const Gtk::TreeModel::iterator &iter, const Gtk::TreeM
M(m_sort); M(m_sort);
M(m_nsfw); M(m_nsfw);
M(m_expanded); M(m_expanded);
M(m_color);
#undef M #undef M
// recursively move children // recursively move children
@@ -1174,4 +1273,5 @@ ChannelList::ModelColumns::ModelColumns() {
add(m_sort); add(m_sort);
add(m_nsfw); add(m_nsfw);
add(m_expanded); add(m_expanded);
add(m_color);
} }

View File

@@ -1,10 +1,14 @@
#pragma once #pragma once
#include <gtkmm.h>
#include <string> #include <string>
#include <queue> #include <queue>
#include <mutex> #include <mutex>
#include <unordered_set> #include <unordered_set>
#include <unordered_map> #include <unordered_map>
#include <gtkmm/paned.h>
#include <gtkmm/scrolledwindow.h>
#include <gtkmm/treemodel.h>
#include <gtkmm/treestore.h>
#include <gtkmm/treeview.h>
#include <sigc++/sigc++.h> #include <sigc++/sigc++.h>
#include "discord/discord.hpp" #include "discord/discord.hpp"
#include "state.hpp" #include "state.hpp"
@@ -64,6 +68,7 @@ protected:
Gtk::TreeModelColumn<Glib::RefPtr<Gdk::PixbufAnimation>> m_icon_anim; Gtk::TreeModelColumn<Glib::RefPtr<Gdk::PixbufAnimation>> m_icon_anim;
Gtk::TreeModelColumn<int64_t> m_sort; Gtk::TreeModelColumn<int64_t> m_sort;
Gtk::TreeModelColumn<bool> m_nsfw; Gtk::TreeModelColumn<bool> m_nsfw;
Gtk::TreeModelColumn<std::optional<Gdk::RGBA>> m_color; // for folders right now
// Gtk::CellRenderer's property_is_expanded only works how i want it to if it has children // Gtk::CellRenderer's property_is_expanded only works how i want it to if it has children
// because otherwise it doesnt count as an "expander" (property_is_expander) // because otherwise it doesnt count as an "expander" (property_is_expander)
// so this solution will have to do which i hate but the alternative is adding invisible children // so this solution will have to do which i hate but the alternative is adding invisible children
@@ -75,13 +80,15 @@ protected:
ModelColumns m_columns; ModelColumns m_columns;
Glib::RefPtr<Gtk::TreeStore> m_model; Glib::RefPtr<Gtk::TreeStore> m_model;
Gtk::TreeModel::iterator AddGuild(const GuildData &guild); Gtk::TreeModel::iterator AddFolder(const UserSettingsGuildFoldersEntry &folder);
Gtk::TreeModel::iterator AddGuild(const GuildData &guild, const Gtk::TreeNodeChildren &root);
Gtk::TreeModel::iterator UpdateCreateChannelCategory(const ChannelData &channel); Gtk::TreeModel::iterator UpdateCreateChannelCategory(const ChannelData &channel);
Gtk::TreeModel::iterator CreateThreadRow(const Gtk::TreeNodeChildren &children, const ChannelData &channel); Gtk::TreeModel::iterator CreateThreadRow(const Gtk::TreeNodeChildren &children, const ChannelData &channel);
void UpdateChannelCategory(const ChannelData &channel); void UpdateChannelCategory(const ChannelData &channel);
// separation necessary because a channel and guild can share the same id // separation necessary because a channel and guild can share the same id
Gtk::TreeModel::iterator GetIteratorForTopLevelFromID(Snowflake id);
Gtk::TreeModel::iterator GetIteratorForGuildFromID(Snowflake id); Gtk::TreeModel::iterator GetIteratorForGuildFromID(Snowflake id);
Gtk::TreeModel::iterator GetIteratorForRowFromID(Snowflake id); Gtk::TreeModel::iterator GetIteratorForRowFromID(Snowflake id);
Gtk::TreeModel::iterator GetIteratorForRowFromIDOfType(Snowflake id, RenderType type); Gtk::TreeModel::iterator GetIteratorForRowFromIDOfType(Snowflake id, RenderType type);
@@ -172,7 +179,8 @@ protected:
// (GetIteratorForChannelFromID is rather slow) // (GetIteratorForChannelFromID is rather slow)
// only temporary since i dont want to worry about maintaining this map // only temporary since i dont want to worry about maintaining this map
std::unordered_map<Snowflake, Gtk::TreeModel::iterator> m_tmp_channel_map; std::unordered_map<Snowflake, Gtk::TreeModel::iterator> m_tmp_row_map;
std::unordered_map<Snowflake, Gtk::TreeModel::iterator> m_tmp_guild_row_map;
public: public:
using type_signal_action_channel_item_select = sigc::signal<void, Snowflake>; using type_signal_action_channel_item_select = sigc::signal<void, Snowflake>;

View File

@@ -1,6 +1,4 @@
#include "abaddon.hpp"
#include "channelscellrenderer.hpp" #include "channelscellrenderer.hpp"
#include <gtkmm.h>
constexpr static int MentionsRightPad = 7; constexpr static int MentionsRightPad = 7;
#ifndef M_PI #ifndef M_PI
@@ -18,7 +16,8 @@ CellRendererChannels::CellRendererChannels()
, m_property_pixbuf(*this, "pixbuf") , m_property_pixbuf(*this, "pixbuf")
, m_property_pixbuf_animation(*this, "pixbuf-animation") , m_property_pixbuf_animation(*this, "pixbuf-animation")
, m_property_expanded(*this, "expanded") , m_property_expanded(*this, "expanded")
, m_property_nsfw(*this, "nsfw") { , m_property_nsfw(*this, "nsfw")
, m_property_color(*this, "color") {
property_mode() = Gtk::CELL_RENDERER_MODE_ACTIVATABLE; property_mode() = Gtk::CELL_RENDERER_MODE_ACTIVATABLE;
property_xpad() = 2; property_xpad() = 2;
property_ypad() = 2; property_ypad() = 2;
@@ -55,8 +54,14 @@ Glib::PropertyProxy<bool> CellRendererChannels::property_nsfw() {
return m_property_nsfw.get_proxy(); return m_property_nsfw.get_proxy();
} }
Glib::PropertyProxy<std::optional<Gdk::RGBA>> CellRendererChannels::property_color() {
return m_property_color.get_proxy();
}
void CellRendererChannels::get_preferred_width_vfunc(Gtk::Widget &widget, int &minimum_width, int &natural_width) const { void CellRendererChannels::get_preferred_width_vfunc(Gtk::Widget &widget, int &minimum_width, int &natural_width) const {
switch (m_property_type.get_value()) { switch (m_property_type.get_value()) {
case RenderType::Folder:
return get_preferred_width_vfunc_folder(widget, minimum_width, natural_width);
case RenderType::Guild: case RenderType::Guild:
return get_preferred_width_vfunc_guild(widget, minimum_width, natural_width); return get_preferred_width_vfunc_guild(widget, minimum_width, natural_width);
case RenderType::Category: case RenderType::Category:
@@ -80,6 +85,8 @@ void CellRendererChannels::get_preferred_width_vfunc(Gtk::Widget &widget, int &m
void CellRendererChannels::get_preferred_width_for_height_vfunc(Gtk::Widget &widget, int height, int &minimum_width, int &natural_width) const { void CellRendererChannels::get_preferred_width_for_height_vfunc(Gtk::Widget &widget, int height, int &minimum_width, int &natural_width) const {
switch (m_property_type.get_value()) { switch (m_property_type.get_value()) {
case RenderType::Folder:
return get_preferred_width_for_height_vfunc_folder(widget, height, minimum_width, natural_width);
case RenderType::Guild: case RenderType::Guild:
return get_preferred_width_for_height_vfunc_guild(widget, height, minimum_width, natural_width); return get_preferred_width_for_height_vfunc_guild(widget, height, minimum_width, natural_width);
case RenderType::Category: case RenderType::Category:
@@ -103,6 +110,8 @@ void CellRendererChannels::get_preferred_width_for_height_vfunc(Gtk::Widget &wid
void CellRendererChannels::get_preferred_height_vfunc(Gtk::Widget &widget, int &minimum_height, int &natural_height) const { void CellRendererChannels::get_preferred_height_vfunc(Gtk::Widget &widget, int &minimum_height, int &natural_height) const {
switch (m_property_type.get_value()) { switch (m_property_type.get_value()) {
case RenderType::Folder:
return get_preferred_height_vfunc_folder(widget, minimum_height, natural_height);
case RenderType::Guild: case RenderType::Guild:
return get_preferred_height_vfunc_guild(widget, minimum_height, natural_height); return get_preferred_height_vfunc_guild(widget, minimum_height, natural_height);
case RenderType::Category: case RenderType::Category:
@@ -126,6 +135,8 @@ void CellRendererChannels::get_preferred_height_vfunc(Gtk::Widget &widget, int &
void CellRendererChannels::get_preferred_height_for_width_vfunc(Gtk::Widget &widget, int width, int &minimum_height, int &natural_height) const { void CellRendererChannels::get_preferred_height_for_width_vfunc(Gtk::Widget &widget, int width, int &minimum_height, int &natural_height) const {
switch (m_property_type.get_value()) { switch (m_property_type.get_value()) {
case RenderType::Folder:
return get_preferred_height_for_width_vfunc_folder(widget, width, minimum_height, natural_height);
case RenderType::Guild: case RenderType::Guild:
return get_preferred_height_for_width_vfunc_guild(widget, width, minimum_height, natural_height); return get_preferred_height_for_width_vfunc_guild(widget, width, minimum_height, natural_height);
case RenderType::Category: case RenderType::Category:
@@ -149,6 +160,8 @@ void CellRendererChannels::get_preferred_height_for_width_vfunc(Gtk::Widget &wid
void CellRendererChannels::render_vfunc(const Cairo::RefPtr<Cairo::Context> &cr, Gtk::Widget &widget, const Gdk::Rectangle &background_area, const Gdk::Rectangle &cell_area, Gtk::CellRendererState flags) { void CellRendererChannels::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()) { switch (m_property_type.get_value()) {
case RenderType::Folder:
return render_vfunc_folder(cr, widget, background_area, cell_area, flags);
case RenderType::Guild: case RenderType::Guild:
return render_vfunc_guild(cr, widget, background_area, cell_area, flags); return render_vfunc_guild(cr, widget, background_area, cell_area, flags);
case RenderType::Category: case RenderType::Category:
@@ -170,6 +183,69 @@ void CellRendererChannels::render_vfunc(const Cairo::RefPtr<Cairo::Context> &cr,
} }
} }
// folder functions
void CellRendererChannels::get_preferred_width_vfunc_folder(Gtk::Widget &widget, int &minimum_width, int &natural_width) const {
m_renderer_text.get_preferred_width(widget, minimum_width, natural_width);
}
void CellRendererChannels::get_preferred_width_for_height_vfunc_folder(Gtk::Widget &widget, int height, int &minimum_width, int &natural_width) const {
m_renderer_text.get_preferred_width_for_height(widget, height, minimum_width, natural_width);
}
void CellRendererChannels::get_preferred_height_vfunc_folder(Gtk::Widget &widget, int &minimum_height, int &natural_height) const {
m_renderer_text.get_preferred_height(widget, minimum_height, natural_height);
}
void CellRendererChannels::get_preferred_height_for_width_vfunc_folder(Gtk::Widget &widget, int width, int &minimum_height, int &natural_height) const {
m_renderer_text.get_preferred_height_for_width(widget, width, minimum_height, natural_height);
}
void CellRendererChannels::render_vfunc_folder(const Cairo::RefPtr<Cairo::Context> &cr, Gtk::Widget &widget, const Gdk::Rectangle &background_area, const Gdk::Rectangle &cell_area, Gtk::CellRendererState flags) {
constexpr static int len = 5;
int x1, y1, x2, y2, x3, y3;
if (property_expanded()) {
x1 = background_area.get_x() + 7;
y1 = background_area.get_y() + background_area.get_height() / 2 - len;
x2 = background_area.get_x() + 7 + len;
y2 = background_area.get_y() + background_area.get_height() / 2 + len;
x3 = background_area.get_x() + 7 + len * 2;
y3 = background_area.get_y() + background_area.get_height() / 2 - len;
} else {
x1 = background_area.get_x() + 7;
y1 = background_area.get_y() + background_area.get_height() / 2 - len;
x2 = background_area.get_x() + 7 + len * 2;
y2 = background_area.get_y() + background_area.get_height() / 2;
x3 = background_area.get_x() + 7;
y3 = background_area.get_y() + background_area.get_height() / 2 + len;
}
cr->move_to(x1, y1);
cr->line_to(x2, y2);
cr->line_to(x3, y3);
const auto expander_color = Gdk::RGBA(Abaddon::Get().GetSettings().ChannelsExpanderColor);
cr->set_source_rgb(expander_color.get_red(), expander_color.get_green(), expander_color.get_blue());
cr->stroke();
Gtk::Requisition text_minimum, text_natural;
m_renderer_text.get_preferred_size(widget, text_minimum, text_natural);
const int text_x = background_area.get_x() + 22;
const int text_y = background_area.get_y() + background_area.get_height() / 2 - text_natural.height / 2;
const int text_w = text_natural.width;
const int text_h = text_natural.height;
Gdk::Rectangle text_cell_area(text_x, text_y, text_w, text_h);
static const auto color = Gdk::RGBA(Abaddon::Get().GetSettings().ChannelColor);
if (m_property_color.get_value().has_value()) {
m_renderer_text.property_foreground_rgba() = *m_property_color.get_value();
} else {
m_renderer_text.property_foreground_rgba() = color;
}
m_renderer_text.render(cr, widget, background_area, text_cell_area, flags);
m_renderer_text.property_foreground_set() = false;
}
// guild functions // guild functions
void CellRendererChannels::get_preferred_width_vfunc_guild(Gtk::Widget &widget, int &minimum_width, int &natural_width) const { void CellRendererChannels::get_preferred_width_vfunc_guild(Gtk::Widget &widget, int &minimum_width, int &natural_width) const {

View File

@@ -1,11 +1,11 @@
#pragma once #pragma once
#include <gtkmm/cellrenderertext.h>
#include <gdkmm/pixbufanimation.h> #include <gdkmm/pixbufanimation.h>
#include <glibmm/property.h> #include <glibmm/property.h>
#include <map> #include <map>
#include "discord/snowflake.hpp" #include "discord/snowflake.hpp"
enum class RenderType : uint8_t { enum class RenderType : uint8_t {
Folder,
Guild, Guild,
Category, Category,
TextChannel, TextChannel,
@@ -33,6 +33,7 @@ public:
Glib::PropertyProxy<Glib::RefPtr<Gdk::PixbufAnimation>> property_icon_animation(); Glib::PropertyProxy<Glib::RefPtr<Gdk::PixbufAnimation>> property_icon_animation();
Glib::PropertyProxy<bool> property_expanded(); Glib::PropertyProxy<bool> property_expanded();
Glib::PropertyProxy<bool> property_nsfw(); Glib::PropertyProxy<bool> property_nsfw();
Glib::PropertyProxy<std::optional<Gdk::RGBA>> property_color();
protected: protected:
void get_preferred_width_vfunc(Gtk::Widget &widget, int &minimum_width, int &natural_width) const override; void get_preferred_width_vfunc(Gtk::Widget &widget, int &minimum_width, int &natural_width) const override;
@@ -45,6 +46,17 @@ protected:
const Gdk::Rectangle &cell_area, const Gdk::Rectangle &cell_area,
Gtk::CellRendererState flags) override; Gtk::CellRendererState flags) override;
// guild functions
void get_preferred_width_vfunc_folder(Gtk::Widget &widget, int &minimum_width, int &natural_width) const;
void get_preferred_width_for_height_vfunc_folder(Gtk::Widget &widget, int height, int &minimum_width, int &natural_width) const;
void get_preferred_height_vfunc_folder(Gtk::Widget &widget, int &minimum_height, int &natural_height) const;
void get_preferred_height_for_width_vfunc_folder(Gtk::Widget &widget, int width, int &minimum_height, int &natural_height) const;
void render_vfunc_folder(const Cairo::RefPtr<Cairo::Context> &cr,
Gtk::Widget &widget,
const Gdk::Rectangle &background_area,
const Gdk::Rectangle &cell_area,
Gtk::CellRendererState flags);
// guild functions // guild functions
void get_preferred_width_vfunc_guild(Gtk::Widget &widget, int &minimum_width, int &natural_width) const; void get_preferred_width_vfunc_guild(Gtk::Widget &widget, int &minimum_width, int &natural_width) const;
void get_preferred_width_for_height_vfunc_guild(Gtk::Widget &widget, int height, int &minimum_width, int &natural_width) const; void get_preferred_width_for_height_vfunc_guild(Gtk::Widget &widget, int height, int &minimum_width, int &natural_width) const;
@@ -148,6 +160,7 @@ private:
Glib::Property<Glib::RefPtr<Gdk::PixbufAnimation>> m_property_pixbuf_animation; // guild Glib::Property<Glib::RefPtr<Gdk::PixbufAnimation>> m_property_pixbuf_animation; // guild
Glib::Property<bool> m_property_expanded; // category Glib::Property<bool> m_property_expanded; // category
Glib::Property<bool> m_property_nsfw; // channel Glib::Property<bool> m_property_nsfw; // channel
Glib::Property<std::optional<Gdk::RGBA>> m_property_color; // folder
// same pitfalls as in https://github.com/uowuo/abaddon/blob/60404783bd4ce9be26233fe66fc3a74475d9eaa3/components/cellrendererpixbufanimation.hpp#L32-L39 // same pitfalls as in https://github.com/uowuo/abaddon/blob/60404783bd4ce9be26233fe66fc3a74475d9eaa3/components/cellrendererpixbufanimation.hpp#L32-L39
// this will manifest though since guild icons can change // this will manifest though since guild icons can change

View File

@@ -1,7 +1,6 @@
#ifdef WITH_LIBHANDY #ifdef WITH_LIBHANDY
#include "channeltabswitcherhandy.hpp" #include "channeltabswitcherhandy.hpp"
#include "abaddon.hpp"
void selected_page_notify_cb(HdyTabView *view, GParamSpec *pspec, ChannelTabSwitcherHandy *switcher) { void selected_page_notify_cb(HdyTabView *view, GParamSpec *pspec, ChannelTabSwitcherHandy *switcher) {
auto *page = hdy_tab_view_get_selected_page(view); auto *page = hdy_tab_view_get_selected_page(view);

View File

@@ -1,8 +1,7 @@
#pragma once #pragma once
// perhaps this should be conditionally included within cmakelists? // perhaps this should be conditionally included within cmakelists?
#ifdef WITH_LIBHANDY #ifdef WITH_LIBHANDY
#include <gtkmm/box.h> #include <unordered_map>
#include <unordered_map>
#include <handy.h> #include <handy.h>
#include "discord/snowflake.hpp" #include "discord/snowflake.hpp"
#include "state.hpp" #include "state.hpp"

View File

@@ -1,5 +1,4 @@
#include "chatinput.hpp" #include "chatinput.hpp"
#include "abaddon.hpp"
#include "constants.hpp" #include "constants.hpp"
#include <filesystem> #include <filesystem>
@@ -12,7 +11,10 @@ ChatInputText::ChatInputText() {
// hack // hack
auto cb = [this](GdkEventKey *e) -> bool { auto cb = [this](GdkEventKey *e) -> bool {
return event(reinterpret_cast<GdkEvent *>(e)); // we cant use Widget::event here specifically or else for some reason
// it prevents the default binding set from activating sometimes
// specifically event() will return true (why) preventing default handler from running
return m_signal_key_press_proxy.emit(e);
}; };
m_textview.signal_key_press_event().connect(cb, false); m_textview.signal_key_press_event().connect(cb, false);
m_textview.set_hexpand(false); m_textview.set_hexpand(false);
@@ -91,12 +93,16 @@ ChatInputText::type_signal_image_paste ChatInputText::signal_image_paste() {
return m_signal_image_paste; return m_signal_image_paste;
} }
ChatInputText::type_signal_key_press_proxy ChatInputText::signal_key_press_proxy() {
return m_signal_key_press_proxy;
}
ChatInputTextContainer::ChatInputTextContainer() { ChatInputTextContainer::ChatInputTextContainer() {
// triple hack !!! // triple hack !!!
auto cb = [this](GdkEventKey *e) -> bool { auto cb = [this](GdkEventKey *e) -> bool {
return event(reinterpret_cast<GdkEvent *>(e)); return event(reinterpret_cast<GdkEvent *>(e));
}; };
m_input.signal_key_press_event().connect(cb, false); m_input.signal_key_press_proxy().connect(cb);
m_upload_img.property_icon_name() = "document-send-symbolic"; m_upload_img.property_icon_name() = "document-send-symbolic";
m_upload_img.property_icon_size() = Gtk::ICON_SIZE_LARGE_TOOLBAR; m_upload_img.property_icon_size() = Gtk::ICON_SIZE_LARGE_TOOLBAR;

View File

@@ -1,5 +1,4 @@
#pragma once #pragma once
#include <gtkmm.h>
#include "discord/chatsubmitparams.hpp" #include "discord/chatsubmitparams.hpp"
#include "discord/permissions.hpp" #include "discord/permissions.hpp"
@@ -84,15 +83,18 @@ public:
using type_signal_submit = sigc::signal<bool, Glib::ustring>; using type_signal_submit = sigc::signal<bool, Glib::ustring>;
using type_signal_escape = sigc::signal<void>; using type_signal_escape = sigc::signal<void>;
using type_signal_image_paste = sigc::signal<void, Glib::RefPtr<Gdk::Pixbuf>>; using type_signal_image_paste = sigc::signal<void, Glib::RefPtr<Gdk::Pixbuf>>;
using type_signal_key_press_proxy = sigc::signal<bool, GdkEventKey *>;
type_signal_submit signal_submit(); type_signal_submit signal_submit();
type_signal_escape signal_escape(); type_signal_escape signal_escape();
type_signal_image_paste signal_image_paste(); type_signal_image_paste signal_image_paste();
type_signal_key_press_proxy signal_key_press_proxy();
private: private:
type_signal_submit m_signal_submit; type_signal_submit m_signal_submit;
type_signal_escape m_signal_escape; type_signal_escape m_signal_escape;
type_signal_image_paste m_signal_image_paste; type_signal_image_paste m_signal_image_paste;
type_signal_key_press_proxy m_signal_key_press_proxy;
}; };
// file upload, text // file upload, text

View File

@@ -1,7 +1,5 @@
#include <filesystem> #include <filesystem>
#include "chatinputindicator.hpp" #include "chatinputindicator.hpp"
#include "abaddon.hpp"
#include "util.hpp"
constexpr static const int MaxUsersInIndicator = 4; constexpr static const int MaxUsersInIndicator = 4;

View File

@@ -1,5 +1,4 @@
#pragma once #pragma once
#include <gtkmm.h>
#include <unordered_map> #include <unordered_map>
#include "discord/message.hpp" #include "discord/message.hpp"
#include "discord/user.hpp" #include "discord/user.hpp"

View File

@@ -1,12 +1,10 @@
#include "chatlist.hpp" #include "chatlist.hpp"
#include "abaddon.hpp"
#include "chatmessage.hpp" #include "chatmessage.hpp"
#include "constants.hpp" #include "constants.hpp"
ChatList::ChatList() { ChatList::ChatList() {
m_list.get_style_context()->add_class("messages"); m_list.get_style_context()->add_class("messages");
set_can_focus(false);
set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_ALWAYS); set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_ALWAYS);
get_vadjustment()->signal_value_changed().connect(sigc::mem_fun(*this, &ChatList::OnVAdjustmentValueChanged)); get_vadjustment()->signal_value_changed().connect(sigc::mem_fun(*this, &ChatList::OnVAdjustmentValueChanged));

View File

@@ -1,5 +1,4 @@
#pragma once #pragma once
#include <gtkmm.h>
#include <map> #include <map>
#include <vector> #include <vector>
#include "discord/message.hpp" #include "discord/message.hpp"

View File

@@ -1,7 +1,5 @@
#include "abaddon.hpp"
#include "chatmessage.hpp" #include "chatmessage.hpp"
#include "lazyimage.hpp" #include "lazyimage.hpp"
#include "util.hpp"
#include <unordered_map> #include <unordered_map>
constexpr static int EmojiSize = 24; // settings eventually constexpr static int EmojiSize = 24; // settings eventually
@@ -166,7 +164,8 @@ Gtk::TextView *ChatMessageItemContainer::CreateTextComponent(const Message &data
if (data.IsPending) if (data.IsPending)
tv->get_style_context()->add_class("pending"); tv->get_style_context()->add_class("pending");
tv->get_style_context()->add_class("message-text"); tv->get_style_context()->add_class("message-text");
tv->set_can_focus(false); tv->set_can_focus(true);
tv->set_cursor_visible(false);
tv->set_editable(false); tv->set_editable(false);
tv->set_wrap_mode(Gtk::WRAP_WORD_CHAR); tv->set_wrap_mode(Gtk::WRAP_WORD_CHAR);
tv->set_halign(Gtk::ALIGN_FILL); tv->set_halign(Gtk::ALIGN_FILL);
@@ -335,6 +334,7 @@ Gtk::Widget *ChatMessageItemContainer::CreateEmbedComponent(const EmbedData &emb
author_lbl->set_hexpand(false); author_lbl->set_hexpand(false);
author_lbl->set_text(*embed.Author->Name); author_lbl->set_text(*embed.Author->Name);
author_lbl->get_style_context()->add_class("embed-author"); author_lbl->get_style_context()->add_class("embed-author");
author_lbl->set_selectable(true);
author_box->add(*author_lbl); author_box->add(*author_lbl);
} }
} }
@@ -351,6 +351,7 @@ Gtk::Widget *ChatMessageItemContainer::CreateEmbedComponent(const EmbedData &emb
title_label->set_line_wrap(true); title_label->set_line_wrap(true);
title_label->set_line_wrap_mode(Pango::WRAP_WORD_CHAR); title_label->set_line_wrap_mode(Pango::WRAP_WORD_CHAR);
title_label->set_max_width_chars(50); title_label->set_max_width_chars(50);
title_label->set_selectable(true);
title_ev->add(*title_label); title_ev->add(*title_label);
content->pack_start(*title_ev); content->pack_start(*title_ev);
@@ -380,6 +381,7 @@ Gtk::Widget *ChatMessageItemContainer::CreateEmbedComponent(const EmbedData &emb
desc_label->set_halign(Gtk::ALIGN_START); desc_label->set_halign(Gtk::ALIGN_START);
desc_label->set_hexpand(false); desc_label->set_hexpand(false);
desc_label->get_style_context()->add_class("embed-description"); desc_label->get_style_context()->add_class("embed-description");
desc_label->set_selectable(true);
content->pack_start(*desc_label); content->pack_start(*desc_label);
} }
} }
@@ -422,6 +424,8 @@ Gtk::Widget *ChatMessageItemContainer::CreateEmbedComponent(const EmbedData &emb
field_box->pack_start(*field_val); field_box->pack_start(*field_val);
field_lbl->get_style_context()->add_class("embed-field-title"); field_lbl->get_style_context()->add_class("embed-field-title");
field_val->get_style_context()->add_class("embed-field-value"); field_val->get_style_context()->add_class("embed-field-value");
field_lbl->set_selectable(true);
field_val->set_selectable(true);
flow->insert(*field_box, -1); flow->insert(*field_box, -1);
} }
} }
@@ -445,6 +449,7 @@ Gtk::Widget *ChatMessageItemContainer::CreateEmbedComponent(const EmbedData &emb
footer_lbl->set_hexpand(false); footer_lbl->set_hexpand(false);
footer_lbl->set_text(embed.Footer->Text); footer_lbl->set_text(embed.Footer->Text);
footer_lbl->get_style_context()->add_class("embed-footer"); footer_lbl->get_style_context()->add_class("embed-footer");
footer_lbl->set_selectable(true);
content->pack_start(*footer_lbl); content->pack_start(*footer_lbl);
} }
@@ -1170,8 +1175,6 @@ ChatMessageHeader::ChatMessageHeader(const Message &data)
m_meta_box.set_hexpand(true); m_meta_box.set_hexpand(true);
m_meta_box.set_can_focus(false); m_meta_box.set_can_focus(false);
m_content_box.set_can_focus(false);
const auto on_enter_cb = [this](const GdkEventCrossing *event) -> bool { const auto on_enter_cb = [this](const GdkEventCrossing *event) -> bool {
if (m_anim_avatar) if (m_anim_avatar)
m_avatar.property_pixbuf_animation() = m_anim_avatar; m_avatar.property_pixbuf_animation() = m_anim_avatar;

View File

@@ -1,5 +1,4 @@
#pragma once #pragma once
#include <gtkmm.h>
#include "discord/discord.hpp" #include "discord/discord.hpp"
class ChatMessageItemContainer : public Gtk::EventBox { class ChatMessageItemContainer : public Gtk::EventBox {

View File

@@ -1,5 +1,4 @@
#include "chatwindow.hpp" #include "chatwindow.hpp"
#include "abaddon.hpp"
#include "chatinputindicator.hpp" #include "chatinputindicator.hpp"
#include "ratelimitindicator.hpp" #include "ratelimitindicator.hpp"
#include "chatinput.hpp" #include "chatinput.hpp"

View File

@@ -1,5 +1,4 @@
#pragma once #pragma once
#include <gtkmm.h>
#include <string> #include <string>
#include <set> #include <set>
#include "discord/discord.hpp" #include "discord/discord.hpp"

View File

@@ -1,8 +1,6 @@
#include <unordered_set> #include <unordered_set>
#include <utility> #include <utility>
#include "completer.hpp" #include "completer.hpp"
#include "abaddon.hpp"
#include "util.hpp"
constexpr const int CompleterHeight = 150; constexpr const int CompleterHeight = 150;
constexpr const int MaxCompleterEntries = 30; constexpr const int MaxCompleterEntries = 30;

View File

@@ -1,5 +1,4 @@
#pragma once #pragma once
#include <gtkmm.h>
#include <functional> #include <functional>
#include "lazyimage.hpp" #include "lazyimage.hpp"
#include "discord/snowflake.hpp" #include "discord/snowflake.hpp"

View File

@@ -1,5 +1,4 @@
#pragma once #pragma once
#include <gtkmm.h>
class DragListBox : public Gtk::ListBox { class DragListBox : public Gtk::ListBox {
public: public:

View File

@@ -1,5 +1,4 @@
#include "friendslist.hpp" #include "friendslist.hpp"
#include "abaddon.hpp"
#include "lazyimage.hpp" #include "lazyimage.hpp"
using namespace std::string_literals; using namespace std::string_literals;

View File

@@ -1,5 +1,4 @@
#pragma once #pragma once
#include <gtkmm.h>
#include "discord/objects.hpp" #include "discord/objects.hpp"
class FriendsListAddComponent : public Gtk::Box { class FriendsListAddComponent : public Gtk::Box {

View File

@@ -1,7 +1,6 @@
#include "lazyimage.hpp" #include "lazyimage.hpp"
#include <utility> #include <utility>
#include "abaddon.hpp"
LazyImage::LazyImage(int w, int h, bool use_placeholder) LazyImage::LazyImage(int w, int h, bool use_placeholder)
: m_width(w) : m_width(w)

View File

@@ -1,5 +1,4 @@
#pragma once #pragma once
#include <gtkmm.h>
// loads an image only when the widget is drawn for the first time // loads an image only when the widget is drawn for the first time
class LazyImage : public Gtk::Image { class LazyImage : public Gtk::Image {

View File

@@ -1,6 +1,4 @@
#include "memberlist.hpp" #include "memberlist.hpp"
#include "abaddon.hpp"
#include "util.hpp"
#include "lazyimage.hpp" #include "lazyimage.hpp"
#include "statusindicator.hpp" #include "statusindicator.hpp"

View File

@@ -1,5 +1,4 @@
#pragma once #pragma once
#include <gtkmm.h>
#include <mutex> #include <mutex>
#include <unordered_map> #include <unordered_map>
#include <optional> #include <optional>

View File

@@ -1,5 +1,4 @@
#include "progressbar.hpp" #include "progressbar.hpp"
#include "abaddon.hpp"
MessageUploadProgressBar::MessageUploadProgressBar() { MessageUploadProgressBar::MessageUploadProgressBar() {
get_style_context()->add_class("message-progress"); get_style_context()->add_class("message-progress");

View File

@@ -1,5 +1,4 @@
#pragma once #pragma once
#include <gtkmm/progressbar.h>
#include <string> #include <string>
class MessageUploadProgressBar : public Gtk::ProgressBar { class MessageUploadProgressBar : public Gtk::ProgressBar {

View File

@@ -1,5 +1,4 @@
#include "ratelimitindicator.hpp" #include "ratelimitindicator.hpp"
#include "abaddon.hpp"
#include <filesystem> #include <filesystem>
RateLimitIndicator::RateLimitIndicator() RateLimitIndicator::RateLimitIndicator()

View File

@@ -1,5 +1,4 @@
#pragma once #pragma once
#include <gtkmm.h>
#include <unordered_map> #include <unordered_map>
#include <chrono> #include <chrono>
#include "discord/message.hpp" #include "discord/message.hpp"

View File

@@ -1,5 +1,4 @@
#include "statusindicator.hpp" #include "statusindicator.hpp"
#include "abaddon.hpp"
static const constexpr int Diameter = 8; static const constexpr int Diameter = 8;

View File

@@ -1,5 +1,4 @@
#pragma once #pragma once
#include <gtkmm.h>
#include "discord/snowflake.hpp" #include "discord/snowflake.hpp"
#include "discord/activity.hpp" #include "discord/activity.hpp"

View File

@@ -1,5 +1,4 @@
#pragma once #pragma once
#include <gtkmm.h>
class ConfirmDialog : public Gtk::Dialog { class ConfirmDialog : public Gtk::Dialog {
public: public:

View File

@@ -1,5 +1,4 @@
#pragma once #pragma once
#include <gtkmm.h>
#include <string> #include <string>
class EditMessageDialog : public Gtk::Dialog { class EditMessageDialog : public Gtk::Dialog {

View File

@@ -1,5 +1,4 @@
#include "friendpicker.hpp" #include "friendpicker.hpp"
#include "abaddon.hpp"
FriendPickerDialog::FriendPickerDialog(Gtk::Window &parent) FriendPickerDialog::FriendPickerDialog(Gtk::Window &parent)
: Gtk::Dialog("Pick a friend", parent, true) : Gtk::Dialog("Pick a friend", parent, true)

View File

@@ -1,5 +1,4 @@
#pragma once #pragma once
#include <gtkmm.h>
#include "discord/snowflake.hpp" #include "discord/snowflake.hpp"
class FriendPickerDialog : public Gtk::Dialog { class FriendPickerDialog : public Gtk::Dialog {

View File

@@ -1,5 +1,4 @@
#pragma once #pragma once
#include <gtkmm.h>
#include "discord/objects.hpp" #include "discord/objects.hpp"
class SetStatusDialog : public Gtk::Dialog { class SetStatusDialog : public Gtk::Dialog {

View File

@@ -1,6 +1,4 @@
#pragma once #pragma once
#include <gtkmm/dialog.h>
#include <gtkmm/entry.h>
class TextInputDialog : public Gtk::Dialog { class TextInputDialog : public Gtk::Dialog {
public: public:

View File

@@ -1,5 +1,4 @@
#pragma once #pragma once
#include <gtkmm.h>
#include <string> #include <string>
class TokenDialog : public Gtk::Dialog { class TokenDialog : public Gtk::Dialog {

View File

@@ -1,5 +1,4 @@
#include "verificationgate.hpp" #include "verificationgate.hpp"
#include "abaddon.hpp"
VerificationGateDialog::VerificationGateDialog(Gtk::Window &parent, Snowflake guild_id) VerificationGateDialog::VerificationGateDialog(Gtk::Window &parent, Snowflake guild_id)
: Gtk::Dialog("Verification Required", parent, true) : Gtk::Dialog("Verification Required", parent, true)

View File

@@ -1,5 +1,4 @@
#pragma once #pragma once
#include <gtkmm.h>
#include <optional> #include <optional>
#include "discord/objects.hpp" #include "discord/objects.hpp"

View File

@@ -1,7 +1,6 @@
#pragma once #pragma once
#include <string> #include <string>
#include <optional> #include <optional>
#include "util.hpp"
#include "json.hpp" #include "json.hpp"
#include "snowflake.hpp" #include "snowflake.hpp"

View File

@@ -1,4 +1,3 @@
#include "abaddon.hpp"
#include "channel.hpp" #include "channel.hpp"
void from_json(const nlohmann::json &j, ThreadMetadataData &m) { void from_json(const nlohmann::json &j, ThreadMetadataData &m) {
@@ -84,6 +83,10 @@ bool ChannelData::IsCategory() const noexcept {
return Type == ChannelType::GUILD_CATEGORY; return Type == ChannelType::GUILD_CATEGORY;
} }
bool ChannelData::IsText() const noexcept {
return Type == ChannelType::GUILD_TEXT || Type == ChannelType::GUILD_NEWS;
}
bool ChannelData::HasIcon() const noexcept { bool ChannelData::HasIcon() const noexcept {
return Icon.has_value(); return Icon.has_value();
} }
@@ -102,15 +105,14 @@ std::string ChannelData::GetIconURL() const {
std::string ChannelData::GetDisplayName() const { std::string ChannelData::GetDisplayName() const {
if (Name.has_value()) { if (Name.has_value()) {
return "#" + *Name; if (IsDM()) {
return *Name;
} else {
return "#" + *Name;
}
} else { } else {
const auto recipients = GetDMRecipients(); return GetRecipientsDisplay();
if (Type == ChannelType::DM && !recipients.empty())
return recipients[0].Username;
else if (Type == ChannelType::GROUP_DM)
return std::to_string(recipients.size()) + " members";
} }
return "Unknown";
} }
std::vector<Snowflake> ChannelData::GetChildIDs() const { std::vector<Snowflake> ChannelData::GetChildIDs() const {
@@ -139,6 +141,25 @@ std::vector<UserData> ChannelData::GetDMRecipients() const {
return {}; return {};
} }
bool ChannelData::IsText() const noexcept {
return Type == ChannelType::GUILD_TEXT || Type == ChannelType::GUILD_NEWS; std::string ChannelData::GetRecipientsDisplay() const {
const auto self_id = Abaddon::Get().GetDiscordClient().GetUserData().ID;
const auto recipients = GetDMRecipients();
if (Type == ChannelType::DM && !recipients.empty()) {
return recipients[0].Username;
}
Glib::ustring r;
for (size_t i = 0; i < recipients.size(); i++) {
const auto &recipient = recipients[i];
r += recipient.Username;
if (i < recipients.size() - 1) {
r += ", ";
}
}
if (r.empty()) r = "Unnamed";
return r;
} }

View File

@@ -106,4 +106,5 @@ struct ChannelData {
[[nodiscard]] std::vector<Snowflake> GetChildIDs() const; [[nodiscard]] std::vector<Snowflake> GetChildIDs() const;
[[nodiscard]] std::optional<PermissionOverwrite> GetOverwrite(Snowflake id) const; [[nodiscard]] std::optional<PermissionOverwrite> GetOverwrite(Snowflake id) const;
[[nodiscard]] std::vector<UserData> GetDMRecipients() const; [[nodiscard]] std::vector<UserData> GetDMRecipients() const;
[[nodiscard]] std::string GetRecipientsDisplay() const;
}; };

View File

@@ -1,6 +1,4 @@
#include "abaddon.hpp"
#include "discord.hpp" #include "discord.hpp"
#include "util.hpp"
#include <spdlog/spdlog.h> #include <spdlog/spdlog.h>
#include <cinttypes> #include <cinttypes>
#include <utility> #include <utility>
@@ -354,12 +352,10 @@ bool DiscordClient::HasChannelPermission(Snowflake user_id, Snowflake channel_id
} }
Permission DiscordClient::ComputePermissions(Snowflake member_id, Snowflake guild_id) const { Permission DiscordClient::ComputePermissions(Snowflake member_id, Snowflake guild_id) const {
const auto member = GetMember(member_id, guild_id); const auto member_roles = m_store.GetMemberRoles(guild_id, member_id);
const auto guild = GetGuild(guild_id); const auto guild_owner = m_store.GetGuildOwner(guild_id);
if (!member.has_value() || !guild.has_value())
return Permission::NONE;
if (guild->OwnerID == member_id) if (guild_owner == member_id)
return Permission::ALL; return Permission::ALL;
const auto everyone = GetRole(guild_id); const auto everyone = GetRole(guild_id);
@@ -367,7 +363,7 @@ Permission DiscordClient::ComputePermissions(Snowflake member_id, Snowflake guil
return Permission::NONE; return Permission::NONE;
Permission perms = everyone->Permissions; Permission perms = everyone->Permissions;
for (const auto role_id : member->Roles) { for (const auto role_id : member_roles) {
const auto role = GetRole(role_id); const auto role = GetRole(role_id);
if (role.has_value()) if (role.has_value())
perms |= role->Permissions; perms |= role->Permissions;
@@ -384,8 +380,8 @@ Permission DiscordClient::ComputeOverwrites(Permission base, Snowflake member_id
return Permission::ALL; return Permission::ALL;
const auto channel = GetChannel(channel_id); const auto channel = GetChannel(channel_id);
const auto member = GetMember(member_id, *channel->GuildID); const auto member_roles = m_store.GetMemberRoles(*channel->GuildID, member_id);
if (!member.has_value() || !channel.has_value()) if (!channel.has_value())
return Permission::NONE; return Permission::NONE;
Permission perms = base; Permission perms = base;
@@ -397,7 +393,7 @@ Permission DiscordClient::ComputeOverwrites(Permission base, Snowflake member_id
Permission allow = Permission::NONE; Permission allow = Permission::NONE;
Permission deny = Permission::NONE; Permission deny = Permission::NONE;
for (const auto role_id : member->Roles) { for (const auto role_id : member_roles) {
const auto overwrite = GetPermissionOverwrite(channel_id, role_id); const auto overwrite = GetPermissionOverwrite(channel_id, role_id);
if (overwrite.has_value()) { if (overwrite.has_value()) {
allow |= overwrite->Allow; allow |= overwrite->Allow;
@@ -1280,6 +1276,10 @@ void DiscordClient::SetUserAgent(const std::string &agent) {
m_websocket.SetUserAgent(agent); m_websocket.SetUserAgent(agent);
} }
void DiscordClient::SetDumpReady(bool dump) {
m_dump_ready = dump;
}
bool DiscordClient::IsChannelMuted(Snowflake id) const noexcept { bool DiscordClient::IsChannelMuted(Snowflake id) const noexcept {
return m_muted_channels.find(id) != m_muted_channels.end(); return m_muted_channels.find(id) != m_muted_channels.end();
} }
@@ -1650,6 +1650,17 @@ void DiscordClient::ProcessNewGuild(GuildData &guild) {
void DiscordClient::HandleGatewayReady(const GatewayMessage &msg) { void DiscordClient::HandleGatewayReady(const GatewayMessage &msg) {
m_ready_received = true; m_ready_received = true;
if (m_dump_ready) {
const auto name = "./payload_ready-" + Glib::DateTime::create_now_utc().format("%Y-%m-%d_%H-%M-%S") + ".json";
auto *fp = std::fopen(name.c_str(), "wb");
if (fp != nullptr) {
const auto contents = msg.Data.dump(4);
std::fwrite(contents.data(), contents.size(), 1, fp);
std::fclose(fp);
}
}
ReadyEventData data = msg.Data; ReadyEventData data = msg.Data;
for (auto &g : data.Guilds) for (auto &g : data.Guilds)
ProcessNewGuild(g); ProcessNewGuild(g);
@@ -2428,6 +2439,10 @@ std::set<Snowflake> DiscordClient::GetPrivateChannels() const {
return {}; return {};
} }
const UserSettings &DiscordClient::GetUserSettings() const {
return m_user_settings;
}
EPremiumType DiscordClient::GetSelfPremiumType() const { EPremiumType DiscordClient::GetSelfPremiumType() const {
const auto &data = GetUserData(); const auto &data = GetUserData();
if (data.PremiumType.has_value()) if (data.PremiumType.has_value())

View File

@@ -37,6 +37,7 @@ public:
std::vector<Message> GetMessagesForChannel(Snowflake id, size_t limit = 50) const; std::vector<Message> GetMessagesForChannel(Snowflake id, size_t limit = 50) const;
std::vector<Message> GetMessagesBefore(Snowflake channel_id, Snowflake message_id, size_t limit = 50) const; std::vector<Message> GetMessagesBefore(Snowflake channel_id, Snowflake message_id, size_t limit = 50) const;
std::set<Snowflake> GetPrivateChannels() const; std::set<Snowflake> GetPrivateChannels() const;
const UserSettings &GetUserSettings() const;
EPremiumType GetSelfPremiumType() const; EPremiumType GetSelfPremiumType() const;
@@ -205,6 +206,8 @@ public:
void UpdateToken(const std::string &token); void UpdateToken(const std::string &token);
void SetUserAgent(const std::string &agent); void SetUserAgent(const std::string &agent);
void SetDumpReady(bool dump);
bool IsChannelMuted(Snowflake id) const noexcept; bool IsChannelMuted(Snowflake id) const noexcept;
bool IsGuildMuted(Snowflake id) const noexcept; bool IsGuildMuted(Snowflake id) const noexcept;
int GetUnreadStateForChannel(Snowflake id) const noexcept; int GetUnreadStateForChannel(Snowflake id) const noexcept;
@@ -223,6 +226,8 @@ private:
std::vector<uint8_t> m_decompress_buf; std::vector<uint8_t> m_decompress_buf;
z_stream m_zstream; z_stream m_zstream;
bool m_dump_ready = false;
static std::string GetAPIURL(); static std::string GetAPIURL();
static std::string GetGatewayURL(); static std::string GetGatewayURL();

View File

@@ -1,5 +1,4 @@
#include "guild.hpp" #include "guild.hpp"
#include "abaddon.hpp"
void from_json(const nlohmann::json &j, GuildData &m) { void from_json(const nlohmann::json &j, GuildData &m) {
JS_D("id", m.ID); JS_D("id", m.ID);

View File

@@ -1,6 +1,5 @@
#include "interactions.hpp" #include "interactions.hpp"
#include "json.hpp" #include "json.hpp"
#include "abaddon.hpp"
void from_json(const nlohmann::json &j, MessageInteractionData &m) { void from_json(const nlohmann::json &j, MessageInteractionData &m) {
JS_D("id", m.ID); JS_D("id", m.ID);

View File

@@ -1,24 +1,24 @@
#pragma once #pragma once
#include <nlohmann/json.hpp> #include <nlohmann/json.hpp>
#include <optional> #include <optional>
#include "util.hpp" #include "misc/is_optional.hpp"
namespace detail { // more or less because idk what to name this stuff namespace detail { // more or less because idk what to name this stuff
template<typename T> template<typename T>
inline void json_direct(const ::nlohmann::json &j, const char *key, T &val) { inline void json_direct(const nlohmann::json &j, const char *key, T &val) {
if constexpr (::util::is_optional<T>::value) if constexpr (is_optional<T>::value)
val = j.at(key).get<typename T::value_type>(); val = j.at(key).get<typename T::value_type>();
else else
j.at(key).get_to(val); j.at(key).get_to(val);
} }
template<typename T> template<typename T>
inline void json_optional(const ::nlohmann::json &j, const char *key, T &val) { inline void json_optional(const nlohmann::json &j, const char *key, T &val) {
if constexpr (::util::is_optional<T>::value) { if constexpr (is_optional<T>::value) {
if (j.contains(key)) if (j.contains(key))
val = j.at(key).get<typename T::value_type>(); val = j.at(key).get<typename T::value_type>();
else else
val = ::std::nullopt; val = std::nullopt;
} else { } else {
if (j.contains(key)) if (j.contains(key))
j.at(key).get_to(val); j.at(key).get_to(val);
@@ -26,13 +26,13 @@ inline void json_optional(const ::nlohmann::json &j, const char *key, T &val) {
} }
template<typename T> template<typename T>
inline void json_nullable(const ::nlohmann::json &j, const char *key, T &val) { inline void json_nullable(const nlohmann::json &j, const char *key, T &val) {
if constexpr (::util::is_optional<T>::value) { if constexpr (is_optional<T>::value) {
const auto &at = j.at(key); const auto &at = j.at(key);
if (!at.is_null()) if (!at.is_null())
val = at.get<typename T::value_type>(); val = at.get<typename T::value_type>();
else else
val = ::std::nullopt; val = std::nullopt;
} else { } else {
const auto &at = j.at(key); const auto &at = j.at(key);
if (!at.is_null()) if (!at.is_null())
@@ -41,16 +41,16 @@ inline void json_nullable(const ::nlohmann::json &j, const char *key, T &val) {
} }
template<typename T> template<typename T>
inline void json_optional_nullable(const ::nlohmann::json &j, const char *key, T &val) { inline void json_optional_nullable(const nlohmann::json &j, const char *key, T &val) {
if constexpr (::util::is_optional<T>::value) { if constexpr (is_optional<T>::value) {
if (j.contains(key)) { if (j.contains(key)) {
const auto &at = j.at(key); const auto &at = j.at(key);
if (!at.is_null()) if (!at.is_null())
val = at.get<typename T::value_type>(); val = at.get<typename T::value_type>();
else else
val = ::std::nullopt; val = std::nullopt;
} else { } else {
val = ::std::nullopt; val = std::nullopt;
} }
} else { } else {
if (j.contains(key)) { if (j.contains(key)) {
@@ -62,14 +62,14 @@ inline void json_optional_nullable(const ::nlohmann::json &j, const char *key, T
} }
template<typename T> template<typename T>
inline void json_update_optional_nullable(const ::nlohmann::json &j, const char *key, T &val) { inline void json_update_optional_nullable(const nlohmann::json &j, const char *key, T &val) {
if constexpr (::util::is_optional<T>::value) { if constexpr (is_optional<T>::value) {
if (j.contains(key)) { if (j.contains(key)) {
const auto &at = j.at(key); const auto &at = j.at(key);
if (!at.is_null()) if (!at.is_null())
val = at.get<typename T::value_type>(); val = at.get<typename T::value_type>();
else else
val = ::std::nullopt; val = std::nullopt;
} }
} else { } else {
if (j.contains(key)) { if (j.contains(key)) {
@@ -83,8 +83,8 @@ inline void json_update_optional_nullable(const ::nlohmann::json &j, const char
} }
template<typename T, typename U> template<typename T, typename U>
inline void json_update_optional_nullable_default(const ::nlohmann::json &j, const char *key, T &val, const U &fallback) { inline void json_update_optional_nullable_default(const nlohmann::json &j, const char *key, T &val, const U &fallback) {
if constexpr (::util::is_optional<T>::value) { if constexpr (is_optional<T>::value) {
if (j.contains(key)) { if (j.contains(key)) {
const auto &at = j.at(key); const auto &at = j.at(key);
if (at.is_null()) if (at.is_null())

View File

@@ -1,5 +1,4 @@
#include "member.hpp" #include "member.hpp"
#include "abaddon.hpp"
void from_json(const nlohmann::json &j, GuildMember &m) { void from_json(const nlohmann::json &j, GuildMember &m) {
JS_O("user", m.User); JS_O("user", m.User);

View File

@@ -1,8 +1,8 @@
#pragma once #pragma once
#include <cstdint> #include <cstdint>
#include "snowflake.hpp"
#include "json.hpp" #include "json.hpp"
#include "util.hpp" #include "misc/bitwise.hpp"
#include "snowflake.hpp"
constexpr static uint64_t PERMISSION_MAX_BIT = 36; constexpr static uint64_t PERMISSION_MAX_BIT = 36;
enum class Permission : uint64_t { enum class Permission : uint64_t {
@@ -46,6 +46,7 @@ enum class Permission : uint64_t {
ALL = 0x1FFFFFFFFFULL, ALL = 0x1FFFFFFFFFULL,
}; };
template<> template<>
struct Bitwise<Permission> { struct Bitwise<Permission> {
static const bool enable = true; static const bool enable = true;

View File

@@ -1,5 +1,4 @@
#include "snowflake.hpp" #include "snowflake.hpp"
#include "util.hpp"
#include <chrono> #include <chrono>
#include <ctime> #include <ctime>
#include <iomanip> #include <iomanip>

View File

@@ -473,6 +473,40 @@ std::vector<BanData> Store::GetBans(Snowflake guild_id) const {
return ret; return ret;
} }
Snowflake Store::GetGuildOwner(Snowflake guild_id) const {
auto &s = m_stmt_get_guild_owner;
s->Bind(1, guild_id);
if (s->FetchOne()) {
Snowflake ret;
s->Get(0, ret);
s->Reset();
return ret;
}
s->Reset();
return Snowflake::Invalid;
}
std::vector<Snowflake> Store::GetMemberRoles(Snowflake guild_id, Snowflake user_id) const {
std::vector<Snowflake> ret;
auto &s = m_stmt_get_member_roles;
s->Bind(1, user_id);
s->Bind(2, guild_id);
while (s->FetchOne()) {
auto &f = ret.emplace_back();
s->Get(0, f);
}
s->Reset();
return ret;
}
std::vector<Message> Store::GetLastMessages(Snowflake id, size_t num) const { std::vector<Message> Store::GetLastMessages(Snowflake id, size_t num) const {
auto &s = m_stmt_get_last_msgs; auto &s = m_stmt_get_last_msgs;
std::vector<Message> msgs; std::vector<Message> msgs;
@@ -2198,6 +2232,14 @@ bool Store::CreateStatements() {
return false; return false;
} }
m_stmt_get_guild_owner = std::make_unique<Statement>(m_db, R"(
SELECT owner_id FROM guilds WHERE id = ?
)");
if (!m_stmt_get_guild_owner->OK()) {
fprintf(stderr, "failed to prepare get guild owner statement: %s\n", m_db.ErrStr());
return false;
}
return true; return true;
} }

View File

@@ -1,5 +1,4 @@
#pragma once #pragma once
#include "util.hpp"
#include "objects.hpp" #include "objects.hpp"
#include <unordered_map> #include <unordered_map>
#include <unordered_set> #include <unordered_set>
@@ -39,6 +38,9 @@ public:
std::optional<BanData> GetBan(Snowflake guild_id, Snowflake user_id) const; std::optional<BanData> GetBan(Snowflake guild_id, Snowflake user_id) const;
std::vector<BanData> GetBans(Snowflake guild_id) const; std::vector<BanData> GetBans(Snowflake guild_id) const;
Snowflake GetGuildOwner(Snowflake guild_id) const;
std::vector<Snowflake> GetMemberRoles(Snowflake guild_id, Snowflake user_id) const;
std::vector<Message> GetLastMessages(Snowflake id, size_t num) const; 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> GetMessagesBefore(Snowflake channel_id, Snowflake message_id, size_t limit) const;
std::vector<Message> GetPinnedMessages(Snowflake channel_id) const; std::vector<Message> GetPinnedMessages(Snowflake channel_id) const;
@@ -308,5 +310,6 @@ private:
STMT(get_chan_ids_parent); STMT(get_chan_ids_parent);
STMT(get_guild_member_ids); STMT(get_guild_member_ids);
STMT(clr_role); STMT(clr_role);
STMT(get_guild_owner);
#undef STMT #undef STMT
}; };

View File

@@ -1,5 +1,4 @@
#include "user.hpp" #include "user.hpp"
#include "abaddon.hpp"
bool UserData::IsABot() const noexcept { bool UserData::IsABot() const noexcept {
return IsBot.has_value() && *IsBot; return IsBot.has_value() && *IsBot;

View File

@@ -1,13 +1,14 @@
#pragma once #pragma once
#include "json.hpp" #include "json.hpp"
#include "snowflake.hpp" #include "snowflake.hpp"
#include <optional>
#include <string> #include <string>
struct UserSettingsGuildFoldersEntry { struct UserSettingsGuildFoldersEntry {
int Color = -1; // null std::optional<int> Color;
std::vector<Snowflake> GuildIDs; std::vector<Snowflake> GuildIDs;
Snowflake ID; // null (this can be a snowflake as a string or an int that isnt a snowflake lol) std::optional<Snowflake> ID; // (this can be a snowflake as a string or an int that isnt a snowflake lol)
std::string Name; // null std::optional<std::string> Name;
friend void from_json(const nlohmann::json &j, UserSettingsGuildFoldersEntry &m); friend void from_json(const nlohmann::json &j, UserSettingsGuildFoldersEntry &m);
}; };

View File

@@ -3,7 +3,6 @@
#include <cstdio> #include <cstdio>
#include <unordered_map> #include <unordered_map>
#include <vector> #include <vector>
#include <gtkmm.h>
// shoutout to gtk for only supporting .svg's sometimes // shoutout to gtk for only supporting .svg's sometimes

View File

@@ -1,4 +1,3 @@
#include "abaddon.hpp"
#include "filecache.hpp" #include "filecache.hpp"
#include <utility> #include <utility>

View File

@@ -8,7 +8,6 @@
#include <unordered_map> #include <unordered_map>
#include <future> #include <future>
#include <mutex> #include <mutex>
#include "util.hpp"
#include "http.hpp" #include "http.hpp"
class FileCacheWorkerThread { class FileCacheWorkerThread {

View File

@@ -1,8 +1,6 @@
#include "imgmanager.hpp" #include "imgmanager.hpp"
#include <utility> #include <utility>
#include "util.hpp"
#include "abaddon.hpp"
ImageManager::ImageManager() { ImageManager::ImageManager() {
m_cb_dispatcher.connect(sigc::mem_fun(*this, &ImageManager::RunCallbacks)); m_cb_dispatcher.connect(sigc::mem_fun(*this, &ImageManager::RunCallbacks));

View File

@@ -3,7 +3,6 @@
#include <unordered_map> #include <unordered_map>
#include <functional> #include <functional>
#include <queue> #include <queue>
#include <gtkmm.h>
#include "filecache.hpp" #include "filecache.hpp"
class ImageManager { class ImageManager {

38
src/misc/bitwise.hpp Normal file
View File

@@ -0,0 +1,38 @@
#pragma once
#include <type_traits>
template<typename T>
struct Bitwise {
static const bool enable = false;
};
template<typename T>
typename std::enable_if<Bitwise<T>::enable, T>::type operator|(T a, T b) {
using x = typename std::underlying_type<T>::type;
return static_cast<T>(static_cast<x>(a) | static_cast<x>(b));
}
template<typename T>
typename std::enable_if<Bitwise<T>::enable, T>::type operator|=(T &a, T b) {
using x = typename std::underlying_type<T>::type;
a = static_cast<T>(static_cast<x>(a) | static_cast<x>(b));
return a;
}
template<typename T>
typename std::enable_if<Bitwise<T>::enable, T>::type operator&(T a, T b) {
using x = typename std::underlying_type<T>::type;
return static_cast<T>(static_cast<x>(a) & static_cast<x>(b));
}
template<typename T>
typename std::enable_if<Bitwise<T>::enable, T>::type operator&=(T &a, T b) {
using x = typename std::underlying_type<T>::type;
a = static_cast<T>(static_cast<x>(a) & static_cast<x>(b));
return a;
}
template<typename T>
typename std::enable_if<Bitwise<T>::enable, T>::type operator~(T a) {
return static_cast<T>(~static_cast<typename std::underlying_type<T>::type>(a));
}

9
src/misc/is_optional.hpp Normal file
View File

@@ -0,0 +1,9 @@
#pragma once
#include <optional>
#include <type_traits>
template<typename T>
struct is_optional : std::false_type {};
template<typename T>
struct is_optional<std::optional<T>> : std::true_type {};

View File

@@ -1,5 +1,4 @@
#include "platform.hpp" #include "platform.hpp"
#include "util.hpp"
#include <config.h> #include <config.h>
#include <filesystem> #include <filesystem>
#include <fstream> #include <fstream>

View File

@@ -1,6 +1,15 @@
#include "settings.hpp" #include "settings.hpp"
#include <filesystem> #include <filesystem>
#include <fstream> #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) SettingsManager::SettingsManager(const std::string &filename)
: m_filename(filename) { : m_filename(filename) {
@@ -36,7 +45,6 @@ void SettingsManager::ReadSettings() {
SMSTR("discord", "api_base", APIBaseURL); SMSTR("discord", "api_base", APIBaseURL);
SMSTR("discord", "gateway", GatewayURL); SMSTR("discord", "gateway", GatewayURL);
SMSTR("discord", "token", DiscordToken);
SMBOOL("discord", "memory_db", UseMemoryDB); SMBOOL("discord", "memory_db", UseMemoryDB);
SMBOOL("discord", "prefetch", Prefetch); SMBOOL("discord", "prefetch", Prefetch);
SMBOOL("discord", "autoconnect", Autoconnect); SMBOOL("discord", "autoconnect", Autoconnect);
@@ -61,6 +69,32 @@ void SettingsManager::ReadSettings() {
SMSTR("style", "mentionbadgetextcolor", MentionBadgeTextColor); SMSTR("style", "mentionbadgetextcolor", MentionBadgeTextColor);
SMSTR("style", "unreadcolor", UnreadIndicatorColor); 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 SMBOOL
#undef SMSTR #undef SMSTR
#undef SMINT #undef SMINT
@@ -92,7 +126,6 @@ void SettingsManager::Close() {
SMSTR("discord", "api_base", APIBaseURL); SMSTR("discord", "api_base", APIBaseURL);
SMSTR("discord", "gateway", GatewayURL); SMSTR("discord", "gateway", GatewayURL);
SMSTR("discord", "token", DiscordToken);
SMBOOL("discord", "memory_db", UseMemoryDB); SMBOOL("discord", "memory_db", UseMemoryDB);
SMBOOL("discord", "prefetch", Prefetch); SMBOOL("discord", "prefetch", Prefetch);
SMBOOL("discord", "autoconnect", Autoconnect); SMBOOL("discord", "autoconnect", Autoconnect);
@@ -117,6 +150,17 @@ void SettingsManager::Close() {
SMSTR("style", "mentionbadgetextcolor", MentionBadgeTextColor); SMSTR("style", "mentionbadgetextcolor", MentionBadgeTextColor);
SMSTR("style", "unreadcolor", UnreadIndicatorColor); 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 SMSTR
#undef SMBOOL #undef SMBOOL
#undef SMINT #undef SMINT

View File

@@ -1,5 +1,4 @@
#include "startup.hpp" #include "startup.hpp"
#include "abaddon.hpp"
#include <future> #include <future>
#include <memory> #include <memory>

View File

@@ -1,7 +1,5 @@
#pragma once #pragma once
#include <glibmm/dispatcher.h> #include <glibmm/dispatcher.h>
#include <gtkmm/messagedialog.h>
#include <gtkmm/window.h>
#include <optional> #include <optional>
// fetch cookies, build number async // fetch cookies, build number async

View File

@@ -1,4 +1,3 @@
#include "util.hpp"
#include <array> #include <array>
#include <cstring> #include <cstring>
#include <filesystem> #include <filesystem>

View File

@@ -13,17 +13,26 @@
#include <condition_variable> #include <condition_variable>
#include <optional> #include <optional>
#include <type_traits> #include <type_traits>
#include <gtkmm.h>
#include <sigc++/slot.h>
namespace Glib {
class ustring;
}
namespace Gdk {
class RGBA;
}
namespace Gtk {
class Widget;
class Menu;
class ListBox;
} // namespace Gtk
#define NOOP_CALLBACK [](...) {} #define NOOP_CALLBACK [](...) {}
namespace util { namespace util {
template<typename T>
struct is_optional : ::std::false_type {};
template<typename T>
struct is_optional<::std::optional<T>> : ::std::true_type {};
bool IsFolder(std::string_view path); bool IsFolder(std::string_view path);
bool IsFile(std::string_view path); bool IsFile(std::string_view path);
@@ -44,42 +53,6 @@ std::string HumanReadableBytes(uint64_t bytes);
std::string FormatISO8601(const std::string &in, int extra_offset = 0, const std::string &fmt = "%x %X"); std::string FormatISO8601(const std::string &in, int extra_offset = 0, const std::string &fmt = "%x %X");
void AddPointerCursor(Gtk::Widget &widget); void AddPointerCursor(Gtk::Widget &widget);
template<typename T>
struct Bitwise {
static const bool enable = false;
};
template<typename T>
typename std::enable_if<Bitwise<T>::enable, T>::type operator|(T a, T b) {
using x = typename std::underlying_type<T>::type;
return static_cast<T>(static_cast<x>(a) | static_cast<x>(b));
}
template<typename T>
typename std::enable_if<Bitwise<T>::enable, T>::type operator|=(T &a, T b) {
using x = typename std::underlying_type<T>::type;
a = static_cast<T>(static_cast<x>(a) | static_cast<x>(b));
return a;
}
template<typename T>
typename std::enable_if<Bitwise<T>::enable, T>::type operator&(T a, T b) {
using x = typename std::underlying_type<T>::type;
return static_cast<T>(static_cast<x>(a) & static_cast<x>(b));
}
template<typename T>
typename std::enable_if<Bitwise<T>::enable, T>::type operator&=(T &a, T b) {
using x = typename std::underlying_type<T>::type;
a = static_cast<T>(static_cast<x>(a) & static_cast<x>(b));
return a;
}
template<typename T>
typename std::enable_if<Bitwise<T>::enable, T>::type operator~(T a) {
return static_cast<T>(~static_cast<typename std::underlying_type<T>::type>(a));
}
template<typename T> template<typename T>
inline void AlphabeticalSort(T start, T end, std::function<std::string(const typename std::iterator_traits<T>::value_type &)> get_string) { inline void AlphabeticalSort(T start, T end, std::function<std::string(const typename std::iterator_traits<T>::value_type &)> get_string) {
std::sort(start, end, [&](const auto &a, const auto &b) -> bool { std::sort(start, end, [&](const auto &a, const auto &b) -> bool {

View File

@@ -1,5 +1,4 @@
#include "auditlogpane.hpp" #include "auditlogpane.hpp"
#include "abaddon.hpp"
using namespace std::string_literals; using namespace std::string_literals;

View File

@@ -1,5 +1,4 @@
#pragma once #pragma once
#include <gtkmm.h>
#include "discord/objects.hpp" #include "discord/objects.hpp"
class GuildSettingsAuditLogPane : public Gtk::ScrolledWindow { class GuildSettingsAuditLogPane : public Gtk::ScrolledWindow {

View File

@@ -1,5 +1,4 @@
#include "banspane.hpp" #include "banspane.hpp"
#include "abaddon.hpp"
// gtk_list_store_set_value: assertion 'column >= 0 && column < priv->n_columns' failed // gtk_list_store_set_value: assertion 'column >= 0 && column < priv->n_columns' failed
// dont care to figure out why this happens cuz it doesnt seem to break anything // dont care to figure out why this happens cuz it doesnt seem to break anything

View File

@@ -1,5 +1,4 @@
#pragma once #pragma once
#include <gtkmm.h>
#include "discord/snowflake.hpp" #include "discord/snowflake.hpp"
#include "discord/ban.hpp" #include "discord/ban.hpp"

View File

@@ -1,5 +1,4 @@
#include "emojispane.hpp" #include "emojispane.hpp"
#include "abaddon.hpp"
#include "components/cellrendererpixbufanimation.hpp" #include "components/cellrendererpixbufanimation.hpp"
GuildSettingsEmojisPane::GuildSettingsEmojisPane(Snowflake guild_id) GuildSettingsEmojisPane::GuildSettingsEmojisPane(Snowflake guild_id)

View File

@@ -1,5 +1,4 @@
#pragma once #pragma once
#include <gtkmm.h>
#include "discord/emoji.hpp" #include "discord/emoji.hpp"
class GuildSettingsEmojisPane : public Gtk::Box { class GuildSettingsEmojisPane : public Gtk::Box {

View File

@@ -1,5 +1,4 @@
#include "infopane.hpp" #include "infopane.hpp"
#include "abaddon.hpp"
#include <filesystem> #include <filesystem>
GuildSettingsInfoPane::GuildSettingsInfoPane(Snowflake id) GuildSettingsInfoPane::GuildSettingsInfoPane(Snowflake id)

View File

@@ -1,5 +1,4 @@
#pragma once #pragma once
#include <gtkmm.h>
#include "discord/guild.hpp" #include "discord/guild.hpp"
class GuildSettingsInfoPane : public Gtk::Grid { class GuildSettingsInfoPane : public Gtk::Grid {

View File

@@ -1,5 +1,4 @@
#include "invitespane.hpp" #include "invitespane.hpp"
#include "abaddon.hpp"
GuildSettingsInvitesPane::GuildSettingsInvitesPane(Snowflake id) GuildSettingsInvitesPane::GuildSettingsInvitesPane(Snowflake id)
: GuildID(id) : GuildID(id)

View File

@@ -1,5 +1,4 @@
#pragma once #pragma once
#include <gtkmm.h>
#include "discord/objects.hpp" #include "discord/objects.hpp"
class GuildSettingsInvitesPane : public Gtk::ScrolledWindow { class GuildSettingsInvitesPane : public Gtk::ScrolledWindow {

View File

@@ -1,5 +1,4 @@
#include "memberspane.hpp" #include "memberspane.hpp"
#include "abaddon.hpp"
GuildSettingsMembersPane::GuildSettingsMembersPane(Snowflake id) GuildSettingsMembersPane::GuildSettingsMembersPane(Snowflake id)
: Gtk::Box(Gtk::ORIENTATION_VERTICAL) : Gtk::Box(Gtk::ORIENTATION_VERTICAL)

View File

@@ -1,6 +1,5 @@
#pragma once #pragma once
#include <unordered_set> #include <unordered_set>
#include <gtkmm.h>
#include "discord/member.hpp" #include "discord/member.hpp"
#include "discord/guild.hpp" #include "discord/guild.hpp"
#include "components/lazyimage.hpp" #include "components/lazyimage.hpp"

View File

@@ -1,5 +1,4 @@
#include "rolespane.hpp" #include "rolespane.hpp"
#include "abaddon.hpp"
GuildSettingsRolesPane::GuildSettingsRolesPane(Snowflake id) GuildSettingsRolesPane::GuildSettingsRolesPane(Snowflake id)
: Gtk::Box(Gtk::ORIENTATION_HORIZONTAL) : Gtk::Box(Gtk::ORIENTATION_HORIZONTAL)

View File

@@ -1,5 +1,4 @@
#pragma once #pragma once
#include <gtkmm.h>
#include <unordered_map> #include <unordered_map>
#include "discord/guild.hpp" #include "discord/guild.hpp"
#include "components/draglistbox.hpp" #include "components/draglistbox.hpp"

View File

@@ -1,5 +1,4 @@
#include "guildsettingswindow.hpp" #include "guildsettingswindow.hpp"
#include "abaddon.hpp"
GuildSettingsWindow::GuildSettingsWindow(Snowflake id) GuildSettingsWindow::GuildSettingsWindow(Snowflake id)
: m_main(Gtk::ORIENTATION_VERTICAL) : m_main(Gtk::ORIENTATION_VERTICAL)

View File

@@ -1,5 +1,4 @@
#pragma once #pragma once
#include <gtkmm.h>
#include "discord/snowflake.hpp" #include "discord/snowflake.hpp"
#include "guildsettings/infopane.hpp" #include "guildsettings/infopane.hpp"
#include "guildsettings/banspane.hpp" #include "guildsettings/banspane.hpp"

View File

@@ -1,5 +1,4 @@
#include "mainwindow.hpp" #include "mainwindow.hpp"
#include "abaddon.hpp"
MainWindow::MainWindow() MainWindow::MainWindow()
: m_main_box(Gtk::ORIENTATION_VERTICAL) : m_main_box(Gtk::ORIENTATION_VERTICAL)
@@ -262,8 +261,10 @@ void MainWindow::SetupMenu() {
m_menu_file.set_submenu(m_menu_file_sub); m_menu_file.set_submenu(m_menu_file_sub);
m_menu_file_reload_css.set_label("Reload CSS"); m_menu_file_reload_css.set_label("Reload CSS");
m_menu_file_clear_cache.set_label("Clear file cache"); m_menu_file_clear_cache.set_label("Clear file cache");
m_menu_file_dump_ready.set_label("Dump ready message");
m_menu_file_sub.append(m_menu_file_reload_css); m_menu_file_sub.append(m_menu_file_reload_css);
m_menu_file_sub.append(m_menu_file_clear_cache); m_menu_file_sub.append(m_menu_file_clear_cache);
m_menu_file_sub.append(m_menu_file_dump_ready);
m_menu_view.set_label("View"); m_menu_view.set_label("View");
m_menu_view.set_submenu(m_menu_view_sub); m_menu_view.set_submenu(m_menu_view_sub);
@@ -272,6 +273,12 @@ void MainWindow::SetupMenu() {
m_menu_view_threads.set_label("Threads"); 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.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_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 #ifdef WITH_LIBHANDY
m_menu_view_go_back.set_label("Go Back"); m_menu_view_go_back.set_label("Go Back");
m_menu_view_go_forward.set_label("Go Forward"); m_menu_view_go_forward.set_label("Go Forward");
@@ -282,6 +289,8 @@ void MainWindow::SetupMenu() {
m_menu_view_sub.append(m_menu_view_pins); 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_threads);
m_menu_view_sub.append(m_menu_view_mark_guild_as_read); 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 #ifdef WITH_LIBHANDY
m_menu_view_sub.append(m_menu_view_go_back); m_menu_view_sub.append(m_menu_view_go_back);
m_menu_view_sub.append(m_menu_view_go_forward); m_menu_view_sub.append(m_menu_view_go_forward);
@@ -334,6 +343,10 @@ void MainWindow::SetupMenu() {
Abaddon::Get().GetImageManager().ClearCache(); Abaddon::Get().GetImageManager().ClearCache();
}); });
m_menu_file_dump_ready.signal_toggled().connect([this]() {
Abaddon::Get().GetDiscordClient().SetDumpReady(m_menu_file_dump_ready.get_active());
});
m_menu_discord_add_recipient.signal_activate().connect([this] { m_menu_discord_add_recipient.signal_activate().connect([this] {
m_signal_action_add_recipient.emit(GetChatActiveChannel()); m_signal_action_add_recipient.emit(GetChatActiveChannel());
}); });
@@ -361,6 +374,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.GetRoot()->set_visible(m_menu_view_members.get_active());
});
#ifdef WITH_LIBHANDY #ifdef WITH_LIBHANDY
m_menu_view_go_back.signal_activate().connect([this] { m_menu_view_go_back.signal_activate().connect([this] {
GoBack(); GoBack();

View File

@@ -3,8 +3,11 @@
#include "components/chatwindow.hpp" #include "components/chatwindow.hpp"
#include "components/memberlist.hpp" #include "components/memberlist.hpp"
#include "components/friendslist.hpp" #include "components/friendslist.hpp"
<<<<<<< HEAD
#include "components/voiceinfobox.hpp" #include "components/voiceinfobox.hpp"
#include <gtkmm.h> #include <gtkmm.h>
=======
>>>>>>> master
class MainWindow : public Gtk::Window { class MainWindow : public Gtk::Window {
public: public:
@@ -76,6 +79,7 @@ private:
Gtk::Menu m_menu_file_sub; Gtk::Menu m_menu_file_sub;
Gtk::MenuItem m_menu_file_reload_css; Gtk::MenuItem m_menu_file_reload_css;
Gtk::MenuItem m_menu_file_clear_cache; Gtk::MenuItem m_menu_file_clear_cache;
Gtk::CheckMenuItem m_menu_file_dump_ready;
Gtk::MenuItem m_menu_view; Gtk::MenuItem m_menu_view;
Gtk::Menu m_menu_view_sub; Gtk::Menu m_menu_view_sub;
@@ -83,6 +87,8 @@ private:
Gtk::MenuItem m_menu_view_pins; Gtk::MenuItem m_menu_view_pins;
Gtk::MenuItem m_menu_view_threads; Gtk::MenuItem m_menu_view_threads;
Gtk::MenuItem m_menu_view_mark_guild_as_read; Gtk::MenuItem m_menu_view_mark_guild_as_read;
Gtk::CheckMenuItem m_menu_view_channels;
Gtk::CheckMenuItem m_menu_view_members;
#ifdef WITH_LIBHANDY #ifdef WITH_LIBHANDY
Gtk::MenuItem m_menu_view_go_back; Gtk::MenuItem m_menu_view_go_back;
Gtk::MenuItem m_menu_view_go_forward; Gtk::MenuItem m_menu_view_go_forward;

View File

@@ -1,5 +1,4 @@
#include "pinnedwindow.hpp" #include "pinnedwindow.hpp"
#include "abaddon.hpp"
PinnedWindow::PinnedWindow(const ChannelData &data) PinnedWindow::PinnedWindow(const ChannelData &data)
: ChannelID(data.ID) { : ChannelID(data.ID) {

View File

@@ -1,5 +1,4 @@
#pragma once #pragma once
#include <gtkmm.h>
#include "discord/errors.hpp" #include "discord/errors.hpp"
#include "discord/channel.hpp" #include "discord/channel.hpp"
#include "discord/message.hpp" #include "discord/message.hpp"

View File

@@ -1,5 +1,4 @@
#include "mutualfriendspane.hpp" #include "mutualfriendspane.hpp"
#include "abaddon.hpp"
MutualFriendItem::MutualFriendItem(const UserData &user) MutualFriendItem::MutualFriendItem(const UserData &user)
: Gtk::Box(Gtk::ORIENTATION_HORIZONTAL) { : Gtk::Box(Gtk::ORIENTATION_HORIZONTAL) {

View File

@@ -1,5 +1,4 @@
#pragma once #pragma once
#include <gtkmm.h>
#include "discord/objects.hpp" #include "discord/objects.hpp"
class MutualFriendItem : public Gtk::Box { class MutualFriendItem : public Gtk::Box {

View File

@@ -1,5 +1,4 @@
#include "mutualguildspane.hpp" #include "mutualguildspane.hpp"
#include "abaddon.hpp"
MutualGuildItem::MutualGuildItem(const MutualGuildData &guild) MutualGuildItem::MutualGuildItem(const MutualGuildData &guild)
: Gtk::Box(Gtk::ORIENTATION_HORIZONTAL) : Gtk::Box(Gtk::ORIENTATION_HORIZONTAL)

View File

@@ -1,5 +1,4 @@
#pragma once #pragma once
#include <gtkmm.h>
#include "discord/objects.hpp" #include "discord/objects.hpp"
class MutualGuildItem : public Gtk::Box { class MutualGuildItem : public Gtk::Box {

Some files were not shown because too many files have changed in this diff Show More