6 Commits

Author SHA1 Message Date
ouwou
ad0d4e7d4d merge 2021-05-06 20:11:15 -04:00
ouwou
1ca6235e09 Merge branch 'master' into mobile 2021-05-02 00:45:00 -04:00
ouwou
c04fc2e60c Merge branch 'master' into mobile 2021-04-24 04:26:06 -04:00
ouwou
a7bf9a2404 mobile layout (#24) 2021-04-18 23:51:43 -04:00
ouwou
2065ef4940 Merge branch 'alt-channels' into mobile 2021-04-18 23:39:09 -04:00
ouwou
3aefab652e add option to use labels for channel list (#24) 2021-04-15 15:45:49 -04:00
250 changed files with 5398 additions and 13406 deletions

View File

@@ -3,60 +3,56 @@ name: Abaddon CI
on: [push, pull_request]
jobs:
msys2:
name: msys2-mingw64
windows:
name: windows-${{ matrix.buildtype }}
runs-on: windows-latest
strategy:
matrix:
buildtype: [Debug, RelWithDebInfo, MinSizeRel]
defaults:
run:
shell: msys2 {0}
buildtype: [Debug, RelWithDebInfo]
steps:
- uses: actions/checkout@v1
with:
submodules: true
- name: Setup MSYS2
uses: msys2/setup-msys2@v2
- name: Fetch CMake
uses: lukka/get-cmake@latest
- name: Fetch dependencies
uses: lukka/run-vcpkg@main
with:
msystem: mingw64
update: true
install: >-
git
make
mingw-w64-x86_64-toolchain
mingw-w64-x86_64-cmake
mingw-w64-x86_64-ninja
mingw-w64-x86_64-sqlite3
mingw-w64-x86_64-nlohmann-json
mingw-w64-x86_64-curl
mingw-w64-x86_64-zlib
mingw-w64-x86_64-gtkmm3
vcpkgArguments: gtkmm nlohmann-json zlib sqlite3 glibmm openssl ixwebsocket curl
vcpkgDirectory: ${{ github.workspace }}/ci/vcpkg/
vcpkgTriplet: x64-windows
- name: Build
run: |
cmake -GNinja -Bbuild -DCMAKE_BUILD_TYPE=${{ matrix.buildtype }}
cmake --build build
uses: lukka/run-cmake@main
with:
useVcpkgToolchainFile: true
vcpkgTriplet: x64-windows
buildDirectory: ${{ runner.workspace }}/build
cmakeBuildType: ${{ matrix.buildtype }}
- name: Setup Artifact
- name: Setup artifact files
shell: cmd
run: |
mkdir -p build/artifactdir/bin build/artifactdir/ssl/certs build/artifactdir/lib build/artifactdir/share/glib-2.0/schemas
cd build
cp *.exe artifactdir/bin
cd ..
cp /mingw64/ssl/certs/ca-bundle.crt build/artifactdir/ssl/certs
cp -r /mingw64/lib/gdk-pixbuf-2.0 build/artifactdir/lib
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
cat "ci/msys-deps.txt" | sed 's/\r$//' | xargs -I % cp /mingw64% build/artifactdir/bin
del /f /s /q "${{ runner.workspace }}\build\CMakeFiles"
rmdir /s /q "${{ runner.workspace }}\build\CMakeFiles"
del /f /s /q "${{ runner.workspace }}\build\.ninja_deps"
del /f /s /q "${{ runner.workspace }}\build\.ninja_log"
del /f /s /q "${{ runner.workspace }}\build\abaddon.ilk"
del /f /s /q "${{ runner.workspace }}\build\CMakeCache.txt"
xcopy /E /I "${{ github.workspace }}\css" "${{ runner.workspace }}\build\css"
xcopy /E /I "${{ github.workspace }}\res" "${{ runner.workspace }}\build\res"
mkdir "${{ runner.workspace }}\build\share"
xcopy /E /I "${{ github.workspace }}\ci\gtk-for-windows\gtk-nsis-pack\share\glib-2.0" "${{ runner.workspace }}\build\share\glib-2.0"
copy "${{ github.workspace }}\ci\vcpkg\installed\x64-windows\tools\glib\gspawn-win64-helper.exe" "${{ runner.workspace }}\build\gspawn-win64-helper.exe"
copy "${{ github.workspace }}\ci\vcpkg\installed\x64-windows\tools\glib\gspawn-win64-helper-console.exe" "${{ runner.workspace }}\build\gspawn-win64-helper-console.exe"
- name: Upload build
uses: actions/upload-artifact@v2
with:
name: build-windows-msys2-${{ matrix.buildtype }}
path: build/artifactdir
name: build-windows-${{ matrix.buildtype }}
path: ${{ runner.workspace }}/build
mac:
name: macos-${{ matrix.buildtype }}
runs-on: macos-latest
@@ -69,7 +65,7 @@ jobs:
submodules: true
- name: Fetch CMake
uses: lukka/get-cmake@v3.21.2
uses: lukka/get-cmake@latest
- name: Fetch dependencies
run: |
@@ -77,7 +73,7 @@ jobs:
brew install nlohmann-json
- name: Build
uses: lukka/run-cmake@v3
uses: lukka/run-cmake@main
with:
buildDirectory: ${{ runner.workspace }}/build
cmakeBuildType: ${{ matrix.buildtype }}
@@ -86,8 +82,8 @@ jobs:
run: |
mkdir "${{ runner.workspace }}/artifactdir"
cp "${{runner.workspace}}/build/abaddon" "${{ runner.workspace }}/artifactdir/abaddon"
cp -r "${{ github.workspace }}/res/css" "${{ runner.workspace }}/artifactdir/css"
cp -r "${{ github.workspace }}/res/res" "${{ runner.workspace }}/artifactdir/res"
cp -r "${{ github.workspace }}/css" "${{ runner.workspace }}/artifactdir/css"
cp -r "${{ github.workspace }}/res" "${{ runner.workspace }}/artifactdir/res"
- name: Upload build
uses: actions/upload-artifact@v2
@@ -107,7 +103,7 @@ jobs:
submodules: true
- name: Fetch CMake
uses: lukka/get-cmake@v3.21.2
uses: lukka/get-cmake@latest
- name: Fetch dependencies
run: |
@@ -126,7 +122,7 @@ jobs:
sudo apt-get install libcurl4-gnutls-dev
- name: Build
uses: lukka/run-cmake@v3
uses: lukka/run-cmake@main
env:
CC: gcc-9
CXX: g++-9
@@ -139,8 +135,8 @@ jobs:
run: |
mkdir "${{ runner.workspace }}/artifactdir"
cp "${{runner.workspace}}/build/abaddon" "${{ runner.workspace }}/artifactdir/abaddon"
cp -r "${{ github.workspace }}/res/css" "${{ runner.workspace }}/artifactdir/css"
cp -r "${{ github.workspace }}/res/res" "${{ runner.workspace }}/artifactdir/res"
cp -r "${{ github.workspace }}/css" "${{ runner.workspace }}/artifactdir/css"
cp -r "${{ github.workspace }}/res" "${{ runner.workspace }}/artifactdir/res"
- name: Upload build
uses: actions/upload-artifact@v2

2
.gitignore vendored
View File

@@ -354,5 +354,3 @@ testdata/
build/
out/
fonts/fonts.conf

15
.gitmodules vendored
View File

@@ -1,6 +1,15 @@
[submodule "vcpkg"]
path = ci/vcpkg
url = https://github.com/microsoft/vcpkg/
[submodule "thirdparty/simpleini"]
path = thirdparty/simpleini
url = https://github.com/brofield/simpleini
[submodule "thirdparty/IXWebSocket"]
path = thirdparty/IXWebSocket
url = https://github.com/machinezone/ixwebsocket
[submodule "ci/vcpkg"]
path = ci/vcpkg
url = https://github.com/microsoft/vcpkg
[submodule "ci/gtk-for-windows"]
path = ci/gtk-for-windows
url = https://github.com/tschoonj/GTK-for-Windows-Runtime-Environment-Installer
[submodule "subprojects/ixwebsocket"]
path = subprojects/ixwebsocket
url = https://github.com/machinezone/ixwebsocket

View File

@@ -2,26 +2,27 @@ cmake_minimum_required(VERSION 3.16)
project(abaddon)
set(ABADDON_RESOURCE_DIR "/usr/share/abaddon" CACHE PATH "Fallback directory for resources on Linux")
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${CMAKE_SOURCE_DIR}/cmake/")
set(USE_TLS TRUE)
set(USE_OPEN_SSL TRUE)
find_package(nlohmann_json REQUIRED)
find_package(CURL)
find_package(ZLIB REQUIRED)
find_package(SQLite3 REQUIRED)
find_package(gtkmm REQUIRED)
set(USE_TLS TRUE)
set(USE_OPEN_SSL TRUE)
find_package(IXWebSocket QUIET)
if (NOT IXWebSocket_FOUND)
message("ixwebsocket was not found and will be included as a submodule")
add_subdirectory(subprojects/ixwebsocket)
find_path(IXWEBSOCKET_INCLUDE_DIRS ixwebsocket/IXWebSocket.h)
find_library(IXWEBSOCKET_LIBRARY ixwebsocket)
if (NOT IXWEBSOCKET_LIBRARY)
add_subdirectory(thirdparty/IXWebSocket)
include_directories(IXWEBSOCKET_INCLUDE_DIRS)
endif()
include_directories(thirdparty/simpleini)
if(MINGW OR WIN32)
link_libraries(ws2_32)
endif()
@@ -29,22 +30,27 @@ endif()
if(WIN32)
add_compile_definitions(_CRT_SECURE_NO_WARNINGS)
add_compile_definitions(NOMINMAX)
find_package(Fontconfig REQUIRED)
link_libraries(${Fontconfig_LIBRARIES})
endif()
configure_file(${PROJECT_SOURCE_DIR}/src/config.h.in ${PROJECT_BINARY_DIR}/config.h)
file(GLOB_RECURSE ABADDON_SOURCES
"src/*.h"
"src/*.hpp"
"src/*.cpp"
file(GLOB ABADDON_SOURCES
"*.h"
"*.hpp"
"*.cpp"
"discord/*.hpp"
"discord/*.cpp"
"components/*.hpp"
"components/*.cpp"
"windows/*.hpp"
"windows/*.cpp"
"windows/guildsettings/*.hpp"
"windows/guildsettings/*.cpp"
"windows/profile/*.hpp"
"windows/profile/*.cpp"
"dialogs/*.hpp"
"dialogs/*.cpp"
)
add_executable(abaddon ${ABADDON_SOURCES})
target_include_directories(abaddon PUBLIC ${PROJECT_SOURCE_DIR}/src)
target_include_directories(abaddon PUBLIC ${PROJECT_BINARY_DIR})
target_include_directories(abaddon PUBLIC ${GTKMM_INCLUDE_DIRS})
target_include_directories(abaddon PUBLIC ${ZLIB_INCLUDE_DIRS})
target_include_directories(abaddon PUBLIC ${SQLite3_INCLUDE_DIRS})
@@ -56,8 +62,8 @@ if ((CMAKE_CXX_COMPILER_ID STREQUAL "GNU") OR
target_link_libraries(abaddon stdc++fs)
endif()
if (IXWebSocket_LIBRARIES)
target_link_libraries(abaddon ${IXWebSocket_LIBRARIES})
if (IXWEBSOCKET_LIBRARY)
target_link_libraries(abaddon ${IXWEBSOCKET_LIBRARY})
find_library(MBEDTLS_X509_LIBRARY mbedx509)
find_library(MBEDTLS_TLS_LIBRARY mbedtls)
find_library(MBEDTLS_CRYPTO_LIBRARY mbedcrypto)

180
README.md
View File

@@ -4,16 +4,12 @@ Alternative Discord client made in C++ with GTK
<img src="/.readme/s3.png">
<a href="https://discord.gg/wkCU3vuzG5"><img src="https://discord.com/api/guilds/858156817711890443/widget.png?style=shield"></a>
Current features:
* Not Electron
* Handles most types of chat messages including embeds, images, and replies
* Completely styleable/customizable with CSS (if you have a system GTK theme it won't really use it though)
* Identifies to Discord as the web client unlike other clients so less likely to be falsely flagged as spam<sup>1</sup>
* Identifies to gateway as the web client unlike other clients so less likely to be falsely flagged as spam<sup>1</sup>
* Set status
* Unread and mention indicators
* Start new DMs and group DMs
* View user profiles (notes, mutual servers, mutual friends)
* Kick, ban, and unban members
@@ -22,103 +18,73 @@ Current features:
* Manage emojis
* View audit log
* Emojis<sup>2</sup>
* Thread support<sup>3</sup>
* Animated avatars, server icons, emojis (can be turned off)
1 - Abaddon tries its best to make Discord think it's a legitimate web client. Some of the things done to do this
include: using a browser user agent, sending the same IDENTIFY message that the official web client does, using API v9
endpoints in all cases, and not using endpoints the web client does not normally use. There are still a few smaller
inconsistencies, however. For example the web client sends lots of telemetry via the `/science` endpoint (uBlock origin
stops this) as well as in the headers of all requests. **In any case,** you should use an official client for joining
servers, sending new DMs, or managing your friends list if you are afraid of being caught in Discord's spam filters
(unlikely).
2 - Unicode emojis are substituted manually as opposed to rendered by GTK on non-Windows platforms. This can be changed
with the `stock_emojis` setting as shown at the bottom of this README. A CBDT-based font using Twemoji is provided to
allow GTK to render emojis natively on Windows.
3 - There are some inconsistencies with thread state that might be encountered in some more uncommon cases, but they are
the result of fundamental issues with Discord's thread implementation.
1 - Other third-party clients send the IDENTIFY message that bots use which makes Discord more likely to think you are selfbotting or spamming. However, Discord still loves to ban people's accounts for no good reason, even users of the official clients. If you want to be really careful avoid joining servers really fast or cold DMing people.
2 - Getting emojis to function properly on GTK is still something I've yet to figure out ([#5](../../issues/5)). Unicode emojis are manually searched for and replaced in several places as opposed to allowing GTK to figure it out since GTK's way of doing it doesn't work very well.
### Building manually (recommended if not on Windows):
#### Windows (with MSYS2):
1. Install following packages:
* mingw-w64-x86_64-cmake
* mingw-w64-x86_64-ninja
* mingw-w64-x86_64-sqlite3
* mingw-w64-x86_64-nlohmann-json
* mingw-w64-x86_64-curl
* mingw-w64-x86_64-zlib
* mingw-w64-x86_64-gtkmm3
2. `git clone --recurse-submodules="subprojects" https://github.com/uowuo/abaddon && cd abaddon`
#### Windows:
1. `git clone https://github.com/uowuo/abaddon && cd abaddon`
2. `vcpkg install gtkmm:x64-windows nlohmann-json:x64-windows ixwebsocket:x64-windows zlib:x64-windows simpleini:x64-windows sqlite3:x64-windows openssl:x64-windows curl:x64-windows`
3. `mkdir build && cd build`
4. `cmake -GNinja -DCMAKE_BUILD_TYPE=RelWithDebInfo ..`
5. `ninja`
4. `cmake -G"Visual Studio 16 2019" -A x64 -DCMAKE_TOOLCHAIN_FILE=c:\path\to\vcpkg\scripts\buildsystems\vcpkg.cmake -DCMAKE_BUILD_TYPE=RelWithDebInfo -DVCPKG_TARGET_TRIPLET=x64-windows ..`
5. Build with Visual Studio
Or, do steps 1 and 2, and open CMakeLists.txt in Visual Studio if `vcpkg integrate install` was run
#### Mac:
1. `git clone https://github.com/uowuo/abaddon && cd abaddon`
2. `brew install gtkmm3 nlohmann-json`
3. `git submodule update --init subprojects`
4. `mkdir build && cd build`
5. `cmake ..`
6. `make`
3. `mkdir build && cd build`
4. `cmake ..`
5. `make`
#### Linux:
1. Install dependencies: `libgtkmm-3.0-dev`, `libcurl4-gnutls-dev`,
and [nlohmann-json](https://github.com/nlohmann/json)
1. Install dependencies: `libgtkmm-3.0-dev`, `libcurl4-gnutls-dev`, and [nlohmann-json](https://github.com/nlohmann/json)
2. `git clone https://github.com/uowuo/abaddon && cd abaddon`
3. `mkdir build && cd build`
4. `cmake ..`
5. `make`
### Downloads:
Latest release version: https://github.com/uowuo/abaddon/releases/latest
**CI:**
### Downloads (from CI):
- Windows: [here](https://nightly.link/uowuo/abaddon/workflows/ci/master/build-windows-RelWithDebInfo.zip)
- MacOS: [here](https://nightly.link/uowuo/abaddon/workflows/ci/master/build-macos-RelWithDebInfo.zip) unsigned,
unpackaged, requires gtkmm3 (e.g. from homebrew)
- 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
- MacOS: [here](https://nightly.link/uowuo/abaddon/workflows/ci/master/build-macos-RelWithDebInfo.zip) unsigned, unpackaged, requires gtkmm3 (e.g. from homebrew)
- Linux: [here](https://nightly.link/uowuo/abaddon/workflows/ci/master/build-linux-MinSizeRel.zip) unpackaged (for now), requires gtkmm3. built on Ubuntu 18.04 + gcc9
⚠️ If you use Windows, make sure to start from the `bin` directory
On Linux, `css` and `res` can also be loaded from `~/.local/share/abaddon` or `/usr/share/abaddon`
`abaddon.ini` will also be automatically used if located at `~/.config/abaddon/abaddon.ini` and there is
no `abaddon.ini` in the working directory
#### Dependencies:
⚠️ Make sure you start from the directory where `css` and `res` are or else stuff will be broken
#### Dependencies:
* [gtkmm](https://www.gtkmm.org/en/)
* [JSON for Modern C++](https://github.com/nlohmann/json)
* [IXWebSocket](https://github.com/machinezone/IXWebSocket)
* [libcurl](https://curl.se/)
* [zlib](https://zlib.net/)
* [simpleini](https://github.com/brofield/simpleini)
* [SQLite3](https://www.sqlite.org/index.html)
### TODO:
* Voice support
* Unread indicators
* User activities
* Nicknames
* More server management stuff
* Manage friends
* A bunch of other stuff
### Styling
#### CSS selectors
.app-window - Applied to all windows. This means the main window and all popups
.app-popup - Additional class for `.app-window`s when the window is not the main window
.channel-list - Container of the channel list
.app-popup - Additional class for `.app-window`s when the window is not the main window
.channel-list - Container of the channel list
.channel-row - All rows within the channel container
.channel-row-channel - Only rows containing a channel
.channel-row-category - Only rows containing a category
.channel-row-guild - Only rows containing a guild
.channel-row-label - All labels within the channel container
.nsfw - Applied to channel row labels and their container for NSFW channels
.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
@@ -134,45 +100,47 @@ no `abaddon.ini` in the working directory
.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
.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
.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
.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
.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)
.offline - Applied to status indicators when the associated user is offline
.typing-indicator - The typing indicator (also used for replies)
Used in reorderable list implementation:
.drag-icon .drag-hover-top .drag-hover-bottom
.drag-icon
.drag-hover-top
.drag-hover-bottom
Used in guild settings popup:
.guild-settings-window
.guild-members-pane-list - Container for list of members in the members pane
.guild-members-pane-info - Container for member info
.guild-roles-pane-list - Container for list of roles in the roles pane
.guild-roles-pane-list - Container for list of roles in the roles pane
Used in profile popup:
.mutual-friend-item - Applied to every item in the mutual friends list
.mutual-friend-item-name - Name in mutual friend item
@@ -197,59 +165,29 @@ Used in profile popup:
.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
.profile-badge
### Settings
Settings are configured (for now) by editing abaddon.ini
The format is similar to the standard Windows ini format **except**:
* `#` is used to begin comments as opposed to `;`
* Section and key names are case-sensitive
You should edit these while the client is closed even though there's an option to reload while running
This listing is organized by section.
For example, memory_db would be set by adding `memory_db = true` under the line `[discord]`
#### discord
* gateway (string) - override url for Discord gateway. must be json format and use zlib stream compression
* api_base (string) - override base url for Discord API
* memory_db (true or false, default false) - if true, Discord data will be kept in memory as opposed to on disk
* token (string) - Discord token used to login, this can be set from the menu
* prefetch (true or false, default false) - if true, new messages will cause the avatar and image attachments to be
automatically downloaded
* prefetch (true or false, default false) - if true, new messages will cause the avatar and image attachments to be automatically downloaded
#### http
* user_agent (string) - sets the user-agent to use in HTTP requests to the Discord API (not including media/images)
* concurrent (int, default 20) - how many images can be concurrently retrieved
* concurrent (int, default 10) - how many images can be concurrently retrieved
#### gui
* member_list_discriminator (true or false, default true) - show user discriminators in the member list
* stock_emojis (true or false, default true) - allow abaddon to substitute unicode emojis with images from emojis.bin,
must be false to allow GTK to render emojis itself
* custom_emojis (true or false, default true) - download and use custom Discord emojis
* emojis (true or false, default true) - resolve unicode and custom emojis to images. this needs to be false to allow GTK to render emojis by itself
* css (string) - path to the main CSS file
* animations (true or false, default true) - use animated images where available (e.g. server icons, emojis, avatars).
false means static images will be used
* animated_guild_hover_only (true or false, default true) - only animate guild icons when the guild is being hovered
over
* animations (true or false, default true) - use animated images where available (e.g. server icons, emojis, avatars). false means static images will be used
* owner_crown (true or false, default true) - show a crown next to the owner
* unreads (true or false, default true) - show unread indicators and mention badges
#### style
#### misc
* linkcolor (string) - color to use for links in messages
* expandercolor (string) - color to use for the expander in the channel list
* nsfwchannelcolor (string) - color to use for NSFW channels in the channel list
* channelcolor (string) - color to use for SFW channels in the channel list
* mentionbadgecolor (string) - background color for mention badges
* mentionbadgetextcolor (string) - color to use for number displayed on mention badges
* unreadcolor (string) - color to use for the unread indicator
### Environment variables
* ABADDON_NO_FC (Windows only) - don't use custom font config
* ABADDON_CONFIG - change path of configuration file to use. relative to cwd or can be absolute

View File

@@ -2,7 +2,6 @@
#include <memory>
#include <string>
#include <algorithm>
#include "platform.hpp"
#include "discord/discord.hpp"
#include "dialogs/token.hpp"
#include "dialogs/editmessage.hpp"
@@ -14,21 +13,19 @@
#include "abaddon.hpp"
#include "windows/guildsettingswindow.hpp"
#include "windows/profilewindow.hpp"
#include "windows/pinnedwindow.hpp"
#include "windows/threadswindow.hpp"
#ifdef _WIN32
#pragma comment(lib, "crypt32.lib")
#endif
Abaddon::Abaddon()
: m_settings(Platform::FindConfigFile())
, m_discord(GetSettings().UseMemoryDB) // stupid but easy
, m_emojis(GetResPath("/emojis.bin")) {
: m_settings("abaddon.ini")
, m_emojis("res/emojis.bin")
, m_discord(m_settings.GetUseMemoryDB()) { // stupid but easy
LoadFromSettings();
// todo: set user agent for non-client(?)
std::string ua = GetSettings().UserAgent;
std::string ua = m_settings.GetUserAgent();
m_discord.SetUserAgent(ua);
m_discord.signal_gateway_ready().connect(sigc::mem_fun(*this, &Abaddon::DiscordOnReady));
@@ -36,14 +33,18 @@ Abaddon::Abaddon()
m_discord.signal_message_delete().connect(sigc::mem_fun(*this, &Abaddon::DiscordOnMessageDelete));
m_discord.signal_message_update().connect(sigc::mem_fun(*this, &Abaddon::DiscordOnMessageUpdate));
m_discord.signal_guild_member_list_update().connect(sigc::mem_fun(*this, &Abaddon::DiscordOnGuildMemberListUpdate));
m_discord.signal_thread_member_list_update().connect(sigc::mem_fun(*this, &Abaddon::DiscordOnThreadMemberListUpdate));
m_discord.signal_guild_create().connect(sigc::mem_fun(*this, &Abaddon::DiscordOnGuildCreate));
m_discord.signal_guild_delete().connect(sigc::mem_fun(*this, &Abaddon::DiscordOnGuildDelete));
m_discord.signal_channel_delete().connect(sigc::mem_fun(*this, &Abaddon::DiscordOnChannelDelete));
m_discord.signal_channel_update().connect(sigc::mem_fun(*this, &Abaddon::DiscordOnChannelUpdate));
m_discord.signal_channel_create().connect(sigc::mem_fun(*this, &Abaddon::DiscordOnChannelCreate));
m_discord.signal_guild_update().connect(sigc::mem_fun(*this, &Abaddon::DiscordOnGuildUpdate));
m_discord.signal_reaction_add().connect(sigc::mem_fun(*this, &Abaddon::DiscordOnReactionAdd));
m_discord.signal_reaction_remove().connect(sigc::mem_fun(*this, &Abaddon::DiscordOnReactionRemove));
m_discord.signal_guild_join_request_create().connect(sigc::mem_fun(*this, &Abaddon::DiscordOnGuildJoinRequestCreate));
m_discord.signal_thread_update().connect(sigc::mem_fun(*this, &Abaddon::DiscordOnThreadUpdate));
m_discord.signal_message_sent().connect(sigc::mem_fun(*this, &Abaddon::DiscordOnMessageSent));
m_discord.signal_disconnected().connect(sigc::mem_fun(*this, &Abaddon::DiscordOnDisconnect));
if (GetSettings().Prefetch)
if (m_settings.GetPrefetch())
m_discord.signal_message_create().connect([this](const Message &message) {
if (message.Author.HasAvatar())
m_img_mgr.Prefetch(message.Author.GetAvatarURL());
@@ -54,6 +55,11 @@ Abaddon::Abaddon()
});
}
Abaddon::~Abaddon() {
m_settings.Close();
m_discord.Stop();
}
Abaddon &Abaddon::Get() {
static Abaddon instance;
return instance;
@@ -62,49 +68,54 @@ Abaddon &Abaddon::Get() {
int Abaddon::StartGTK() {
m_gtk_app = Gtk::Application::create("com.github.uowuo.abaddon");
// tmp css stuff
m_css_provider = Gtk::CssProvider::create();
m_css_provider->signal_parsing_error().connect([](const Glib::RefPtr<const Gtk::CssSection> &section, const Glib::Error &error) {
Gtk::MessageDialog dlg("css failed parsing (" + error.what() + ")", false, Gtk::MESSAGE_ERROR, Gtk::BUTTONS_OK, true);
m_css_provider->signal_parsing_error().connect([this](const Glib::RefPtr<const Gtk::CssSection> &section, const Glib::Error &error) {
Gtk::MessageDialog dlg(*m_main_window, "css failed parsing (" + error.what() + ")", false, Gtk::MESSAGE_ERROR, Gtk::BUTTONS_OK, true);
dlg.set_position(Gtk::WIN_POS_CENTER);
dlg.run();
});
m_css_low_provider = Gtk::CssProvider::create();
m_css_low_provider->signal_parsing_error().connect([](const Glib::RefPtr<const Gtk::CssSection> &section, const Glib::Error &error) {
Gtk::MessageDialog dlg("low-priority css failed parsing (" + error.what() + ")", false, Gtk::MESSAGE_ERROR, Gtk::BUTTONS_OK, true);
dlg.set_position(Gtk::WIN_POS_CENTER);
dlg.run();
});
#ifdef _WIN32
bool png_found = false;
bool gif_found = false;
for (const auto &fmt : Gdk::Pixbuf::get_formats()) {
if (fmt.get_name() == "png")
png_found = true;
else if (fmt.get_name() == "gif")
gif_found = true;
}
if (!png_found) {
Gtk::MessageDialog dlg("The PNG pixbufloader wasn't detected. Abaddon may not work as a result.", false, Gtk::MESSAGE_ERROR, Gtk::BUTTONS_OK, true);
dlg.set_position(Gtk::WIN_POS_CENTER);
dlg.run();
}
if (!gif_found) {
Gtk::MessageDialog dlg("The GIF pixbufloader wasn't detected. Animations may not display as a result.", false, Gtk::MESSAGE_ERROR, Gtk::BUTTONS_OK, true);
dlg.set_position(Gtk::WIN_POS_CENTER);
dlg.run();
}
#endif
m_main_window = std::make_unique<MainWindow>();
m_main_window->set_title(APP_TITLE);
m_main_window->UpdateComponents();
m_main_window->set_position(Gtk::WIN_POS_CENTER);
// crashes for some stupid reason if i put it somewhere else
SetupUserMenu();
m_main_window->signal_action_connect().connect(sigc::mem_fun(*this, &Abaddon::ActionConnect));
m_main_window->signal_action_disconnect().connect(sigc::mem_fun(*this, &Abaddon::ActionDisconnect));
m_main_window->signal_action_set_token().connect(sigc::mem_fun(*this, &Abaddon::ActionSetToken));
m_main_window->signal_action_reload_css().connect(sigc::mem_fun(*this, &Abaddon::ActionReloadCSS));
m_main_window->signal_action_join_guild().connect(sigc::mem_fun(*this, &Abaddon::ActionJoinGuildDialog));
m_main_window->signal_action_set_status().connect(sigc::mem_fun(*this, &Abaddon::ActionSetStatus));
m_main_window->signal_action_reload_settings().connect(sigc::mem_fun(*this, &Abaddon::ActionReloadSettings));
m_main_window->signal_action_add_recipient().connect(sigc::mem_fun(*this, &Abaddon::ActionAddRecipient));
m_main_window->signal_action_show_user_menu().connect(sigc::mem_fun(*this, &Abaddon::ShowUserMenu));
m_main_window->GetChannelList()->signal_action_channel_item_select().connect(sigc::mem_fun(*this, &Abaddon::ActionChannelOpened));
m_main_window->GetChannelList()->signal_action_guild_leave().connect(sigc::mem_fun(*this, &Abaddon::ActionLeaveGuild));
m_main_window->GetChannelList()->signal_action_guild_settings().connect(sigc::mem_fun(*this, &Abaddon::ActionGuildSettings));
m_main_window->GetChatWindow()->signal_action_message_delete().connect(sigc::mem_fun(*this, &Abaddon::ActionChatDeleteMessage));
m_main_window->GetChatWindow()->signal_action_message_edit().connect(sigc::mem_fun(*this, &Abaddon::ActionChatEditMessage));
m_main_window->GetChatWindow()->signal_action_chat_submit().connect(sigc::mem_fun(*this, &Abaddon::ActionChatInputSubmit));
m_main_window->GetChatWindow()->signal_action_chat_load_history().connect(sigc::mem_fun(*this, &Abaddon::ActionChatLoadHistory));
m_main_window->GetChatWindow()->signal_action_channel_click().connect(sigc::mem_fun(*this, &Abaddon::ActionChannelOpened));
m_main_window->GetChatWindow()->signal_action_insert_mention().connect(sigc::mem_fun(*this, &Abaddon::ActionInsertMention));
m_main_window->GetChatWindow()->signal_action_reaction_add().connect(sigc::mem_fun(*this, &Abaddon::ActionReactionAdd));
m_main_window->GetChatWindow()->signal_action_reaction_remove().connect(sigc::mem_fun(*this, &Abaddon::ActionReactionRemove));
ActionReloadCSS();
m_gtk_app->signal_shutdown().connect([&]() {
StopDiscord();
});
if (!m_settings.IsValid()) {
Gtk::MessageDialog dlg(*m_main_window, "The settings file could not be opened!", false, Gtk::MESSAGE_ERROR, Gtk::BUTTONS_OK, true);
Gtk::MessageDialog dlg(*m_main_window, "The settings file could not be created!", false, Gtk::MESSAGE_ERROR, Gtk::BUTTONS_OK, true);
dlg.set_position(Gtk::WIN_POS_CENTER);
dlg.run();
}
@@ -122,50 +133,13 @@ int Abaddon::StartGTK() {
return 1;
}
// store must be checked before this can be called
m_main_window->UpdateComponents();
// crashes for some stupid reason if i put it somewhere else
SetupUserMenu();
m_main_window->signal_action_connect().connect(sigc::mem_fun(*this, &Abaddon::ActionConnect));
m_main_window->signal_action_disconnect().connect(sigc::mem_fun(*this, &Abaddon::ActionDisconnect));
m_main_window->signal_action_set_token().connect(sigc::mem_fun(*this, &Abaddon::ActionSetToken));
m_main_window->signal_action_reload_css().connect(sigc::mem_fun(*this, &Abaddon::ActionReloadCSS));
m_main_window->signal_action_join_guild().connect(sigc::mem_fun(*this, &Abaddon::ActionJoinGuildDialog));
m_main_window->signal_action_set_status().connect(sigc::mem_fun(*this, &Abaddon::ActionSetStatus));
m_main_window->signal_action_add_recipient().connect(sigc::mem_fun(*this, &Abaddon::ActionAddRecipient));
m_main_window->signal_action_view_pins().connect(sigc::mem_fun(*this, &Abaddon::ActionViewPins));
m_main_window->signal_action_view_threads().connect(sigc::mem_fun(*this, &Abaddon::ActionViewThreads));
m_main_window->GetChannelList()->signal_action_channel_item_select().connect(sigc::mem_fun(*this, &Abaddon::ActionChannelOpened));
m_main_window->GetChannelList()->signal_action_guild_leave().connect(sigc::mem_fun(*this, &Abaddon::ActionLeaveGuild));
m_main_window->GetChannelList()->signal_action_guild_settings().connect(sigc::mem_fun(*this, &Abaddon::ActionGuildSettings));
m_main_window->GetChatWindow()->signal_action_message_edit().connect(sigc::mem_fun(*this, &Abaddon::ActionChatEditMessage));
m_main_window->GetChatWindow()->signal_action_chat_submit().connect(sigc::mem_fun(*this, &Abaddon::ActionChatInputSubmit));
m_main_window->GetChatWindow()->signal_action_chat_load_history().connect(sigc::mem_fun(*this, &Abaddon::ActionChatLoadHistory));
m_main_window->GetChatWindow()->signal_action_channel_click().connect(sigc::mem_fun(*this, &Abaddon::ActionChannelOpened));
m_main_window->GetChatWindow()->signal_action_insert_mention().connect(sigc::mem_fun(*this, &Abaddon::ActionInsertMention));
m_main_window->GetChatWindow()->signal_action_reaction_add().connect(sigc::mem_fun(*this, &Abaddon::ActionReactionAdd));
m_main_window->GetChatWindow()->signal_action_reaction_remove().connect(sigc::mem_fun(*this, &Abaddon::ActionReactionRemove));
ActionReloadCSS();
m_gtk_app->signal_shutdown().connect(sigc::mem_fun(*this, &Abaddon::OnShutdown), false);
m_main_window->show();
return m_gtk_app->run(*m_main_window);
}
void Abaddon::OnShutdown() {
StopDiscord();
m_settings.Close();
}
void Abaddon::LoadFromSettings() {
std::string token = GetSettings().DiscordToken;
if (!token.empty()) {
std::string token = m_settings.GetDiscordToken();
if (token.size()) {
m_discord_token = token;
m_discord.UpdateToken(m_discord_token);
}
@@ -177,7 +151,6 @@ void Abaddon::StartDiscord() {
void Abaddon::StopDiscord() {
m_discord.Stop();
SaveState();
}
bool Abaddon::IsDiscordActive() const {
@@ -200,11 +173,10 @@ const DiscordClient &Abaddon::GetDiscordClient() const {
void Abaddon::DiscordOnReady() {
m_main_window->UpdateComponents();
LoadState();
}
void Abaddon::DiscordOnMessageCreate(const Message &message) {
m_main_window->UpdateChatNewMessage(message);
m_main_window->UpdateChatNewMessage(message.ID); // todo ill fix you later :^)
}
void Abaddon::DiscordOnMessageDelete(Snowflake id, Snowflake channel_id) {
@@ -219,8 +191,28 @@ void Abaddon::DiscordOnGuildMemberListUpdate(Snowflake guild_id) {
m_main_window->UpdateMembers();
}
void Abaddon::DiscordOnThreadMemberListUpdate(const ThreadMemberListUpdateData &data) {
m_main_window->UpdateMembers();
void Abaddon::DiscordOnGuildCreate(const GuildData &guild) {
m_main_window->UpdateChannelsNewGuild(guild.ID);
}
void Abaddon::DiscordOnGuildDelete(Snowflake guild_id) {
m_main_window->UpdateChannelsRemoveGuild(guild_id);
}
void Abaddon::DiscordOnChannelDelete(Snowflake channel_id) {
m_main_window->UpdateChannelsRemoveChannel(channel_id);
}
void Abaddon::DiscordOnChannelUpdate(Snowflake channel_id) {
m_main_window->UpdateChannelsUpdateChannel(channel_id);
}
void Abaddon::DiscordOnChannelCreate(Snowflake channel_id) {
m_main_window->UpdateChannelsCreateChannel(channel_id);
}
void Abaddon::DiscordOnGuildUpdate(Snowflake guild_id) {
m_main_window->UpdateChannelsUpdateGuild(guild_id);
}
void Abaddon::DiscordOnReactionAdd(Snowflake message_id, const Glib::ustring &param) {
@@ -239,7 +231,7 @@ void Abaddon::DiscordOnGuildJoinRequestCreate(const GuildJoinRequestCreateData &
}
void Abaddon::DiscordOnMessageSent(const Message &data) {
m_main_window->UpdateChatNewMessage(data);
m_main_window->UpdateChatNewMessage(data.ID);
}
void Abaddon::DiscordOnDisconnect(bool is_reconnecting, GatewayCloseCode close_code) {
@@ -263,17 +255,8 @@ void Abaddon::DiscordOnDisconnect(bool is_reconnecting, GatewayCloseCode close_c
}
}
void Abaddon::DiscordOnThreadUpdate(const ThreadUpdateData &data) {
if (data.Thread.ID == m_main_window->GetChatActiveChannel()) {
if (data.Thread.ThreadMetadata->IsArchived)
m_main_window->GetChatWindow()->SetTopic("This thread is archived. Sending a message will unarchive it");
else
m_main_window->GetChatWindow()->SetTopic("");
}
}
SettingsManager::Settings &Abaddon::GetSettings() {
return m_settings.GetSettings();
const SettingsManager &Abaddon::GetSettings() const {
return m_settings;
}
Glib::RefPtr<Gtk::CssProvider> Abaddon::GetStyleProvider() {
@@ -292,8 +275,8 @@ void Abaddon::ShowUserMenu(const GdkEvent *event, Snowflake id, Snowflake guild_
delete child;
if (guild.has_value() && user.has_value()) {
const auto roles = user->GetSortedRoles();
m_user_menu_roles->set_visible(!roles.empty());
for (const auto &role : roles) {
m_user_menu_roles->set_visible(roles.size() > 0);
for (const auto role : roles) {
auto *item = Gtk::manage(new Gtk::MenuItem(role.Name));
if (role.Color != 0) {
Gdk::RGBA color;
@@ -337,8 +320,8 @@ void Abaddon::ShowUserMenu(const GdkEvent *event, Snowflake id, Snowflake guild_
void Abaddon::ShowGuildVerificationGateDialog(Snowflake guild_id) {
VerificationGateDialog dlg(*m_main_window, guild_id);
if (dlg.run() == Gtk::RESPONSE_OK) {
const auto cb = [this](DiscordError code) {
if (code != DiscordError::NONE) {
const auto cb = [this](bool success) {
if (!success) {
Gtk::MessageDialog dlg(*m_main_window, "Failed to accept the verification gate.", false, Gtk::MESSAGE_ERROR, Gtk::BUTTONS_OK, true);
dlg.set_position(Gtk::WIN_POS_CENTER);
dlg.run();
@@ -348,20 +331,6 @@ void Abaddon::ShowGuildVerificationGateDialog(Snowflake guild_id) {
}
}
void Abaddon::CheckMessagesForMembers(const ChannelData &chan, const std::vector<Message> &msgs) {
if (!chan.GuildID.has_value()) return;
std::vector<Snowflake> unknown;
std::transform(msgs.begin(), msgs.end(),
std::back_inserter(unknown),
[](const Message &msg) -> Snowflake {
return msg.Author.ID;
});
const auto fetch = m_discord.FilterUnknownMembersFrom(*chan.GuildID, unknown.begin(), unknown.end());
m_discord.RequestMembers(*chan.GuildID, fetch.begin(), fetch.end());
}
void Abaddon::SetupUserMenu() {
m_user_menu = Gtk::manage(new Gtk::Menu);
m_user_menu_insert_mention = Gtk::manage(new Gtk::MenuItem("Insert Mention"));
@@ -404,44 +373,6 @@ void Abaddon::SetupUserMenu() {
m_user_menu->show_all();
}
void Abaddon::SaveState() {
if (!GetSettings().SaveState) return;
AbaddonApplicationState state;
state.ActiveChannel = m_main_window->GetChatActiveChannel();
state.Expansion = m_main_window->GetChannelList()->GetExpansionState();
const auto path = GetStateCachePath();
if (!util::IsFolder(path)) {
std::error_code ec;
std::filesystem::create_directories(path, ec);
}
auto *fp = std::fopen(GetStateCachePath("/state.json").c_str(), "wb");
if (fp == nullptr) return;
const auto s = nlohmann::json(state).dump(4);
std::fwrite(s.c_str(), 1, s.size(), fp);
std::fclose(fp);
}
void Abaddon::LoadState() {
if (!GetSettings().SaveState) {
// call with empty data to purge the temporary table
m_main_window->GetChannelList()->UseExpansionState({});
return;
}
const auto data = ReadWholeFile(GetStateCachePath("/state.json"));
if (data.empty()) return;
try {
AbaddonApplicationState state = nlohmann::json::parse(data.begin(), data.end());
m_main_window->GetChannelList()->UseExpansionState(state.Expansion);
ActionChannelOpened(state.ActiveChannel);
} catch (const std::exception &e) {
printf("failed to load application state: %s\n", e.what());
}
}
void Abaddon::ManageHeapWindow(Gtk::Window *window) {
window->signal_hide().connect([this, window]() {
delete window;
@@ -474,8 +405,8 @@ void Abaddon::on_user_menu_open_dm() {
if (existing.has_value())
ActionChannelOpened(*existing);
else
m_discord.CreateDM(m_shown_user_menu_id, [this](DiscordError code, Snowflake channel_id) {
if (code == DiscordError::NONE) {
m_discord.CreateDM(m_shown_user_menu_id, [this](bool success, Snowflake channel_id) {
if (success) {
// give the gateway a little window to send CHANNEL_CREATE
auto cb = [this, channel_id] {
ActionChannelOpened(channel_id);
@@ -489,33 +420,6 @@ void Abaddon::on_user_menu_remove_recipient() {
m_discord.RemoveGroupDMRecipient(m_main_window->GetChatActiveChannel(), m_shown_user_menu_id);
}
std::string Abaddon::GetCSSPath() {
const static auto path = Platform::FindResourceFolder() + "/css";
return path;
}
std::string Abaddon::GetResPath() {
const static auto path = Platform::FindResourceFolder() + "/res";
return path;
}
std::string Abaddon::GetStateCachePath() {
const static auto path = Platform::FindStateCacheFolder() + "/state";
return path;
}
std::string Abaddon::GetCSSPath(const std::string &path) {
return GetCSSPath() + path;
}
std::string Abaddon::GetResPath(const std::string &path) {
return GetResPath() + path;
}
std::string Abaddon::GetStateCachePath(const std::string &path) {
return GetStateCachePath() + path;
}
void Abaddon::ActionConnect() {
if (!m_discord.IsStarted())
StartDiscord();
@@ -523,7 +427,8 @@ void Abaddon::ActionConnect() {
}
void Abaddon::ActionDisconnect() {
StopDiscord();
if (m_discord.IsStarted())
StopDiscord();
}
void Abaddon::ActionSetToken() {
@@ -533,7 +438,7 @@ void Abaddon::ActionSetToken() {
m_discord_token = dlg.GetToken();
m_discord.UpdateToken(m_discord_token);
m_main_window->UpdateComponents();
GetSettings().DiscordToken = m_discord_token;
m_settings.SetSetting("discord", "token", m_discord_token);
}
}
@@ -547,15 +452,9 @@ void Abaddon::ActionJoinGuildDialog() {
}
void Abaddon::ActionChannelOpened(Snowflake id) {
if (!id.IsValid() || id == m_main_window->GetChatActiveChannel()) return;
m_main_window->GetChatWindow()->SetTopic("");
if (id == m_main_window->GetChatActiveChannel()) return;
const auto channel = m_discord.GetChannel(id);
if (!channel.has_value()) return;
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->Name);
else {
@@ -571,29 +470,20 @@ void Abaddon::ActionChannelOpened(Snowflake id) {
}
m_main_window->UpdateChatActiveChannel(id);
if (m_channels_requested.find(id) == m_channels_requested.end()) {
// dont fire requests we know will fail
if (can_access) {
m_discord.FetchMessagesInChannel(id, [channel, this, id](const std::vector<Message> &msgs) {
CheckMessagesForMembers(*channel, msgs);
m_main_window->UpdateChatWindowContents();
m_channels_requested.insert(id);
});
}
m_discord.FetchMessagesInChannel(id, [this, id](const std::vector<Snowflake> &msgs) {
m_main_window->UpdateChatWindowContents();
m_channels_requested.insert(id);
});
} else {
m_main_window->UpdateChatWindowContents();
}
if (can_access) {
if (channel->IsThread()) {
m_discord.SendThreadLazyLoad(id);
if (channel->ThreadMetadata->IsArchived)
m_main_window->GetChatWindow()->SetTopic("This thread is archived. Sending a message will unarchive it");
} else if (channel->Type != ChannelType::DM && channel->Type != ChannelType::GROUP_DM && channel->GuildID.has_value()) {
m_discord.SendLazyLoad(id);
if (channel->Type != ChannelType::DM && channel->Type != ChannelType::GROUP_DM) {
m_discord.SendLazyLoad(id);
if (m_discord.IsVerificationRequired(*channel->GuildID))
ShowGuildVerificationGateDialog(*channel->GuildID);
}
const auto request = m_discord.GetGuildApplication(*channel->GuildID);
if (request.has_value() && request->ApplicationStatus == GuildApplicationStatus::STARTED)
ShowGuildVerificationGateDialog(*channel->GuildID);
}
}
@@ -605,23 +495,23 @@ void Abaddon::ActionChatLoadHistory(Snowflake id) {
return;
Snowflake before_id = m_main_window->GetChatOldestListedMessage();
auto msgs = m_discord.GetMessagesBefore(id, before_id);
auto knownset = m_discord.GetMessagesForChannel(id);
std::vector<Snowflake> knownvec(knownset.begin(), knownset.end());
std::sort(knownvec.begin(), knownvec.end());
auto latest = std::find_if(knownvec.begin(), knownvec.end(), [&before_id](Snowflake x) -> bool { return x == before_id; });
int distance = std::distance(knownvec.begin(), latest);
if (msgs.size() >= 50) {
m_main_window->UpdateChatPrependHistory(msgs);
if (distance >= 50) {
m_main_window->UpdateChatPrependHistory(std::vector<Snowflake>(knownvec.begin() + distance - 50, knownvec.begin() + distance));
return;
}
m_channels_history_loading.insert(id);
m_discord.FetchMessagesInChannelBefore(id, before_id, [this, id](const std::vector<Message> &msgs) {
m_discord.FetchMessagesInChannelBefore(id, before_id, [this, id](const std::vector<Snowflake> &msgs) {
m_channels_history_loading.erase(id);
const auto channel = m_discord.GetChannel(id);
if (channel.has_value())
CheckMessagesForMembers(*channel, msgs);
if (msgs.empty()) {
if (msgs.size() == 0) {
m_channels_history_loaded.insert(id);
} else {
m_main_window->UpdateChatPrependHistory(msgs);
@@ -638,9 +528,12 @@ void Abaddon::ActionChatInputSubmit(std::string msg, Snowflake channel, Snowflak
m_discord.SendChatMessage(msg, channel);
}
void Abaddon::ActionChatDeleteMessage(Snowflake channel_id, Snowflake id) {
m_discord.DeleteMessage(channel_id, id);
}
void Abaddon::ActionChatEditMessage(Snowflake channel_id, Snowflake id) {
const auto msg = m_discord.GetMessage(id);
if (!msg.has_value()) return;
EditMessageDialog dlg(*m_main_window);
dlg.SetContent(msg->Content);
auto response = dlg.run();
@@ -691,7 +584,7 @@ void Abaddon::ActionSetStatus() {
const auto status = dlg.GetStatusType();
const auto activity_type = dlg.GetActivityType();
const auto activity_name = dlg.GetActivityName();
if (activity_name.empty()) {
if (activity_name == "") {
m_discord.UpdateStatus(status, false);
} else {
ActivityData activity;
@@ -724,41 +617,21 @@ void Abaddon::ActionAddRecipient(Snowflake channel_id) {
}
}
void Abaddon::ActionViewPins(Snowflake channel_id) {
const auto data = m_discord.GetChannel(channel_id);
if (!data.has_value()) return;
auto window = new PinnedWindow(*data);
ManageHeapWindow(window);
window->show();
}
void Abaddon::ActionViewThreads(Snowflake channel_id) {
auto data = m_discord.GetChannel(channel_id);
if (!data.has_value()) return;
if (data->IsThread()) {
data = m_discord.GetChannel(*data->ParentID);
if (!data.has_value()) return;
}
auto window = new ThreadsWindow(*data);
ManageHeapWindow(window);
window->show();
}
bool Abaddon::ShowConfirm(const Glib::ustring &prompt, Gtk::Window *window) {
ConfirmDialog dlg(window != nullptr ? *window : *m_main_window);
dlg.SetConfirmText(prompt);
return dlg.run() == Gtk::RESPONSE_OK;
}
void Abaddon::ActionReloadSettings() {
m_settings.Reload();
}
void Abaddon::ActionReloadCSS() {
try {
Gtk::StyleContext::remove_provider_for_screen(Gdk::Screen::get_default(), m_css_provider);
m_css_provider->load_from_path(GetCSSPath("/" + GetSettings().MainCSS));
m_css_provider->load_from_path(m_settings.GetMainCSS());
Gtk::StyleContext::add_provider_for_screen(Gdk::Screen::get_default(), m_css_provider, GTK_STYLE_PROVIDER_PRIORITY_APPLICATION);
Gtk::StyleContext::remove_provider_for_screen(Gdk::Screen::get_default(), m_css_low_provider);
m_css_low_provider->load_from_path(GetCSSPath("/application-low-priority.css"));
Gtk::StyleContext::add_provider_for_screen(Gdk::Screen::get_default(), m_css_low_provider, GTK_STYLE_PROVIDER_PRIORITY_APPLICATION - 1);
} catch (Glib::Error &e) {
Gtk::MessageDialog dlg(*m_main_window, "css failed to load (" + e.what() + ")", false, Gtk::MESSAGE_ERROR, Gtk::BUTTONS_OK, true);
dlg.set_position(Gtk::WIN_POS_CENTER);
@@ -775,19 +648,6 @@ EmojiResource &Abaddon::GetEmojis() {
}
int main(int argc, char **argv) {
if (std::getenv("ABADDON_NO_FC") == nullptr)
Platform::SetupFonts();
char *systemLocale = std::setlocale(LC_ALL, "");
try {
std::locale::global(std::locale(systemLocale));
} catch (...) {
try {
std::locale::global(std::locale::classic());
std::setlocale(LC_ALL, systemLocale);
} catch (...) {}
}
#if defined(_WIN32) && defined(_MSC_VER)
TCHAR buf[2] { 0 };
GetEnvironmentVariableA("GTK_CSD", buf, sizeof(buf));

View File

@@ -14,18 +14,16 @@
class Abaddon {
private:
Abaddon();
public:
static Abaddon &Get();
~Abaddon();
Abaddon(const Abaddon &) = delete;
Abaddon &operator=(const Abaddon &) = delete;
Abaddon(Abaddon &&) = delete;
Abaddon &operator=(Abaddon &&) = delete;
int StartGTK();
void OnShutdown();
public:
static Abaddon &Get();
int StartGTK();
void StartDiscord();
void StopDiscord();
@@ -38,6 +36,7 @@ public:
void ActionChannelOpened(Snowflake id);
void ActionChatInputSubmit(std::string msg, Snowflake channel, Snowflake referenced_message);
void ActionChatLoadHistory(Snowflake id);
void ActionChatDeleteMessage(Snowflake channel_id, Snowflake id);
void ActionChatEditMessage(Snowflake channel_id, Snowflake id);
void ActionInsertMention(Snowflake id);
void ActionLeaveGuild(Snowflake id);
@@ -48,11 +47,10 @@ public:
void ActionReactionRemove(Snowflake id, const Glib::ustring &param);
void ActionGuildSettings(Snowflake id);
void ActionAddRecipient(Snowflake channel_id);
void ActionViewPins(Snowflake channel_id);
void ActionViewThreads(Snowflake channel_id);
bool ShowConfirm(const Glib::ustring &prompt, Gtk::Window *window = nullptr);
void ActionReloadSettings();
void ActionReloadCSS();
ImageManager &GetImageManager();
@@ -68,37 +66,30 @@ public:
void DiscordOnMessageDelete(Snowflake id, Snowflake channel_id);
void DiscordOnMessageUpdate(Snowflake id, Snowflake channel_id);
void DiscordOnGuildMemberListUpdate(Snowflake guild_id);
void DiscordOnThreadMemberListUpdate(const ThreadMemberListUpdateData &data);
void DiscordOnGuildCreate(const GuildData &guild);
void DiscordOnGuildDelete(Snowflake guild_id);
void DiscordOnChannelDelete(Snowflake channel_id);
void DiscordOnChannelUpdate(Snowflake channel_id);
void DiscordOnChannelCreate(Snowflake channel_id);
void DiscordOnGuildUpdate(Snowflake guild_id);
void DiscordOnReactionAdd(Snowflake message_id, const Glib::ustring &param);
void DiscordOnReactionRemove(Snowflake message_id, const Glib::ustring &param);
void DiscordOnGuildJoinRequestCreate(const GuildJoinRequestCreateData &data);
void DiscordOnMessageSent(const Message &data);
void DiscordOnDisconnect(bool is_reconnecting, GatewayCloseCode close_code);
void DiscordOnThreadUpdate(const ThreadUpdateData &data);
SettingsManager::Settings &GetSettings();
const SettingsManager &GetSettings() const;
Glib::RefPtr<Gtk::CssProvider> GetStyleProvider();
void ShowUserMenu(const GdkEvent *event, Snowflake id, Snowflake guild_id);
void ManageHeapWindow(Gtk::Window *window);
static std::string GetCSSPath();
static std::string GetResPath();
static std::string GetStateCachePath();
static std::string GetCSSPath(const std::string &path);
static std::string GetResPath(const std::string &path);
static std::string GetStateCachePath(const std::string &path);
protected:
void ShowGuildVerificationGateDialog(Snowflake guild_id);
void CheckMessagesForMembers(const ChannelData &chan, const std::vector<Message> &msgs);
void SetupUserMenu();
void SaveState();
void LoadState();
void ManageHeapWindow(Gtk::Window *window);
Snowflake m_shown_user_menu_id;
Snowflake m_shown_user_menu_guild_id;
@@ -137,6 +128,5 @@ private:
mutable std::mutex m_mutex;
Glib::RefPtr<Gtk::Application> m_gtk_app;
Glib::RefPtr<Gtk::CssProvider> m_css_provider;
Glib::RefPtr<Gtk::CssProvider> m_css_low_provider; // registered with a lower priority to allow better customization
std::unique_ptr<MainWindow> m_main_window; // wah wah cant create a gtkstylecontext fuck you
std::unique_ptr<MainWindow> m_main_window; // wah wah cant create a gtkstylecontext fuck you
};

View File

@@ -1,57 +0,0 @@
/bin/gdbus.exe
/bin/gspawn-win64-helper-console.exe
/bin/libatk-1.0-0.dll
/bin/libatkmm-1.6-1.dll
/bin/libbrotlicommon.dll
/bin/libbrotlidec.dll
/bin/libbz2-1.dll
/bin/libcairo-2.dll
/bin/libcairo-gobject-2.dll
/bin/libcairomm-1.0-1.dll
/bin/libcrypto-1_1-x64.dll
/bin/libcurl-4.dll
/bin/libdatrie-1.dll
/bin/libdeflate.dll
/bin/libepoxy-0.dll
/bin/libexpat-1.dll
/bin/libffi-7.dll
/bin/libfontconfig-1.dll
/bin/libfreetype-6.dll
/bin/libfribidi-0.dll
/bin/libgcc_s_seh-1.dll
/bin/libgdk-3-0.dll
/bin/libgdk_pixbuf-2.0-0.dll
/bin/libgdkmm-3.0-1.dll
/bin/libgio-2.0-0.dll
/bin/libgiomm-2.4-1.dll
/bin/libglib-2.0-0.dll
/bin/libglibmm-2.4-1.dll
/bin/libgmodule-2.0-0.dll
/bin/libgobject-2.0-0.dll
/bin/libgraphite2.dll
/bin/libgtk-3-0.dll
/bin/libgtkmm-3.0-1.dll
/bin/libharfbuzz-0.dll
/bin/libiconv-2.dll
/bin/libidn2-0.dll
/bin/libintl-8.dll
/bin/libnghttp2-14.dll
/bin/libpango-1.0-0.dll
/bin/libpangocairo-1.0-0.dll
/bin/libpangoft2-1.0-0.dll
/bin/libpangomm-1.4-1.dll
/bin/libpangowin32-1.0-0.dll
/bin/libpcre-1.dll
/bin/libpixman-1-0.dll
/bin/libpng16-16.dll
/bin/libpsl-5.dll
/bin/libsigc-2.0-0.dll
/bin/libsqlite3-0.dll
/bin/libssh2-1.dll
/bin/libssl-1_1-x64.dll
/bin/libstdc++-6.dll
/bin/libthai-0.dll
/bin/libunistring-2.dll
/bin/libwinpthread-1.dll
/bin/libzstd.dll
/bin/zlib1.dll

1
ci/vcpkg Submodule

Submodule ci/vcpkg added at 50ea8c0ab7

View File

@@ -31,6 +31,7 @@ set(HARFBUZZ_INCLUDE_DIRS ${HARFBUZZ_INCLUDE_DIR};${HARFBUZZ_CONFIG_INCLUDE_DIRS
include(FindPackageHandleStandardArgs)
find_package_handle_standard_args(HarfBuzz
FOUND_VAR HARFBUZZ_FOUND
REQUIRED_VARS
HARFBUZZ_LIBRARY
HARFBUZZ_INCLUDE_DIR

View File

@@ -1,30 +0,0 @@
set(IXWebSocket_LIBRARY_NAME ixwebsocket)
find_path(IXWebSocket_INCLUDE_DIR
NAMES ixwebsocket/IXWebSocket.h
HINTS /usr/include
/usr/local/include
/opt/local/include
PATH_SUFFIXES ${IXWebSocket_LIBRARY_NAME})
find_library(IXWebSocket_LIBRARY
NAMES ${IXWebSocket_LIBRARY_NAME}
PATH_SUFFIXES ${IXWebSocket_LIBRARY_NAME}
${IXWebSocket_LIBRARY_NAME}/include)
include(FindPackageHandleStandardArgs)
find_package_handle_standard_args(IXWebSocket
REQUIRED_VARS
IXWebSocket_LIBRARY
IXWebSocket_INCLUDE_DIR)
mark_as_advanced(IXWebSocket_LIBRARY IXWebSocket_INCLUDE_DIR)
if (IXWebSocket_FOUND)
find_package(OpenSSL QUIET)
set(IXWebSocket_INCLUDE_DIRS "${IXWebSocket_INCLUDE_DIR};${OPENSSL_INCLUDE_DIR}")
set(IXWebSocket_LIBRARIES "${IXWebSocket_LIBRARY};${OPENSSL_LIBRARIES}")
endif()

View File

@@ -31,6 +31,7 @@ set(ATK_INCLUDE_DIRS ${ATK_INCLUDE_DIR};${ATK_CONFIG_INCLUDE_DIRS})
include(FindPackageHandleStandardArgs)
find_package_handle_standard_args(atk
FOUND_VAR ATK_FOUND
REQUIRED_VARS
ATK_LIBRARY
ATK_INCLUDE_DIR

View File

@@ -42,6 +42,7 @@ set(ATKMM_INCLUDE_DIRS ${ATKMM_INCLUDE_DIR};${ATKMM_CONFIG_INCLUDE_DIR};${ATK_IN
include(FindPackageHandleStandardArgs)
find_package_handle_standard_args(atkmm
FOUND_VAR ATKMM_FOUND
REQUIRED_VARS
ATKMM_LIBRARY
ATKMM_INCLUDE_DIRS

View File

@@ -31,6 +31,7 @@ set(CAIRO_INCLUDE_DIRS ${CAIRO_INCLUDE_DIR};${CAIRO_CONFIG_INCLUDE_DIRS})
include(FindPackageHandleStandardArgs)
find_package_handle_standard_args(cairo
FOUND_VAR CAIRO_FOUND
REQUIRED_VARS
CAIRO_LIBRARY
CAIRO_INCLUDE_DIR

View File

@@ -45,6 +45,7 @@ set(CAIROMM_INCLUDE_DIRS ${CAIROMM_INCLUDE_DIR};${CAIROMM_CONFIG_INCLUDE_DIRS};$
include(FindPackageHandleStandardArgs)
find_package_handle_standard_args(cairomm
FOUND_VAR CAIROMM_FOUND
REQUIRED_VARS
CAIROMM_LIBRARY
CAIROMM_INCLUDE_DIR

View File

@@ -40,6 +40,7 @@ set(GDKMM_INCLUDE_DIRS ${GDKMM_INCLUDE_DIR};${GDKMM_CONFIG_INCLUDE_DIRS};${GDKMM
include(FindPackageHandleStandardArgs)
find_package_handle_standard_args(gdkmm
FOUND_VAR GDKMM_FOUND
REQUIRED_VARS
GDKMM_LIBRARY
GDKMM_INCLUDE_DIRS

View File

@@ -33,6 +33,7 @@ set(GDKPIXBUF_INCLUDE_DIRS ${GDKPIXBUF_INCLUDE_DIR};${GDKPIXBUF_CONFIG_INCLUDE_D
include(FindPackageHandleStandardArgs)
find_package_handle_standard_args(gdkpixbuf
FOUND_VAR GDKPIXBUF_FOUND
REQUIRED_VARS
GDKPIXBUF_LIBRARY
GDKPIXBUF_INCLUDE_DIR

View File

@@ -50,6 +50,7 @@ endif()
include(FindPackageHandleStandardArgs)
find_package_handle_standard_args(glib
FOUND_VAR GLIB_FOUND
REQUIRED_VARS
GLIB_LIBRARIES
GLIB_INCLUDE_DIRS

View File

@@ -60,6 +60,7 @@ set(GLIBMM_INCLUDE_DIRS ${GLIBMM_INCLUDE_DIR};${GLIBMM_CONFIG_INCLUDE_DIR};${GIO
include(FindPackageHandleStandardArgs)
find_package_handle_standard_args(glibmm
FOUND_VAR GLIBMM_FOUND
REQUIRED_VARS
GLIBMM_LIBRARY
GLIBMM_INCLUDE_DIR

View File

@@ -47,6 +47,7 @@ set(GTK_INCLUDE_DIRS ${GTK_INCLUDE_DIR};${GDK_CONFIG_INCLUDE_DIR};${GDKPIXBUF_IN
include(FindPackageHandleStandardArgs)
find_package_handle_standard_args(gtk
FOUND_VAR GTK_FOUND
REQUIRED_VARS
GTK_LIBRARY
GTK_INCLUDE_DIR

View File

@@ -51,6 +51,7 @@ set(GTKMM_INCLUDE_DIRS ${GTKMM_INCLUDE_DIR};${GTKMM_CONFIG_INCLUDE_DIR};${GDKMM
include(FindPackageHandleStandardArgs)
find_package_handle_standard_args(gtkmm
FOUND_VAR GTKMM_FOUND
REQUIRED_VARS
GTKMM_LIB
GTKMM_INCLUDE_DIRS

View File

@@ -0,0 +1,17 @@
find_path(IXWEBSOCKET_INCLUDE_DIR
NAMES ixwebsocket/IXWebSocket.h)
find_library(IXWEBSOCKET_LIBRARY
NAMES ixwebsocket
HINTS ${IXWEBSOCKET_LIBRARY_ROOT})
set(IXWEBSOCKET_LIBRARIES ${IXWEBSOCKET_LIBRARY})
set(IXWEBSOCKET_INCLUDE_DIRS ${IXWEBSOCKET_INCLUDE_DIR})
include(FindPackageHandleStandardArgs)
find_package_handle_standard_args(ixwebsocket
FOUND_VAR IXWEBSOCKET_FOUND
REQUIRED_VARS
IXWEBSOCKET_LIBRARY
IXWEBSOCKET_INCLUDE_DIR
VERSION_VAR IXWEBSOCKET_VERSION)

View File

@@ -22,6 +22,7 @@ set(NLOHMANN_JSON_LIBRARIES "")
include(FindPackageHandleStandardArgs)
find_package_handle_standard_args(nlohmann_json
FOUND_VAR NLOHMANN_JSON_FOUND
REQUIRED_VARS
NLOHMANN_JSON_INCLUDE_DIR
VERSION_VAR NLOHMANN_JSON_VERSION)

View File

@@ -1,6 +1,4 @@
set(PANGO_LIBRARY_NAME pango-1.0)
set(PANGOCAIRO_LIBRARY_NAME pangocairo-1.0)
set(PANGOFT2_LIBRARY_NAME pangoft2-1.0)
find_package(HarfBuzz)
find_package(cairo)
@@ -44,31 +42,12 @@ find_library(PANGO_LIBRARY
PATH_SUFFIXES ${PANGO_LIBRARY_NAME}
${PANGO_LIBRARY_NAME}/include)
find_library(PANGOCAIRO_LIBRARY
NAMES ${PANGOCAIRO_LIBRARY_NAME}
pangocairo
HINTS ${PANGO_LIBRARY_HINTS}
/usr/lib
/usr/local/lib
/opt/local/lib
PATH_SUFFIXES ${PANGO_LIBRARY_NAME}
${PANGO_LIBRARY_NAME}/include)
find_library(PANGOFT2_LIBRARY
NAMES ${PANGOFT2_LIBRARY_NAME}
pangoft2
HINTS ${PANGO_LIBRARY_HINTS}
/usr/lib
/usr/local/lib
/opt/local/lib
PATH_SUFFIXES ${PANGO_LIBRARY_NAME}
${PANGO_LIBRARY_NAME}/include)
set(PANGO_LIBRARIES ${PANGO_LIBRARY};${HARFBUZZ_LIBRARIES};${CAIRO_LIBRARIES};${FREETYPE_LIBRARIES};${PANGOCAIRO_LIBRARY};${PANGOFT2_LIBRARY})
set(PANGO_LIBRARIES ${PANGO_LIBRARY};${HARFBUZZ_LIBRARIES};${CAIRO_LIBRARIES};${FREETYPE_LIBRARIES})
set(PANGO_INCLUDE_DIRS ${PANGO_INCLUDE_DIR};${PANGO_CONFIG_INCLUDE_DIRS};${HARFBUZZ_INCLUDE_DIR};${CAIRO_INCLUDE_DIRS};${FREETYPE_INCLUDE_DIRS})
include(FindPackageHandleStandardArgs)
find_package_handle_standard_args(pango
FOUND_VAR PANGO_FOUND
REQUIRED_VARS
PANGO_LIBRARY
PANGO_INCLUDE_DIR

View File

@@ -56,6 +56,7 @@ set(PANGOMM_INCLUDE_DIRS ${PANGOMM_INCLUDE_DIR};${PANGOMM_CONFIG_INCLUDE_DIR};${
include(FindPackageHandleStandardArgs)
find_package_handle_standard_args(pangomm
FOUND_VAR PANGOMM_FOUND
REQUIRED_VARS
PANGOMM_LIBRARY
PANGOMM_INCLUDE_DIRS

View File

@@ -32,6 +32,7 @@ set(SIGC++_INCLUDE_DIRS ${SIGC++_INCLUDE_DIR};${SIGC++_CONFIG_INCLUDE_DIR})
include(FindPackageHandleStandardArgs)
find_package_handle_standard_args(sigc++
FOUND_VAR SIGC++_FOUND
REQUIRED_VARS
SIGC++_INCLUDE_DIR
SIGC++_LIBRARY

View File

@@ -10,6 +10,8 @@ CellRendererPixbufAnimation::CellRendererPixbufAnimation()
property_ypad() = 2;
}
CellRendererPixbufAnimation::~CellRendererPixbufAnimation() {}
Glib::PropertyProxy<Glib::RefPtr<Gdk::Pixbuf>> CellRendererPixbufAnimation::property_pixbuf() {
return m_property_pixbuf.get_proxy();
}
@@ -59,6 +61,7 @@ void CellRendererPixbufAnimation::render_vfunc(const Cairo::RefPtr<Cairo::Contex
Gtk::CellRendererState flags) {
Gtk::Requisition minimum, natural;
get_preferred_size(widget, minimum, natural);
auto alloc = widget.get_allocation();
int xpad, ypad;
get_padding(xpad, ypad);
int pix_x = cell_area.get_x() + xpad;

View File

@@ -6,7 +6,7 @@
class CellRendererPixbufAnimation : public Gtk::CellRenderer {
public:
CellRendererPixbufAnimation();
~CellRendererPixbufAnimation() override = default;
virtual ~CellRendererPixbufAnimation();
Glib::PropertyProxy<Glib::RefPtr<Gdk::Pixbuf>> property_pixbuf();
Glib::PropertyProxy<Glib::RefPtr<Gdk::PixbufAnimation>> property_pixbuf_animation();

813
components/channels.cpp Normal file
View File

@@ -0,0 +1,813 @@
#include "channels.hpp"
#include <algorithm>
#include <map>
#include <unordered_map>
#include "../abaddon.hpp"
#include "../imgmanager.hpp"
#include "../util.hpp"
#include "statusindicator.hpp"
void ChannelListRow::Collapse() {}
void ChannelListRow::Expand() {}
void ChannelListRow::MakeReadOnly(Gtk::TextView *tv) {
tv->set_can_focus(false);
tv->set_editable(false);
tv->signal_realize().connect([tv]() {
auto window = tv->get_window(Gtk::TEXT_WINDOW_TEXT);
auto display = window->get_display();
auto cursor = Gdk::Cursor::create(display, "default"); // textview uses "text" which looks out of place
window->set_cursor(cursor);
});
// stupid hack to prevent selection
auto buf = tv->get_buffer();
buf->property_has_selection().signal_changed().connect([tv, buf]() {
Gtk::TextBuffer::iterator a, b;
buf->get_bounds(a, b);
buf->select_range(a, a);
});
}
ChannelListRowDMHeader::ChannelListRowDMHeader() {
m_ev = Gtk::manage(new Gtk::EventBox);
m_box = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL));
m_lbl = Gtk::manage(new Gtk::Label);
get_style_context()->add_class("channel-row");
m_lbl->get_style_context()->add_class("channel-row-label");
m_lbl->set_use_markup(true);
m_lbl->set_markup("<b>Direct Messages</b>");
m_box->set_halign(Gtk::ALIGN_START);
m_box->pack_start(*m_lbl);
m_ev->add(*m_box);
add(*m_ev);
show_all_children();
}
ChannelListRowDMChannel::ChannelListRowDMChannel(const ChannelData *data) {
ID = data->ID;
m_ev = Gtk::manage(new Gtk::EventBox);
m_box = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL));
std::optional<UserData> top_recipient; // potentially nullopt in group dm
const auto recipients = data->GetDMRecipients();
if (recipients.size() > 0)
top_recipient = recipients[0];
const static bool alt = Abaddon::Get().GetSettings().GetUseMobileLayout();
if (alt) {
auto *tmp = Gtk::manage(new Gtk::Label);
m_lbl = tmp;
if (data->Type == ChannelType::DM)
tmp->set_text(top_recipient->Username);
else if (data->Type == ChannelType::GROUP_DM)
tmp->set_text(std::to_string(recipients.size()) + " users");
} else {
auto *tmp = Gtk::manage(new Gtk::TextView);
m_lbl = tmp;
MakeReadOnly(tmp);
auto buf = tmp->get_buffer();
if (data->Type == ChannelType::DM)
buf->set_text(top_recipient->Username);
else if (data->Type == ChannelType::GROUP_DM)
buf->set_text(std::to_string(recipients.size()) + " users");
static bool show_emojis = Abaddon::Get().GetSettings().GetShowEmojis();
if (show_emojis)
Abaddon::Get().GetEmojis().ReplaceEmojis(buf, ChannelEmojiSize);
}
AddWidgetMenuHandler(m_ev, m_menu);
AddWidgetMenuHandler(m_lbl, m_menu);
m_menu_copy_id = Gtk::manage(new Gtk::MenuItem("_Copy ID", true));
m_menu_copy_id->signal_activate().connect([this] {
Gtk::Clipboard::get()->set_text(std::to_string(ID));
});
if (data->Type == ChannelType::GROUP_DM)
m_menu_close = Gtk::manage(new Gtk::MenuItem("_Leave DM", true));
else
m_menu_close = Gtk::manage(new Gtk::MenuItem("_Close DM", true));
m_menu_close->signal_activate().connect([this] {
Abaddon::Get().GetDiscordClient().CloseDM(ID);
});
m_menu.append(*m_menu_copy_id);
m_menu.append(*m_menu_close);
m_menu.show_all();
get_style_context()->add_class("channel-row");
m_lbl->get_style_context()->add_class("channel-row-label");
if (data->Type == ChannelType::DM) {
m_status = Gtk::manage(new StatusIndicator(top_recipient->ID));
m_status->set_margin_start(5);
m_icon = Gtk::manage(new Gtk::Image(Abaddon::Get().GetImageManager().GetPlaceholder(24)));
auto cb = [this](const Glib::RefPtr<Gdk::Pixbuf> &pb) {
m_icon->property_pixbuf() = pb->scale_simple(24, 24, Gdk::INTERP_BILINEAR);
};
Abaddon::Get().GetImageManager().LoadFromURL(top_recipient->GetAvatarURL("png", "16"), sigc::track_obj(cb, *this));
}
m_box->set_halign(Gtk::ALIGN_START);
if (m_icon != nullptr)
m_box->pack_start(*m_icon);
if (m_status != nullptr)
m_box->pack_start(*m_status);
m_box->pack_start(*m_lbl);
m_ev->add(*m_box);
add(*m_ev);
show_all_children();
}
ChannelListRowGuild::ChannelListRowGuild(const GuildData *data) {
ID = data->ID;
m_ev = Gtk::manage(new Gtk::EventBox);
m_box = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL));
const static bool alt = Abaddon::Get().GetSettings().GetUseMobileLayout();
if (alt) {
m_lbl = Gtk::manage(new Gtk::Label(data->Name));
} else {
auto *tmp = Gtk::manage(new Gtk::TextView);
m_lbl = tmp;
MakeReadOnly(tmp);
auto buf = tmp->get_buffer();
Gtk::TextBuffer::iterator start, end;
buf->get_bounds(start, end);
buf->insert_markup(start, "<b>" + Glib::Markup::escape_text(data->Name) + "</b>");
static bool show_emojis = Abaddon::Get().GetSettings().GetShowEmojis();
if (show_emojis)
Abaddon::Get().GetEmojis().ReplaceEmojis(buf, ChannelEmojiSize);
}
AddWidgetMenuHandler(m_ev, m_menu);
AddWidgetMenuHandler(m_lbl, m_menu);
m_menu_copyid = Gtk::manage(new Gtk::MenuItem("_Copy ID", true));
m_menu_copyid->signal_activate().connect([this]() {
m_signal_copy_id.emit();
});
m_menu.append(*m_menu_copyid);
m_menu_leave = Gtk::manage(new Gtk::MenuItem("_Leave Guild", true));
m_menu_leave->signal_activate().connect([this]() {
m_signal_leave.emit();
});
m_menu.append(*m_menu_leave);
m_menu_settings = Gtk::manage(new Gtk::MenuItem("Guild _Settings", true));
m_menu_settings->signal_activate().connect([this]() {
m_signal_settings.emit();
});
m_menu.append(*m_menu_settings);
m_menu.show_all();
const auto show_animations = Abaddon::Get().GetSettings().GetShowAnimations();
auto &img = Abaddon::Get().GetImageManager();
if (data->HasIcon()) {
if (data->HasAnimatedIcon() && show_animations) {
m_icon = Gtk::manage(new Gtk::Image(img.GetPlaceholder(24)));
auto cb = [this](const Glib::RefPtr<Gdk::PixbufAnimation> &pb) {
m_icon->property_pixbuf_animation() = pb;
};
img.LoadAnimationFromURL(data->GetIconURL("gif", "32"), 24, 24, sigc::track_obj(cb, *this));
} else {
m_icon = Gtk::manage(new Gtk::Image(img.GetPlaceholder(24)));
auto cb = [this](const Glib::RefPtr<Gdk::Pixbuf> &pb) {
m_icon->property_pixbuf() = pb->scale_simple(24, 24, Gdk::INTERP_BILINEAR);
};
img.LoadFromURL(data->GetIconURL("png", "32"), sigc::track_obj(cb, *this));
}
} else {
m_icon = Gtk::manage(new Gtk::Image(Abaddon::Get().GetImageManager().GetPlaceholder(24)));
}
get_style_context()->add_class("channel-row");
get_style_context()->add_class("channel-row-guild");
m_lbl->get_style_context()->add_class("channel-row-label");
m_box->set_halign(Gtk::ALIGN_START);
m_box->pack_start(*m_icon);
m_box->pack_start(*m_lbl);
m_ev->add(*m_box);
add(*m_ev);
show_all_children();
}
ChannelListRowGuild::type_signal_copy_id ChannelListRowGuild::signal_copy_id() {
return m_signal_copy_id;
}
ChannelListRowGuild::type_signal_leave ChannelListRowGuild::signal_leave() {
return m_signal_leave;
}
ChannelListRowGuild::type_signal_settings ChannelListRowGuild::signal_settings() {
return m_signal_settings;
}
ChannelListRowCategory::ChannelListRowCategory(const ChannelData *data) {
ID = data->ID;
m_ev = Gtk::manage(new Gtk::EventBox);
m_box = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL));
const static bool alt = Abaddon::Get().GetSettings().GetUseMobileLayout();
if (alt) {
m_lbl = Gtk::manage(new Gtk::Label(*data->Name));
} else {
auto *tmp = Gtk::manage(new Gtk::TextView);
m_lbl = tmp;
MakeReadOnly(tmp);
auto buf = tmp->get_buffer();
buf->set_text(*data->Name);
static bool show_emojis = Abaddon::Get().GetSettings().GetShowEmojis();
if (show_emojis)
Abaddon::Get().GetEmojis().ReplaceEmojis(buf, ChannelEmojiSize);
}
m_arrow = Gtk::manage(new Gtk::Arrow(Gtk::ARROW_DOWN, Gtk::SHADOW_NONE));
m_menu_copyid = Gtk::manage(new Gtk::MenuItem("_Copy ID", true));
m_menu_copyid->signal_activate().connect([this]() {
m_signal_copy_id.emit();
});
m_menu.append(*m_menu_copyid);
m_menu.show_all();
AddWidgetMenuHandler(m_ev, m_menu);
AddWidgetMenuHandler(m_lbl, m_menu);
get_style_context()->add_class("channel-row");
get_style_context()->add_class("channel-row-category");
m_lbl->get_style_context()->add_class("channel-row-label");
m_box->set_halign(Gtk::ALIGN_START);
m_box->pack_start(*m_arrow);
m_box->pack_start(*m_lbl);
m_ev->add(*m_box);
add(*m_ev);
show_all_children();
}
void ChannelListRowCategory::Collapse() {
m_arrow->set(Gtk::ARROW_RIGHT, Gtk::SHADOW_NONE);
}
void ChannelListRowCategory::Expand() {
m_arrow->set(IsUserCollapsed ? Gtk::ARROW_RIGHT : Gtk::ARROW_DOWN, Gtk::SHADOW_NONE);
}
ChannelListRowCategory::type_signal_copy_id ChannelListRowCategory::signal_copy_id() {
return m_signal_copy_id;
}
ChannelListRowChannel::ChannelListRowChannel(const ChannelData *data) {
ID = data->ID;
m_ev = Gtk::manage(new Gtk::EventBox);
m_box = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL));
const static bool alt = Abaddon::Get().GetSettings().GetUseMobileLayout();
if (alt) {
m_lbl = Gtk::manage(new Gtk::Label("#" + *data->Name));
} else {
auto *tmp = Gtk::manage(new Gtk::TextView);
m_lbl = tmp;
MakeReadOnly(tmp);
auto buf = tmp->get_buffer();
buf->set_text("#" + *data->Name);
static bool show_emojis = Abaddon::Get().GetSettings().GetShowEmojis();
if (show_emojis)
Abaddon::Get().GetEmojis().ReplaceEmojis(buf, ChannelEmojiSize);
}
m_menu_copyid = Gtk::manage(new Gtk::MenuItem("_Copy ID", true));
m_menu_copyid->signal_activate().connect([this]() {
m_signal_copy_id.emit();
});
m_menu.append(*m_menu_copyid);
m_menu.show_all();
AddWidgetMenuHandler(m_ev, m_menu);
AddWidgetMenuHandler(m_lbl, m_menu);
get_style_context()->add_class("channel-row");
get_style_context()->add_class("channel-row-channel");
m_lbl->get_style_context()->add_class("channel-row-label");
if (data->IsNSFW.has_value() && *data->IsNSFW) {
get_style_context()->add_class("nsfw");
m_lbl->get_style_context()->add_class("nsfw");
}
m_box->set_halign(Gtk::ALIGN_START);
m_box->pack_start(*m_lbl);
m_ev->add(*m_box);
add(*m_ev);
show_all_children();
}
ChannelListRowChannel::type_signal_copy_id ChannelListRowChannel::signal_copy_id() {
return m_signal_copy_id;
}
ChannelList::ChannelList() {
m_main = Gtk::manage(new Gtk::ScrolledWindow);
m_list = Gtk::manage(new Gtk::ListBox);
m_list->get_style_context()->add_class("channel-list");
m_list->set_activate_on_single_click(true);
m_list->signal_row_activated().connect(sigc::mem_fun(*this, &ChannelList::on_row_activated));
m_main->add(*m_list);
m_main->show_all();
// maybe will regret doing it this way
auto &discord = Abaddon::Get().GetDiscordClient();
auto cb = [this, &discord](const Message &message) {
const auto channel = discord.GetChannel(message.ChannelID);
if (!channel.has_value()) return;
if (channel->Type == ChannelType::DM || channel->Type == ChannelType::GROUP_DM)
CheckBumpDM(message.ChannelID);
};
discord.signal_message_create().connect(sigc::track_obj(cb, *this));
}
Gtk::Widget *ChannelList::GetRoot() const {
return m_main;
}
void ChannelList::UpdateNewGuild(Snowflake id) {
auto sort = Abaddon::Get().GetDiscordClient().GetUserSortedGuilds();
if (sort.size() == 1) {
UpdateListing();
return;
}
const auto insert_at = [this, id](int listpos) {
InsertGuildAt(id, listpos);
};
auto it = std::find(sort.begin(), sort.end(), id);
if (it == sort.end()) return;
// if the new guild pos is at the end use -1
if (it + 1 == sort.end()) {
insert_at(-1);
return;
}
// find the position of the guild below it into the listbox
auto below_id = *(it + 1);
auto below_it = m_id_to_row.find(below_id);
if (below_it == m_id_to_row.end()) {
UpdateListing();
return;
}
auto below_pos = below_it->second->get_index();
// stick it just above
insert_at(below_pos - 1);
}
void ChannelList::UpdateRemoveGuild(Snowflake id) {
auto it = m_guild_id_to_row.find(id);
if (it == m_guild_id_to_row.end()) return;
auto row = dynamic_cast<ChannelListRow *>(it->second);
if (row == nullptr) return;
DeleteRow(row);
}
void ChannelList::UpdateRemoveChannel(Snowflake id) {
auto it = m_id_to_row.find(id);
if (it == m_id_to_row.end()) return;
auto row = dynamic_cast<ChannelListRow *>(it->second);
if (row == nullptr) return;
DeleteRow(row);
}
// this is total shit
void ChannelList::UpdateChannelCategory(Snowflake id) {
const auto data = Abaddon::Get().GetDiscordClient().GetChannel(id);
const auto guild = Abaddon::Get().GetDiscordClient().GetGuild(*data->GuildID);
auto git = m_guild_id_to_row.find(*data->GuildID);
if (git == m_guild_id_to_row.end()) return;
auto *guild_row = git->second;
if (!data.has_value() || !guild.has_value()) return;
auto it = m_id_to_row.find(id);
if (it == m_id_to_row.end()) return;
auto row = dynamic_cast<ChannelListRowCategory *>(it->second);
if (row == nullptr) return;
const bool old_collapsed = row->IsUserCollapsed;
const bool visible = row->is_visible();
std::map<int, Snowflake> child_rows;
for (auto child : row->Children) {
child_rows[child->get_index()] = child->ID;
}
guild_row->Children.erase(row);
DeleteRow(row);
int pos = guild_row->get_index();
const auto sorted = guild->GetSortedChannels(id);
const auto sorted_it = std::find(sorted.begin(), sorted.end(), id);
if (sorted_it == sorted.end()) return;
if (std::next(sorted_it) == sorted.end()) {
const auto x = m_id_to_row.find(*std::prev(sorted_it));
if (x != m_id_to_row.end())
pos = x->second->get_index() + 1;
} else {
const auto x = m_id_to_row.find(*std::next(sorted_it));
if (x != m_id_to_row.end())
pos = x->second->get_index();
}
auto *new_row = Gtk::manage(new ChannelListRowCategory(&*data));
new_row->IsUserCollapsed = old_collapsed;
if (visible)
new_row->show();
m_id_to_row[id] = new_row;
new_row->signal_copy_id().connect(sigc::bind(sigc::mem_fun(*this, &ChannelList::OnMenuCopyID), new_row->ID));
new_row->Parent = guild_row;
guild_row->Children.insert(new_row);
m_list->insert(*new_row, pos);
int i = 1;
for (const auto &[idx, child_id] : child_rows) {
const auto channel = Abaddon::Get().GetDiscordClient().GetChannel(child_id);
if (channel.has_value()) {
auto *new_child = Gtk::manage(new ChannelListRowChannel(&*channel));
new_row->Children.insert(new_child);
new_child->Parent = new_row;
new_child->signal_copy_id().connect(sigc::bind(sigc::mem_fun(*this, &ChannelList::OnMenuCopyID), new_child->ID));
m_id_to_row[child_id] = new_child;
if (visible && !new_row->IsUserCollapsed)
new_child->show();
m_list->insert(*new_child, pos + i++);
}
}
}
// so is this
void ChannelList::UpdateChannel(Snowflake id) {
const auto data = Abaddon::Get().GetDiscordClient().GetChannel(id);
const auto guild = Abaddon::Get().GetDiscordClient().GetGuild(*data->GuildID);
const auto *guild_row = m_guild_id_to_row.at(*data->GuildID);
if (data->Type == ChannelType::GUILD_CATEGORY) {
UpdateChannelCategory(id);
return;
}
auto it = m_id_to_row.find(id);
if (it == m_id_to_row.end()) return; // stuff like voice doesnt have a row yet
auto row = dynamic_cast<ChannelListRowChannel *>(it->second);
const bool old_collapsed = row->IsUserCollapsed;
const bool old_visible = row->is_visible();
DeleteRow(row);
int pos = guild_row->get_index() + 1; // fallback
const auto sorted = guild->GetSortedChannels();
const auto sorted_it = std::find(sorted.begin(), sorted.end(), id);
if (sorted_it + 1 == sorted.end()) {
const auto x = m_id_to_row.find(*std::prev(sorted_it));
if (x != m_id_to_row.end())
pos = x->second->get_index() + 1;
} else {
const auto x = m_id_to_row.find(*std::next(sorted_it));
if (x != m_id_to_row.end())
pos = x->second->get_index();
}
auto *new_row = Gtk::manage(new ChannelListRowChannel(&*data));
new_row->IsUserCollapsed = old_collapsed;
m_id_to_row[id] = new_row;
if (data->ParentID.has_value()) {
new_row->Parent = m_id_to_row.at(*data->ParentID);
} else {
new_row->Parent = m_guild_id_to_row.at(*data->GuildID);
}
new_row->Parent->Children.insert(new_row);
if (new_row->Parent->is_visible() && !new_row->Parent->IsUserCollapsed)
new_row->show();
new_row->signal_copy_id().connect(sigc::bind(sigc::mem_fun(*this, &ChannelList::OnMenuCopyID), new_row->ID));
m_list->insert(*new_row, pos);
}
void ChannelList::UpdateCreateDMChannel(Snowflake id) {
const auto chan = Abaddon::Get().GetDiscordClient().GetChannel(id);
auto *dm_row = Gtk::manage(new ChannelListRowDMChannel(&*chan));
dm_row->IsUserCollapsed = false;
m_list->insert(*dm_row, m_dm_header_row->get_index() + 1);
m_dm_header_row->Children.insert(dm_row);
m_id_to_row[id] = dm_row;
if (!m_dm_header_row->IsUserCollapsed)
dm_row->show();
}
void ChannelList::UpdateCreateChannel(Snowflake id) {
const auto &discord = Abaddon::Get().GetDiscordClient();
const auto data = discord.GetChannel(id);
if (data->Type == ChannelType::DM || data->Type == ChannelType::GROUP_DM) {
UpdateCreateDMChannel(id);
return;
}
const auto guild = discord.GetGuild(*data->GuildID);
auto *guild_row = m_guild_id_to_row.at(*data->GuildID);
int pos = guild_row->get_index() + 1;
const auto sorted = guild->GetSortedChannels();
const auto sorted_it = std::find(sorted.begin(), sorted.end(), id);
if (sorted_it + 1 == sorted.end()) {
const auto x = m_id_to_row.find(*std::prev(sorted_it));
if (x != m_id_to_row.end())
pos = x->second->get_index() + 1;
} else {
const auto x = m_id_to_row.find(*std::next(sorted_it));
if (x != m_id_to_row.end())
pos = x->second->get_index();
}
ChannelListRow *row;
if (data->Type == ChannelType::GUILD_TEXT || data->Type == ChannelType::GUILD_NEWS) {
auto *tmp = Gtk::manage(new ChannelListRowChannel(&*data));
tmp->signal_copy_id().connect(sigc::bind(sigc::mem_fun(*this, &ChannelList::OnMenuCopyID), tmp->ID));
row = tmp;
} else if (data->Type == ChannelType::GUILD_CATEGORY) {
auto *tmp = Gtk::manage(new ChannelListRowCategory(&*data));
tmp->signal_copy_id().connect(sigc::bind(sigc::mem_fun(*this, &ChannelList::OnMenuCopyID), tmp->ID));
row = tmp;
} else
return;
row->IsUserCollapsed = false;
if (!guild_row->IsUserCollapsed)
row->show();
row->Parent = guild_row;
guild_row->Children.insert(row);
m_id_to_row[id] = row;
m_list->insert(*row, pos);
}
void ChannelList::UpdateGuild(Snowflake id) {
// the only thing changed is the row containing the guild item so just recreate it
const auto data = Abaddon::Get().GetDiscordClient().GetGuild(id);
if (!data.has_value()) return;
auto it = m_guild_id_to_row.find(id);
if (it == m_guild_id_to_row.end()) return;
auto *row = dynamic_cast<ChannelListRowGuild *>(it->second);
const auto children = row->Children;
const auto index = row->get_index();
const bool old_collapsed = row->IsUserCollapsed;
const bool old_gindex = row->GuildIndex;
delete row;
auto *new_row = Gtk::manage(new ChannelListRowGuild(&*data));
new_row->IsUserCollapsed = old_collapsed;
new_row->GuildIndex = old_gindex;
m_guild_id_to_row[new_row->ID] = new_row;
new_row->signal_leave().connect(sigc::bind(sigc::mem_fun(*this, &ChannelList::OnGuildMenuLeave), new_row->ID));
new_row->signal_copy_id().connect(sigc::bind(sigc::mem_fun(*this, &ChannelList::OnMenuCopyID), new_row->ID));
new_row->signal_settings().connect(sigc::bind(sigc::mem_fun(*this, &ChannelList::OnGuildMenuSettings), new_row->ID));
new_row->Children = children;
for (auto child : children)
child->Parent = new_row;
new_row->show_all();
m_list->insert(*new_row, index);
}
void ChannelList::SetActiveChannel(Snowflake id) {
auto it = m_id_to_row.find(id);
if (it == m_id_to_row.end()) return;
m_list->select_row(*it->second);
}
void ChannelList::CollapseRow(ChannelListRow *row) {
row->Collapse();
for (auto child : row->Children) {
child->hide();
CollapseRow(child);
}
}
void ChannelList::ExpandRow(ChannelListRow *row) {
row->Expand();
row->show();
if (!row->IsUserCollapsed)
for (auto child : row->Children)
ExpandRow(child);
}
void ChannelList::DeleteRow(ChannelListRow *row) {
for (auto child : row->Children)
DeleteRow(child);
if (row->Parent != nullptr)
row->Parent->Children.erase(row);
else
printf("row has no parent!\n");
if (dynamic_cast<ChannelListRowGuild *>(row) != nullptr)
m_guild_id_to_row.erase(row->ID);
else
m_id_to_row.erase(row->ID);
delete row;
}
void ChannelList::on_row_activated(Gtk::ListBoxRow *tmprow) {
auto row = dynamic_cast<ChannelListRow *>(tmprow);
if (row == nullptr) return;
bool new_collapsed = !row->IsUserCollapsed;
row->IsUserCollapsed = new_collapsed;
// kinda ugly
if (dynamic_cast<ChannelListRowChannel *>(row) != nullptr || dynamic_cast<ChannelListRowDMChannel *>(row) != nullptr)
m_signal_action_channel_item_select.emit(row->ID);
if (new_collapsed)
CollapseRow(row);
else
ExpandRow(row);
}
void ChannelList::InsertGuildAt(Snowflake id, int pos) {
const auto insert_and_adjust = [&](Gtk::Widget &widget) {
m_list->insert(widget, pos);
if (pos != -1) pos++;
};
const auto &discord = Abaddon::Get().GetDiscordClient();
const auto guild_data = discord.GetGuild(id);
if (!guild_data.has_value()) return;
std::map<int, ChannelData> orphan_channels;
std::unordered_map<Snowflake, std::vector<ChannelData>> cat_to_channels;
if (guild_data->Channels.has_value())
for (const auto &dc : *guild_data->Channels) {
const auto channel = discord.GetChannel(dc.ID);
if (!channel.has_value()) continue;
if (channel->Type != ChannelType::GUILD_TEXT && channel->Type != ChannelType::GUILD_NEWS) continue;
if (channel->ParentID.has_value())
cat_to_channels[*channel->ParentID].push_back(*channel);
else
orphan_channels[*channel->Position] = *channel;
}
auto *guild_row = Gtk::manage(new ChannelListRowGuild(&*guild_data));
guild_row->show_all();
guild_row->IsUserCollapsed = true;
guild_row->GuildIndex = m_guild_count++;
insert_and_adjust(*guild_row);
m_guild_id_to_row[guild_row->ID] = guild_row;
guild_row->signal_leave().connect(sigc::bind(sigc::mem_fun(*this, &ChannelList::OnGuildMenuLeave), guild_row->ID));
guild_row->signal_copy_id().connect(sigc::bind(sigc::mem_fun(*this, &ChannelList::OnMenuCopyID), guild_row->ID));
guild_row->signal_settings().connect(sigc::bind(sigc::mem_fun(*this, &ChannelList::OnGuildMenuSettings), guild_row->ID));
// add channels with no parent category
for (const auto &[pos, channel] : orphan_channels) {
auto *chan_row = Gtk::manage(new ChannelListRowChannel(&channel));
chan_row->IsUserCollapsed = false;
chan_row->signal_copy_id().connect(sigc::bind(sigc::mem_fun(*this, &ChannelList::OnMenuCopyID), chan_row->ID));
insert_and_adjust(*chan_row);
guild_row->Children.insert(chan_row);
chan_row->Parent = guild_row;
m_id_to_row[chan_row->ID] = chan_row;
}
// categories
std::map<int, std::vector<ChannelData>> sorted_categories;
if (guild_data->Channels.has_value())
for (const auto &dc : *guild_data->Channels) {
const auto channel = discord.GetChannel(dc.ID);
if (!channel.has_value()) continue;
if (channel->Type == ChannelType::GUILD_CATEGORY)
sorted_categories[*channel->Position].push_back(*channel);
}
for (auto &[pos, catvec] : sorted_categories) {
std::sort(catvec.begin(), catvec.end(), [](const ChannelData &a, const ChannelData &b) { return a.ID < b.ID; });
for (const auto cat : catvec) {
auto *cat_row = Gtk::manage(new ChannelListRowCategory(&cat));
cat_row->IsUserCollapsed = false;
cat_row->signal_copy_id().connect(sigc::bind(sigc::mem_fun(*this, &ChannelList::OnMenuCopyID), cat_row->ID));
insert_and_adjust(*cat_row);
guild_row->Children.insert(cat_row);
cat_row->Parent = guild_row;
m_id_to_row[cat_row->ID] = cat_row;
// child channels
if (cat_to_channels.find(cat.ID) == cat_to_channels.end()) continue;
std::map<int, ChannelData> sorted_channels;
for (const auto channel : cat_to_channels.at(cat.ID))
sorted_channels[*channel.Position] = channel;
for (const auto &[pos, channel] : sorted_channels) {
auto *chan_row = Gtk::manage(new ChannelListRowChannel(&channel));
chan_row->IsUserCollapsed = false;
chan_row->signal_copy_id().connect(sigc::bind(sigc::mem_fun(*this, &ChannelList::OnMenuCopyID), chan_row->ID));
insert_and_adjust(*chan_row);
cat_row->Children.insert(chan_row);
chan_row->Parent = cat_row;
m_id_to_row[chan_row->ID] = chan_row;
}
}
}
}
void ChannelList::AddPrivateChannels() {
const auto &discord = Abaddon::Get().GetDiscordClient();
auto dms_ = discord.GetPrivateChannels();
std::vector<ChannelData> dms;
for (const auto &x : dms_) {
const auto chan = discord.GetChannel(x);
dms.push_back(*chan);
}
std::sort(dms.begin(), dms.end(), [&](const ChannelData &a, const ChannelData &b) -> bool {
return a.LastMessageID > b.LastMessageID;
});
m_dm_header_row = Gtk::manage(new ChannelListRowDMHeader);
m_dm_header_row->show_all();
m_dm_header_row->IsUserCollapsed = true;
m_list->add(*m_dm_header_row);
for (const auto &dm : dms) {
auto *dm_row = Gtk::manage(new ChannelListRowDMChannel(&dm));
dm_row->Parent = m_dm_header_row;
m_id_to_row[dm.ID] = dm_row;
dm_row->IsUserCollapsed = false;
m_list->add(*dm_row);
m_dm_header_row->Children.insert(dm_row);
}
}
void ChannelList::UpdateListing() {
std::unordered_set<Snowflake> guilds = Abaddon::Get().GetDiscordClient().GetGuilds();
auto children = m_list->get_children();
auto it = children.begin();
while (it != children.end()) {
delete *it;
it++;
}
m_id_to_row.clear();
m_guild_count = 0;
AddPrivateChannels();
auto sorted_guilds = Abaddon::Get().GetDiscordClient().GetUserSortedGuilds();
for (auto gid : sorted_guilds) {
InsertGuildAt(gid, -1);
}
}
void ChannelList::OnMenuCopyID(Snowflake id) {
Gtk::Clipboard::get()->set_text(std::to_string(id));
}
void ChannelList::OnGuildMenuLeave(Snowflake id) {
m_signal_action_guild_leave.emit(id);
}
void ChannelList::OnGuildMenuSettings(Snowflake id) {
m_signal_action_guild_settings.emit(id);
}
void ChannelList::CheckBumpDM(Snowflake channel_id) {
auto it = m_id_to_row.find(channel_id);
if (it == m_id_to_row.end()) return;
auto *row = it->second;
const auto index = row->get_index();
if (index == 1) return; // 1 is top of dm list
const bool selected = row->is_selected();
row->Parent->Children.erase(row);
delete row;
const auto chan = Abaddon::Get().GetDiscordClient().GetChannel(channel_id);
auto *dm_row = Gtk::manage(new ChannelListRowDMChannel(&*chan));
dm_row->Parent = m_dm_header_row;
m_dm_header_row->Children.insert(dm_row);
m_id_to_row[channel_id] = dm_row;
dm_row->IsUserCollapsed = false;
m_list->insert(*dm_row, 1);
m_dm_header_row->Children.insert(dm_row);
if (selected)
m_list->select_row(*dm_row);
if (m_dm_header_row->is_visible() && !m_dm_header_row->IsUserCollapsed)
dm_row->show();
}
ChannelList::type_signal_action_channel_item_select ChannelList::signal_action_channel_item_select() {
return m_signal_action_channel_item_select;
}
ChannelList::type_signal_action_guild_leave ChannelList::signal_action_guild_leave() {
return m_signal_action_guild_leave;
}
ChannelList::type_signal_action_guild_settings ChannelList::signal_action_guild_settings() {
return m_signal_action_guild_settings;
}

191
components/channels.hpp Normal file
View File

@@ -0,0 +1,191 @@
#pragma once
#include <gtkmm.h>
#include <string>
#include <queue>
#include <mutex>
#include <unordered_set>
#include <unordered_map>
#include <sigc++/sigc++.h>
#include "../discord/discord.hpp"
static const constexpr int ChannelEmojiSize = 16;
class ChannelListRow : public Gtk::ListBoxRow {
public:
bool IsUserCollapsed;
Snowflake ID;
std::unordered_set<ChannelListRow *> Children;
ChannelListRow *Parent = nullptr;
virtual void Collapse();
virtual void Expand();
static void MakeReadOnly(Gtk::TextView *tv);
};
class ChannelListRowDMHeader : public ChannelListRow {
public:
ChannelListRowDMHeader();
protected:
Gtk::EventBox *m_ev;
Gtk::Box *m_box;
Gtk::Label *m_lbl;
};
class StatusIndicator;
class ChannelListRowDMChannel : public ChannelListRow {
public:
ChannelListRowDMChannel(const ChannelData *data);
protected:
Gtk::EventBox *m_ev;
Gtk::Box *m_box;
StatusIndicator *m_status = nullptr;
Gtk::Widget *m_lbl;
Gtk::Image *m_icon = nullptr;
Gtk::Menu m_menu;
Gtk::MenuItem *m_menu_close; // leave if group
Gtk::MenuItem *m_menu_copy_id;
};
class ChannelListRowGuild : public ChannelListRow {
public:
ChannelListRowGuild(const GuildData *data);
int GuildIndex;
protected:
Gtk::EventBox *m_ev;
Gtk::Box *m_box;
Gtk::Widget *m_lbl;
Gtk::Image *m_icon;
Gtk::Menu m_menu;
Gtk::MenuItem *m_menu_copyid;
Gtk::MenuItem *m_menu_leave;
Gtk::MenuItem *m_menu_settings;
private:
typedef sigc::signal<void> type_signal_copy_id;
typedef sigc::signal<void> type_signal_leave;
typedef sigc::signal<void> type_signal_settings;
type_signal_copy_id m_signal_copy_id;
type_signal_leave m_signal_leave;
type_signal_settings m_signal_settings;
public:
type_signal_copy_id signal_copy_id();
type_signal_leave signal_leave();
type_signal_settings signal_settings();
};
class ChannelListRowCategory : public ChannelListRow {
public:
ChannelListRowCategory(const ChannelData *data);
virtual void Collapse();
virtual void Expand();
protected:
Gtk::EventBox *m_ev;
Gtk::Box *m_box;
Gtk::Widget *m_lbl;
Gtk::Arrow *m_arrow;
Gtk::Menu m_menu;
Gtk::MenuItem *m_menu_copyid;
private:
typedef sigc::signal<void> type_signal_copy_id;
type_signal_copy_id m_signal_copy_id;
public:
type_signal_copy_id signal_copy_id();
};
class ChannelListRowChannel : public ChannelListRow {
public:
ChannelListRowChannel(const ChannelData *data);
protected:
Gtk::EventBox *m_ev;
Gtk::Box *m_box;
Gtk::Widget *m_lbl;
Gtk::Menu m_menu;
Gtk::MenuItem *m_menu_copyid;
private:
typedef sigc::signal<void> type_signal_copy_id;
type_signal_copy_id m_signal_copy_id;
public:
type_signal_copy_id signal_copy_id();
};
class ChannelList {
public:
ChannelList();
Gtk::Widget *GetRoot() const;
void UpdateListing();
void UpdateNewGuild(Snowflake id);
void UpdateRemoveGuild(Snowflake id);
void UpdateRemoveChannel(Snowflake id);
void UpdateChannel(Snowflake id);
void UpdateCreateDMChannel(Snowflake id);
void UpdateCreateChannel(Snowflake id);
void UpdateGuild(Snowflake id);
void SetActiveChannel(Snowflake id);
protected:
Gtk::ListBox *m_list;
Gtk::ScrolledWindow *m_main;
ChannelListRowDMHeader *m_dm_header_row = nullptr;
void CollapseRow(ChannelListRow *row);
void ExpandRow(ChannelListRow *row);
void DeleteRow(ChannelListRow *row);
void UpdateChannelCategory(Snowflake id);
void on_row_activated(Gtk::ListBoxRow *row);
int m_guild_count;
void OnMenuCopyID(Snowflake id);
void OnGuildMenuLeave(Snowflake id);
void OnGuildMenuSettings(Snowflake id);
Gtk::Menu m_channel_menu;
Gtk::MenuItem *m_channel_menu_copyid;
// i would use one map but in really old guilds there can be a channel w/ same id as the guild so this hacky shit has to do
std::unordered_map<Snowflake, ChannelListRow *> m_guild_id_to_row;
std::unordered_map<Snowflake, ChannelListRow *> m_id_to_row;
void InsertGuildAt(Snowflake id, int pos);
void AddPrivateChannels();
void CheckBumpDM(Snowflake channel_id);
public:
typedef sigc::signal<void, Snowflake> type_signal_action_channel_item_select;
typedef sigc::signal<void, Snowflake> type_signal_action_guild_leave;
typedef sigc::signal<void, Snowflake> type_signal_action_guild_settings;
type_signal_action_channel_item_select signal_action_channel_item_select();
type_signal_action_guild_leave signal_action_guild_leave();
type_signal_action_guild_settings signal_action_guild_settings();
protected:
type_signal_action_channel_item_select m_signal_action_channel_item_select;
type_signal_action_guild_leave m_signal_action_guild_leave;
type_signal_action_guild_settings m_signal_action_guild_settings;
};

View File

@@ -1,7 +1,7 @@
#include <filesystem>
#include "chatinputindicator.hpp"
#include "abaddon.hpp"
#include "util.hpp"
#include "../abaddon.hpp"
#include "../util.hpp"
constexpr static const int MaxUsersInIndicator = 4;
@@ -21,20 +21,19 @@ ChatInputIndicator::ChatInputIndicator()
m_label.show();
// try loading gif
const static auto path = Abaddon::GetResPath("/typing_indicator.gif");
if (!std::filesystem::exists(path)) return;
auto gif_data = ReadWholeFile(path);
if (!std::filesystem::exists("./res/typing_indicator.gif")) return;
auto gif_data = ReadWholeFile("./res/typing_indicator.gif");
auto loader = Gdk::PixbufLoader::create();
loader->signal_size_prepared().connect([&](int inw, int inh) {
int w, h;
GetImageDimensions(inw, inh, w, h, 20, 10);
loader->set_size(w, h);
});
loader->write(gif_data.data(), gif_data.size());
try {
loader->signal_size_prepared().connect([&](int inw, int inh) {
int w, h;
GetImageDimensions(inw, inh, w, h, 20, 10);
loader->set_size(w, h);
});
loader->write(gif_data.data(), gif_data.size());
loader->close();
m_img.property_pixbuf_animation() = loader->get_animation();
} catch (...) {}
} catch (const std::exception &) {}
}
void ChatInputIndicator::AddUser(Snowflake channel_id, const UserData &user, int timeout) {
@@ -84,14 +83,14 @@ void ChatInputIndicator::OnMessageCreate(const Message &message) {
void ChatInputIndicator::SetTypingString(const Glib::ustring &str) {
m_label.set_text(str);
if (str.empty())
if (str == "")
m_img.hide();
else if (m_img.property_pixbuf_animation().get_value())
m_img.show();
}
void ChatInputIndicator::ComputeTypingString() {
if (!m_custom_markup.empty()) {
if (m_custom_markup != "") {
m_label.set_markup(m_custom_markup);
m_img.hide();
return;
@@ -104,7 +103,7 @@ void ChatInputIndicator::ComputeTypingString() {
if (user.has_value())
typers.push_back(*user);
}
if (typers.empty()) {
if (typers.size() == 0) {
SetTypingString("");
} else if (typers.size() == 1) {
SetTypingString(typers[0].Username + " is typing...");
@@ -112,7 +111,7 @@ void ChatInputIndicator::ComputeTypingString() {
SetTypingString(typers[0].Username + " and " + typers[1].Username + " are typing...");
} else if (typers.size() > 2 && typers.size() <= MaxUsersInIndicator) {
Glib::ustring str;
for (size_t i = 0; i < typers.size() - 1; i++)
for (int i = 0; i < typers.size() - 1; i++)
str += typers[i].Username + ", ";
SetTypingString(str + "and " + typers[typers.size() - 1].Username + " are typing...");
} else { // size() > MaxUsersInIndicator

View File

@@ -1,8 +1,8 @@
#pragma once
#include <gtkmm.h>
#include <unordered_map>
#include "discord/message.hpp"
#include "discord/user.hpp"
#include "../discord/message.hpp"
#include "../discord/user.hpp"
class ChatInputIndicator : public Gtk::Box {
public:

View File

@@ -1,7 +1,7 @@
#include "abaddon.hpp"
#include "chatmessage.hpp"
#include "../abaddon.hpp"
#include "../util.hpp"
#include "lazyimage.hpp"
#include "util.hpp"
#include <unordered_map>
constexpr static int EmojiSize = 24; // settings eventually
@@ -9,11 +9,32 @@ constexpr static int AvatarSize = 32;
constexpr static int EmbedImageWidth = 400;
constexpr static int EmbedImageHeight = 300;
constexpr static int ThumbnailSize = 100;
constexpr static int StickerComponentSize = 160;
ChatMessageItemContainer::ChatMessageItemContainer()
: m_main(Gtk::ORIENTATION_VERTICAL) {
add(m_main);
ChatMessageItemContainer::ChatMessageItemContainer() {
m_main = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_VERTICAL));
add(*m_main);
m_menu_copy_id = Gtk::manage(new Gtk::MenuItem("Copy ID"));
m_menu_copy_id->signal_activate().connect(sigc::mem_fun(*this, &ChatMessageItemContainer::on_menu_copy_id));
m_menu.append(*m_menu_copy_id);
m_menu_delete_message = Gtk::manage(new Gtk::MenuItem("Delete Message"));
m_menu_delete_message->signal_activate().connect(sigc::mem_fun(*this, &ChatMessageItemContainer::on_menu_delete_message));
m_menu.append(*m_menu_delete_message);
m_menu_edit_message = Gtk::manage(new Gtk::MenuItem("Edit Message"));
m_menu_edit_message->signal_activate().connect(sigc::mem_fun(*this, &ChatMessageItemContainer::on_menu_edit_message));
m_menu.append(*m_menu_edit_message);
m_menu_copy_content = Gtk::manage(new Gtk::MenuItem("Copy Content"));
m_menu_copy_content->signal_activate().connect(sigc::mem_fun(*this, &ChatMessageItemContainer::on_menu_copy_content));
m_menu.append(*m_menu_copy_content);
m_menu_reply_to = Gtk::manage(new Gtk::MenuItem("Reply To"));
m_menu_reply_to->signal_activate().connect(sigc::mem_fun(*this, &ChatMessageItemContainer::on_menu_reply_to));
m_menu.append(*m_menu_reply_to);
m_menu.show_all();
m_link_menu_copy = Gtk::manage(new Gtk::MenuItem("Copy Link"));
m_link_menu_copy->signal_activate().connect(sigc::mem_fun(*this, &ChatMessageItemContainer::on_link_menu_copy));
@@ -22,65 +43,68 @@ ChatMessageItemContainer::ChatMessageItemContainer()
m_link_menu.show_all();
}
ChatMessageItemContainer *ChatMessageItemContainer::FromMessage(const Message &data) {
ChatMessageItemContainer *ChatMessageItemContainer::FromMessage(Snowflake id) {
const auto data = Abaddon::Get().GetDiscordClient().GetMessage(id);
if (!data.has_value()) return nullptr;
auto *container = Gtk::manage(new ChatMessageItemContainer);
container->ID = data.ID;
container->ChannelID = data.ChannelID;
container->ID = data->ID;
container->ChannelID = data->ChannelID;
if (data.Nonce.has_value())
container->Nonce = *data.Nonce;
if (data->Nonce.has_value())
container->Nonce = *data->Nonce;
if (!data.Content.empty() || data.Type != MessageType::DEFAULT) {
container->m_text_component = container->CreateTextComponent(data);
if (data->Content.size() > 0 || data->Type != MessageType::DEFAULT) {
container->m_text_component = container->CreateTextComponent(&*data);
container->AttachEventHandlers(*container->m_text_component);
container->m_main.add(*container->m_text_component);
container->m_main->add(*container->m_text_component);
}
if ((data.MessageReference.has_value() || data.Interaction.has_value()) && data.Type != MessageType::CHANNEL_FOLLOW_ADD) {
auto *widget = container->CreateReplyComponent(data);
if (widget != nullptr) {
container->m_main.add(*widget);
container->m_main.child_property_position(*widget) = 0; // eek
if ((data->MessageReference.has_value() || data->Interaction.has_value()) && data->Type != MessageType::CHANNEL_FOLLOW_ADD) {
auto *widget = container->CreateReplyComponent(*data);
container->m_main->add(*widget);
container->m_main->child_property_position(*widget) = 0; // eek
}
// there should only ever be 1 embed (i think?)
if (data->Embeds.size() == 1) {
const auto &embed = data->Embeds[0];
if (IsEmbedImageOnly(embed)) {
auto *widget = container->CreateImageComponent(*embed.Thumbnail->ProxyURL, *embed.Thumbnail->URL, *embed.Thumbnail->Width, *embed.Thumbnail->Height);
container->AttachEventHandlers(*widget);
container->m_main->add(*widget);
} else {
container->m_embed_component = container->CreateEmbedComponent(embed);
container->AttachEventHandlers(*container->m_embed_component);
container->m_main->add(*container->m_embed_component);
}
}
if (!data.Embeds.empty()) {
container->m_embed_component = container->CreateEmbedsComponent(data.Embeds);
container->m_main.add(*container->m_embed_component);
}
// i dont think attachments can be edited
// also this can definitely be done much better holy shit
for (const auto &a : data.Attachments) {
for (const auto &a : data->Attachments) {
if (IsURLViewableImage(a.ProxyURL) && a.Width.has_value() && a.Height.has_value()) {
auto *widget = container->CreateImageComponent(a.ProxyURL, a.URL, *a.Width, *a.Height);
container->m_main.add(*widget);
container->m_main->add(*widget);
} else {
auto *widget = container->CreateAttachmentComponent(a);
container->m_main.add(*widget);
container->m_main->add(*widget);
}
}
// only 1?
/*
DEPRECATED
if (data.Stickers.has_value()) {
const auto &sticker = data.Stickers.value()[0];
if (data->Stickers.has_value()) {
const auto &sticker = data->Stickers.value()[0];
// todo: lottie, proper apng
if (sticker.FormatType == StickerFormatType::PNG || sticker.FormatType == StickerFormatType::APNG) {
auto *widget = container->CreateStickerComponent(sticker);
container->m_main->add(*widget);
}
}*/
if (data.StickerItems.has_value()) {
auto *widget = container->CreateStickersComponent(*data.StickerItems);
container->m_main.add(*widget);
}
if (data.Reactions.has_value() && !data.Reactions->empty()) {
container->m_reactions_component = container->CreateReactionsComponent(data);
container->m_main.add(*container->m_reactions_component);
if (data->Reactions.has_value() && data->Reactions->size() > 0) {
container->m_reactions_component = container->CreateReactionsComponent(*data);
container->m_main->add(*container->m_reactions_component);
}
container->UpdateAttributes();
@@ -99,33 +123,28 @@ void ChatMessageItemContainer::UpdateContent() {
m_embed_component = nullptr;
}
if (!data->Embeds.empty()) {
m_embed_component = CreateEmbedsComponent(data->Embeds);
if (data->Embeds.size() == 1) {
m_embed_component = CreateEmbedComponent(data->Embeds[0]);
AttachEventHandlers(*m_embed_component);
m_main.add(*m_embed_component);
m_embed_component->show_all();
m_main->add(*m_embed_component);
}
}
void ChatMessageItemContainer::UpdateReactions() {
if (m_reactions_component != nullptr) {
if (m_reactions_component != nullptr)
delete m_reactions_component;
m_reactions_component = nullptr;
}
const auto data = Abaddon::Get().GetDiscordClient().GetMessage(ID);
if (data->Reactions.has_value() && !data->Reactions->empty()) {
if (data->Reactions.has_value() && data->Reactions->size() > 0) {
m_reactions_component = CreateReactionsComponent(*data);
m_reactions_component->show_all();
m_main.add(*m_reactions_component);
m_main->add(*m_reactions_component);
}
}
void ChatMessageItemContainer::SetFailed() {
if (m_text_component != nullptr) {
m_text_component->get_style_context()->remove_class("pending");
m_text_component->get_style_context()->add_class("failed");
}
m_text_component->get_style_context()->remove_class("pending");
m_text_component->get_style_context()->add_class("failed");
}
void ChatMessageItemContainer::UpdateAttributes() {
@@ -141,7 +160,7 @@ void ChatMessageItemContainer::UpdateAttributes() {
m_attrib_label = Gtk::manage(new Gtk::Label);
m_attrib_label->set_halign(Gtk::ALIGN_START);
m_attrib_label->show();
m_main.add(*m_attrib_label); // todo: maybe insert markup into existing text widget's buffer if the circumstances are right (or pack horizontally)
m_main->add(*m_attrib_label); // todo: maybe insert markup into existing text widget's buffer if the circumstances are right (or pack horizontally)
}
if (deleted)
@@ -150,7 +169,7 @@ void ChatMessageItemContainer::UpdateAttributes() {
m_attrib_label->set_markup("<span color='#999999'>[edited]</span>");
}
void ChatMessageItemContainer::AddClickHandler(Gtk::Widget *widget, const std::string &url) {
void ChatMessageItemContainer::AddClickHandler(Gtk::Widget *widget, std::string url) {
// clang-format off
widget->signal_button_press_event().connect([url](GdkEventButton *event) -> bool {
if (event->type == GDK_BUTTON_PRESS && event->button == GDK_BUTTON_PRIMARY) {
@@ -162,10 +181,10 @@ void ChatMessageItemContainer::AddClickHandler(Gtk::Widget *widget, const std::s
// clang-format on
}
Gtk::TextView *ChatMessageItemContainer::CreateTextComponent(const Message &data) {
Gtk::TextView *ChatMessageItemContainer::CreateTextComponent(const Message *data) {
auto *tv = Gtk::manage(new Gtk::TextView);
if (data.IsPending)
if (data->IsPending)
tv->get_style_context()->add_class("pending");
tv->get_style_context()->add_class("message-text");
tv->set_can_focus(false);
@@ -191,7 +210,6 @@ void ChatMessageItemContainer::UpdateTextComponent(Gtk::TextView *tv) {
case MessageType::DEFAULT:
case MessageType::INLINE_REPLY:
b->insert(s, data->Content);
HandleRoleMentions(b);
HandleUserMentions(b);
HandleLinks(*tv);
HandleChannelMentions(tv);
@@ -224,13 +242,11 @@ void ChatMessageItemContainer::UpdateTextComponent(Gtk::TextView *tv) {
}
} break;
case MessageType::RECIPIENT_ADD: {
if (data->Mentions.empty()) break;
const auto &adder = Abaddon::Get().GetDiscordClient().GetUser(data->Author.ID);
const auto &added = data->Mentions[0];
b->insert_markup(s, "<i><span color='#999999'><span color='#eeeeee'>" + adder->Username + "</span> added <span color='#eeeeee'>" + added.Username + "</span></span></i>");
} break;
case MessageType::RECIPIENT_REMOVE: {
if (data->Mentions.empty()) break;
const auto &adder = Abaddon::Get().GetDiscordClient().GetUser(data->Author.ID);
const auto &added = data->Mentions[0];
if (adder->ID == added.ID)
@@ -273,42 +289,10 @@ void ChatMessageItemContainer::UpdateTextComponent(Gtk::TextView *tv) {
case MessageType::GUILD_DISCOVERY_GRACE_PERIOD_FINAL_WARNING: {
b->insert_markup(s, "<i><span color='#999999'>This server has failed Discovery activity requirements for 3 weeks in a row. If this server fails for 1 more week, it will be removed from Discovery.</span></i>");
} break;
case MessageType::THREAD_CREATED: {
const auto author = Abaddon::Get().GetDiscordClient().GetUser(data->Author.ID);
if (data->MessageReference.has_value() && data->MessageReference->ChannelID.has_value()) {
auto iter = b->insert_markup(s, "<i><span color='#999999'>" + author->GetEscapedBoldName() + " started a thread: </span></i>");
auto tag = b->create_tag();
tag->property_weight() = Pango::WEIGHT_BOLD;
m_channel_tagmap[tag] = *data->MessageReference->ChannelID;
b->insert_with_tag(iter, data->Content, tag);
tv->signal_button_press_event().connect(sigc::mem_fun(*this, &ChatMessageItemContainer::OnClickChannel), false);
} else {
b->insert_markup(s, "<i><span color='#999999'>" + author->GetEscapedBoldName() + " started a thread: </span><b>" + Glib::Markup::escape_text(data->Content) + "</b></i>");
}
} break;
default: break;
}
}
Gtk::Widget *ChatMessageItemContainer::CreateEmbedsComponent(const std::vector<EmbedData> &embeds) {
auto *box = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_VERTICAL));
for (const auto &embed : embeds) {
if (IsEmbedImageOnly(embed)) {
auto *widget = CreateImageComponent(*embed.Thumbnail->ProxyURL, *embed.Thumbnail->URL, *embed.Thumbnail->Width, *embed.Thumbnail->Height);
widget->show();
AttachEventHandlers(*widget);
box->add(*widget);
} else {
auto *widget = CreateEmbedComponent(embed);
widget->show();
AttachEventHandlers(*widget);
box->add(*widget);
}
}
return box;
}
Gtk::Widget *ChatMessageItemContainer::CreateEmbedComponent(const EmbedData &embed) {
Gtk::EventBox *ev = Gtk::manage(new Gtk::EventBox);
ev->set_can_focus(true);
@@ -321,6 +305,8 @@ Gtk::Widget *ChatMessageItemContainer::CreateEmbedComponent(const EmbedData &emb
constexpr static int AuthorIconSize = 20;
if (embed.Author->ProxyIconURL.has_value()) {
auto &img = Abaddon::Get().GetImageManager();
auto *author_img = Gtk::manage(new LazyImage(*embed.Author->ProxyIconURL, AuthorIconSize, AuthorIconSize));
author_img->set_halign(Gtk::ALIGN_START);
author_img->set_valign(Gtk::ALIGN_START);
@@ -361,14 +347,14 @@ Gtk::Widget *ChatMessageItemContainer::CreateEmbedComponent(const EmbedData &emb
if (embed.URL.has_value()) {
AddPointerCursor(*title_ev);
auto url = *embed.URL;
title_ev->signal_button_press_event().connect([url = std::move(url)](GdkEventButton *event) -> bool {
title_ev->signal_button_press_event().connect([this, url = std::move(url)](GdkEventButton *event) -> bool {
if (event->button == GDK_BUTTON_PRIMARY) {
LaunchBrowser(url);
return true;
}
return false;
});
static auto color = Abaddon::Get().GetSettings().LinkColor;
static auto color = Abaddon::Get().GetSettings().GetLinkColor();
title_label->override_color(Gdk::RGBA(color));
title_label->set_markup("<b>" + Glib::Markup::escape_text(*embed.Title) + "</b>");
}
@@ -389,7 +375,7 @@ Gtk::Widget *ChatMessageItemContainer::CreateEmbedComponent(const EmbedData &emb
}
// todo: handle inline fields
if (embed.Fields.has_value() && !embed.Fields->empty()) {
if (embed.Fields.has_value() && embed.Fields->size() > 0) {
auto *flow = Gtk::manage(new Gtk::FlowBox);
flow->set_orientation(Gtk::ORIENTATION_HORIZONTAL);
flow->set_min_children_per_line(3);
@@ -516,24 +502,19 @@ Gtk::Widget *ChatMessageItemContainer::CreateAttachmentComponent(const Attachmen
return ev;
}
Gtk::Widget *ChatMessageItemContainer::CreateStickersComponent(const std::vector<StickerItem> &data) {
auto *box = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_VERTICAL));
Gtk::Widget *ChatMessageItemContainer::CreateStickerComponent(const StickerData &data) {
auto *box = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL));
auto *imgw = Gtk::manage(new Gtk::Image);
box->add(*imgw);
auto &img = Abaddon::Get().GetImageManager();
for (const auto &sticker : data) {
// no lottie
if (sticker.FormatType != StickerFormatType::PNG && sticker.FormatType != StickerFormatType::APNG) continue;
auto *ev = Gtk::manage(new Gtk::EventBox);
auto *img = Gtk::manage(new LazyImage(sticker.GetURL(), StickerComponentSize, StickerComponentSize, false));
img->set_halign(Gtk::ALIGN_START);
img->set_size_request(StickerComponentSize, StickerComponentSize); // should this go in LazyImage ?
img->show();
ev->show();
ev->add(*img);
box->add(*ev);
if (data.FormatType == StickerFormatType::PNG || data.FormatType == StickerFormatType::APNG) {
auto cb = [this, imgw](const Glib::RefPtr<Gdk::Pixbuf> &pixbuf) {
imgw->property_pixbuf() = pixbuf;
};
img.LoadFromURL(data.GetURL(), sigc::track_obj(cb, *imgw));
}
box->show();
AttachEventHandlers(*box);
return box;
}
@@ -591,7 +572,7 @@ Gtk::Widget *ChatMessageItemContainer::CreateReactionsComponent(const Message &d
// image
if (is_stock) { // unicode/stock
const auto shortcode = emojis.GetShortCodeForPattern(reaction.Emoji.Name);
if (!shortcode.empty())
if (shortcode != "")
ev->set_tooltip_text(shortcode);
const auto &pb = emojis.GetPixBuf(reaction.Emoji.Name);
@@ -620,8 +601,6 @@ Gtk::Widget *ChatMessageItemContainer::CreateReactionsComponent(const Message &d
}
Gtk::Widget *ChatMessageItemContainer::CreateReplyComponent(const Message &data) {
if (data.Type == MessageType::THREAD_CREATED) return nullptr;
auto *box = Gtk::manage(new Gtk::Box);
auto *lbl = Gtk::manage(new Gtk::Label);
lbl->set_single_line_mode(true);
@@ -650,14 +629,6 @@ Gtk::Widget *ChatMessageItemContainer::CreateReplyComponent(const Message &data)
return author->GetEscapedBoldString<false>();
};
// if the message wasnt fetched from store it might have an un-fetched reference
std::optional<std::shared_ptr<Message>> referenced_message = data.ReferencedMessage;
if (data.MessageReference.has_value() && data.MessageReference->MessageID.has_value() && !referenced_message.has_value()) {
auto refd = discord.GetMessage(*data.MessageReference->MessageID);
if (refd.has_value())
referenced_message = std::make_shared<Message>(std::move(*refd));
}
if (data.Interaction.has_value()) {
const auto user = *discord.GetUser(data.Interaction->User.ID);
@@ -669,16 +640,16 @@ Gtk::Widget *ChatMessageItemContainer::CreateReplyComponent(const Message &data)
} else {
lbl->set_markup(user.GetEscapedBoldString<false>());
}
} else if (referenced_message.has_value()) {
if (referenced_message.value() == nullptr) {
} else if (data.ReferencedMessage.has_value()) {
if (data.ReferencedMessage.value().get() == nullptr) {
lbl->set_markup("<i>deleted message</i>");
} else {
const auto &referenced = *referenced_message.value();
const auto &referenced = *data.ReferencedMessage.value().get();
Glib::ustring text;
if (referenced.Content.empty()) {
if (!referenced.Attachments.empty()) {
if (referenced.Content == "") {
if (referenced.Attachments.size() > 0) {
text = "<i>attachment</i>";
} else if (!referenced.Embeds.empty()) {
} else if (referenced.Embeds.size() > 0) {
text = "<i>embed</i>";
}
} else {
@@ -697,10 +668,7 @@ Gtk::Widget *ChatMessageItemContainer::CreateReplyComponent(const Message &data)
// which of course would not be an issue if i could figure out how to get fonts to work on this god-forsaken framework
// oh well
// but ill manually get colors for the user who is being replied to
if (referenced.GuildID.has_value())
lbl->set_markup(get_author_markup(referenced.Author.ID, *referenced.GuildID) + ": " + text);
else
lbl->set_markup(get_author_markup(referenced.Author.ID) + ": " + text);
lbl->set_markup(get_author_markup(referenced.Author.ID, *referenced.GuildID) + ": " + text);
}
} else {
lbl->set_markup("<i>reply unavailable</i>");
@@ -727,47 +695,7 @@ bool ChatMessageItemContainer::IsEmbedImageOnly(const EmbedData &data) {
return data.Thumbnail->ProxyURL.has_value() && data.Thumbnail->URL.has_value() && data.Thumbnail->Width.has_value() && data.Thumbnail->Height.has_value();
}
void ChatMessageItemContainer::HandleRoleMentions(const Glib::RefPtr<Gtk::TextBuffer> &buf) {
constexpr static const auto mentions_regex = R"(<@&(\d+)>)";
static auto rgx = Glib::Regex::create(mentions_regex);
Glib::ustring text = GetText(buf);
const auto &discord = Abaddon::Get().GetDiscordClient();
int startpos = 0;
Glib::MatchInfo match;
while (rgx->match(text, startpos, match)) {
int mstart, mend;
if (!match.fetch_pos(0, mstart, mend)) break;
const Glib::ustring role_id = match.fetch(1);
const auto role = discord.GetRole(role_id);
if (!role.has_value()) {
startpos = mend;
continue;
}
Glib::ustring replacement;
if (role->HasColor()) {
replacement = "<b><span color=\"#" + IntToCSSColor(role->Color) + "\">@" + role->GetEscapedName() + "</span></b>";
} else {
replacement = "<b>@" + role->GetEscapedName() + "</b>";
}
const auto chars_start = g_utf8_pointer_to_offset(text.c_str(), text.c_str() + mstart);
const auto chars_end = g_utf8_pointer_to_offset(text.c_str(), text.c_str() + mend);
const auto start_it = buf->get_iter_at_offset(chars_start);
const auto end_it = buf->get_iter_at_offset(chars_end);
auto it = buf->erase(start_it, end_it);
buf->insert_markup(it, replacement);
text = GetText(buf);
startpos = 0;
}
}
void ChatMessageItemContainer::HandleUserMentions(const Glib::RefPtr<Gtk::TextBuffer> &buf) const {
void ChatMessageItemContainer::HandleUserMentions(Glib::RefPtr<Gtk::TextBuffer> buf) {
constexpr static const auto mentions_regex = R"(<@!?(\d+)>)";
static auto rgx = Glib::Regex::create(mentions_regex);
@@ -833,6 +761,7 @@ void ChatMessageItemContainer::HandleCustomEmojis(Gtk::TextView &tv) {
int mstart, mend;
if (!match.fetch_pos(0, mstart, mend)) break;
const bool is_animated = match.fetch(0)[1] == 'a';
const bool show_animations = Abaddon::Get().GetSettings().GetShowAnimations();
const auto chars_start = g_utf8_pointer_to_offset(text.c_str(), text.c_str() + mstart);
const auto chars_end = g_utf8_pointer_to_offset(text.c_str(), text.c_str() + mend);
@@ -840,11 +769,11 @@ void ChatMessageItemContainer::HandleCustomEmojis(Gtk::TextView &tv) {
auto end_it = buf->get_iter_at_offset(chars_end);
startpos = mend;
if (is_animated && Abaddon::Get().GetSettings().ShowAnimations) {
if (is_animated && show_animations) {
const auto mark_start = buf->create_mark(start_it, false);
end_it.backward_char();
const auto mark_end = buf->create_mark(end_it, false);
const auto cb = [&tv, buf, mark_start, mark_end](const Glib::RefPtr<Gdk::PixbufAnimation> &pixbuf) {
const auto cb = [this, &tv, buf, mark_start, mark_end](const Glib::RefPtr<Gdk::PixbufAnimation> &pixbuf) {
auto start_it = mark_start->get_iter();
auto end_it = mark_end->get_iter();
end_it.forward_char();
@@ -862,16 +791,14 @@ void ChatMessageItemContainer::HandleCustomEmojis(Gtk::TextView &tv) {
const auto mark_start = buf->create_mark(start_it, false);
end_it.backward_char();
const auto mark_end = buf->create_mark(end_it, false);
const auto cb = [buf, mark_start, mark_end](const Glib::RefPtr<Gdk::Pixbuf> &pixbuf) {
const auto cb = [this, buf, mark_start, mark_end](const Glib::RefPtr<Gdk::Pixbuf> &pixbuf) {
auto start_it = mark_start->get_iter();
auto end_it = mark_end->get_iter();
end_it.forward_char();
buf->delete_mark(mark_start);
buf->delete_mark(mark_end);
auto it = buf->erase(start_it, end_it);
int width, height;
GetImageDimensions(pixbuf->get_width(), pixbuf->get_height(), width, height, EmojiSize, EmojiSize);
buf->insert_pixbuf(it, pixbuf->scale_simple(width, height, Gdk::INTERP_BILINEAR));
buf->insert_pixbuf(it, pixbuf->scale_simple(EmojiSize, EmojiSize, Gdk::INTERP_BILINEAR));
};
img.LoadFromURL(EmojiData::URLFromID(match.fetch(2)), sigc::track_obj(cb, tv));
}
@@ -881,11 +808,14 @@ void ChatMessageItemContainer::HandleCustomEmojis(Gtk::TextView &tv) {
}
void ChatMessageItemContainer::HandleEmojis(Gtk::TextView &tv) {
if (Abaddon::Get().GetSettings().ShowStockEmojis) HandleStockEmojis(tv);
if (Abaddon::Get().GetSettings().ShowCustomEmojis) HandleCustomEmojis(tv);
static bool emojis = Abaddon::Get().GetSettings().GetShowEmojis();
if (emojis) {
HandleStockEmojis(tv);
HandleCustomEmojis(tv);
}
}
void ChatMessageItemContainer::CleanupEmojis(const Glib::RefPtr<Gtk::TextBuffer> &buf) {
void ChatMessageItemContainer::CleanupEmojis(Glib::RefPtr<Gtk::TextBuffer> buf) {
static auto rgx = Glib::Regex::create(R"(<a?:([\w\d_]+):(\d+)>)");
auto text = GetText(buf);
@@ -905,9 +835,9 @@ void ChatMessageItemContainer::CleanupEmojis(const Glib::RefPtr<Gtk::TextBuffer>
startpos = mend;
const auto it = buf->erase(start_it, end_it);
const int alen = static_cast<int>(text.size());
const int alen = text.size();
text = GetText(buf);
const int blen = static_cast<int>(text.size());
const int blen = text.size();
startpos -= (alen - blen);
buf->insert(it, new_term);
@@ -916,7 +846,7 @@ void ChatMessageItemContainer::CleanupEmojis(const Glib::RefPtr<Gtk::TextBuffer>
}
}
void ChatMessageItemContainer::HandleChannelMentions(const Glib::RefPtr<Gtk::TextBuffer> &buf) {
void ChatMessageItemContainer::HandleChannelMentions(Glib::RefPtr<Gtk::TextBuffer> buf) {
static auto rgx = Glib::Regex::create(R"(<#(\d+)>)");
Glib::ustring text = GetText(buf);
@@ -936,10 +866,8 @@ void ChatMessageItemContainer::HandleChannelMentions(const Glib::RefPtr<Gtk::Tex
}
auto tag = buf->create_tag();
if (chan->Type == ChannelType::GUILD_TEXT) {
m_channel_tagmap[tag] = channel_id;
tag->property_weight() = Pango::WEIGHT_BOLD;
}
m_channel_tagmap[tag] = channel_id;
tag->property_weight() = Pango::WEIGHT_BOLD;
const auto chars_start = g_utf8_pointer_to_offset(text.c_str(), text.c_str() + mstart);
const auto chars_end = g_utf8_pointer_to_offset(text.c_str(), text.c_str() + mend);
@@ -973,12 +901,12 @@ bool ChatMessageItemContainer::OnClickChannel(GdkEventButton *ev) {
return false;
int x, y;
m_text_component->window_to_buffer_coords(Gtk::TEXT_WINDOW_WIDGET, static_cast<int>(ev->x), static_cast<int>(ev->y), x, y);
m_text_component->window_to_buffer_coords(Gtk::TEXT_WINDOW_WIDGET, ev->x, ev->y, x, y);
Gtk::TextBuffer::iterator iter;
m_text_component->get_iter_at_location(iter, x, y);
const auto tags = iter.get_tags();
for (const auto &tag : tags) {
for (auto tag : tags) {
const auto it = m_channel_tagmap.find(tag);
if (it != m_channel_tagmap.end()) {
m_signal_action_channel_click.emit(it->second);
@@ -1002,6 +930,9 @@ void ChatMessageItemContainer::HandleLinks(Gtk::TextView &tv) {
auto buf = tv.get_buffer();
Glib::ustring text = GetText(buf);
// i'd like to let this be done thru css like .message-link { color: #bitch; } but idk how
static auto link_color = Abaddon::Get().GetSettings().GetLinkColor();
int startpos = 0;
Glib::MatchInfo match;
while (rgx->match(text, startpos, match)) {
@@ -1010,7 +941,7 @@ void ChatMessageItemContainer::HandleLinks(Gtk::TextView &tv) {
std::string link = match.fetch(0);
auto tag = buf->create_tag();
m_link_tagmap[tag] = link;
tag->property_foreground_rgba() = Gdk::RGBA(Abaddon::Get().GetSettings().LinkColor);
tag->property_foreground_rgba() = Gdk::RGBA(link_color);
tag->set_property("underline", 1); // stupid workaround for vcpkg bug (i think)
const auto chars_start = g_utf8_pointer_to_offset(text.c_str(), text.c_str() + mstart);
@@ -1036,12 +967,12 @@ bool ChatMessageItemContainer::OnLinkClick(GdkEventButton *ev) {
return false;
int x, y;
m_text_component->window_to_buffer_coords(Gtk::TEXT_WINDOW_WIDGET, static_cast<int>(ev->x), static_cast<int>(ev->y), x, y);
m_text_component->window_to_buffer_coords(Gtk::TEXT_WINDOW_WIDGET, ev->x, ev->y, x, y);
Gtk::TextBuffer::iterator iter;
m_text_component->get_iter_at_location(iter, x, y);
const auto tags = iter.get_tags();
for (const auto &tag : tags) {
for (auto tag : tags) {
const auto it = m_link_tagmap.find(tag);
if (it != m_link_tagmap.end()) {
if (ev->button == GDK_BUTTON_PRIMARY) {
@@ -1058,6 +989,52 @@ bool ChatMessageItemContainer::OnLinkClick(GdkEventButton *ev) {
return false;
}
void ChatMessageItemContainer::ShowMenu(GdkEvent *event) {
const auto &client = Abaddon::Get().GetDiscordClient();
const auto data = client.GetMessage(ID);
if (data->IsDeleted()) {
m_menu_delete_message->set_sensitive(false);
m_menu_edit_message->set_sensitive(false);
} else {
const bool can_edit = client.GetUserData().ID == data->Author.ID;
const bool can_delete = can_edit || client.HasChannelPermission(client.GetUserData().ID, ChannelID, Permission::MANAGE_MESSAGES);
m_menu_delete_message->set_sensitive(can_delete);
m_menu_edit_message->set_sensitive(can_edit);
}
m_menu.popup_at_pointer(event);
}
void ChatMessageItemContainer::on_menu_copy_id() {
Gtk::Clipboard::get()->set_text(std::to_string(ID));
}
void ChatMessageItemContainer::on_menu_delete_message() {
m_signal_action_delete.emit();
}
void ChatMessageItemContainer::on_menu_edit_message() {
m_signal_action_edit.emit();
}
void ChatMessageItemContainer::on_menu_copy_content() {
const auto msg = Abaddon::Get().GetDiscordClient().GetMessage(ID);
if (msg.has_value())
Gtk::Clipboard::get()->set_text(msg->Content);
}
void ChatMessageItemContainer::on_menu_reply_to() {
m_signal_action_reply_to.emit(ID);
}
ChatMessageItemContainer::type_signal_action_delete ChatMessageItemContainer::signal_action_delete() {
return m_signal_action_delete;
}
ChatMessageItemContainer::type_signal_action_edit ChatMessageItemContainer::signal_action_edit() {
return m_signal_action_edit;
}
ChatMessageItemContainer::type_signal_channel_click ChatMessageItemContainer::signal_action_channel_click() {
return m_signal_action_channel_click;
}
@@ -1070,10 +1047,14 @@ ChatMessageItemContainer::type_signal_action_reaction_remove ChatMessageItemCont
return m_signal_action_reaction_remove;
}
ChatMessageItemContainer::type_signal_action_reply_to ChatMessageItemContainer::signal_action_reply_to() {
return m_signal_action_reply_to;
}
void ChatMessageItemContainer::AttachEventHandlers(Gtk::Widget &widget) {
const auto on_button_press_event = [this](GdkEventButton *e) -> bool {
if (e->type == GDK_BUTTON_PRESS && e->button == GDK_BUTTON_SECONDARY) {
event(reinterpret_cast<GdkEvent *>(e)); // illegal ooooooh
const auto on_button_press_event = [this](GdkEventButton *event) -> bool {
if (event->type == GDK_BUTTON_PRESS && event->button == GDK_BUTTON_SECONDARY) {
ShowMenu(reinterpret_cast<GdkEvent *>(event));
return true;
}
@@ -1082,47 +1063,54 @@ void ChatMessageItemContainer::AttachEventHandlers(Gtk::Widget &widget) {
widget.signal_button_press_event().connect(on_button_press_event, false);
}
ChatMessageHeader::ChatMessageHeader(const Message &data)
: m_main_box(Gtk::ORIENTATION_HORIZONTAL)
, m_content_box(Gtk::ORIENTATION_VERTICAL)
, m_meta_box(Gtk::ORIENTATION_HORIZONTAL)
, m_avatar(Abaddon::Get().GetImageManager().GetPlaceholder(AvatarSize)) {
UserID = data.Author.ID;
ChannelID = data.ChannelID;
ChatMessageHeader::ChatMessageHeader(const Message *data) {
UserID = data->Author.ID;
ChannelID = data->ChannelID;
m_main_box = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL));
m_content_box = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_VERTICAL));
m_content_box_ev = Gtk::manage(new Gtk::EventBox);
m_meta_box = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL));
m_meta_ev = Gtk::manage(new Gtk::EventBox);
m_author = Gtk::manage(new Gtk::Label);
m_timestamp = Gtk::manage(new Gtk::Label);
m_avatar_ev = Gtk::manage(new Gtk::EventBox);
const auto author = Abaddon::Get().GetDiscordClient().GetUser(UserID);
auto &img = Abaddon::Get().GetImageManager();
m_avatar = Gtk::manage(new Gtk::Image(img.GetPlaceholder(AvatarSize)));
auto cb = [this](const Glib::RefPtr<Gdk::Pixbuf> &pb) {
m_static_avatar = pb->scale_simple(AvatarSize, AvatarSize, Gdk::INTERP_BILINEAR);
m_avatar.property_pixbuf() = m_static_avatar;
m_avatar->property_pixbuf() = m_static_avatar;
};
img.LoadFromURL(author->GetAvatarURL(data.GuildID), sigc::track_obj(cb, *this));
img.LoadFromURL(author->GetAvatarURL(), sigc::track_obj(cb, *this));
if (author->HasAnimatedAvatar(data.GuildID)) {
if (author->HasAnimatedAvatar()) {
auto cb = [this](const Glib::RefPtr<Gdk::PixbufAnimation> &pb) {
m_anim_avatar = pb;
};
img.LoadAnimationFromURL(author->GetAvatarURL(data.GuildID, "gif"), AvatarSize, AvatarSize, sigc::track_obj(cb, *this));
img.LoadAnimationFromURL(author->GetAvatarURL("gif"), AvatarSize, AvatarSize, sigc::track_obj(cb, *this));
}
get_style_context()->add_class("message-container");
m_author.get_style_context()->add_class("message-container-author");
m_timestamp.get_style_context()->add_class("message-container-timestamp");
m_avatar.get_style_context()->add_class("message-container-avatar");
m_author->get_style_context()->add_class("message-container-author");
m_timestamp->get_style_context()->add_class("message-container-timestamp");
m_avatar->get_style_context()->add_class("message-container-avatar");
m_avatar.set_valign(Gtk::ALIGN_START);
m_avatar.set_margin_right(10);
m_avatar->set_valign(Gtk::ALIGN_START);
m_avatar->set_margin_right(10);
m_author.set_single_line_mode(true);
m_author.set_line_wrap(false);
m_author.set_ellipsize(Pango::ELLIPSIZE_END);
m_author.set_xalign(0.0F);
m_author.set_can_focus(false);
m_author->set_markup(data->Author.GetEscapedBoldName());
m_author->set_single_line_mode(true);
m_author->set_line_wrap(false);
m_author->set_ellipsize(Pango::ELLIPSIZE_END);
m_author->set_xalign(0.f);
m_author->set_can_focus(false);
m_meta_ev.signal_button_press_event().connect(sigc::mem_fun(*this, &ChatMessageHeader::on_author_button_press));
m_meta_ev->signal_button_press_event().connect(sigc::mem_fun(*this, &ChatMessageHeader::on_author_button_press));
if (author->IsBot || data.WebhookID.has_value()) {
if (author->IsBot || data->WebhookID.has_value()) {
m_extra = Gtk::manage(new Gtk::Label);
m_extra->get_style_context()->add_class("message-container-extra");
m_extra->set_single_line_mode(true);
@@ -1132,96 +1120,92 @@ ChatMessageHeader::ChatMessageHeader(const Message &data)
}
if (author->IsBot)
m_extra->set_markup("<b>BOT</b>");
else if (data.WebhookID.has_value())
else if (data->WebhookID.has_value())
m_extra->set_markup("<b>Webhook</b>");
m_timestamp.set_text(data.ID.GetLocalTimestamp());
m_timestamp.set_hexpand(true);
m_timestamp.set_halign(Gtk::ALIGN_END);
m_timestamp.set_ellipsize(Pango::ELLIPSIZE_END);
m_timestamp.set_opacity(0.5);
m_timestamp.set_single_line_mode(true);
m_timestamp.set_margin_start(12);
m_timestamp.set_can_focus(false);
m_timestamp->set_text(data->ID.GetLocalTimestamp());
m_timestamp->set_hexpand(true);
m_timestamp->set_halign(Gtk::ALIGN_END);
m_timestamp->set_ellipsize(Pango::ELLIPSIZE_END);
m_timestamp->set_opacity(0.5);
m_timestamp->set_single_line_mode(true);
m_timestamp->set_margin_start(12);
m_timestamp->set_can_focus(false);
m_main_box.set_hexpand(true);
m_main_box.set_vexpand(true);
m_main_box.set_can_focus(true);
m_main_box->set_hexpand(true);
m_main_box->set_vexpand(true);
m_main_box->set_can_focus(true);
m_meta_box.set_hexpand(true);
m_meta_box.set_can_focus(false);
m_meta_box->set_hexpand(true);
m_meta_box->set_can_focus(false);
m_content_box.set_can_focus(false);
m_content_box->set_can_focus(false);
const auto on_enter_cb = [this](const GdkEventCrossing *event) -> bool {
if (m_anim_avatar)
m_avatar.property_pixbuf_animation() = m_anim_avatar;
m_avatar->property_pixbuf_animation() = m_anim_avatar;
return false;
};
const auto on_leave_cb = [this](const GdkEventCrossing *event) -> bool {
if (m_anim_avatar)
m_avatar.property_pixbuf() = m_static_avatar;
m_avatar->property_pixbuf() = m_static_avatar;
return false;
};
m_content_box_ev.add_events(Gdk::ENTER_NOTIFY_MASK | Gdk::LEAVE_NOTIFY_MASK);
m_meta_ev.add_events(Gdk::ENTER_NOTIFY_MASK | Gdk::LEAVE_NOTIFY_MASK);
m_avatar_ev.add_events(Gdk::ENTER_NOTIFY_MASK | Gdk::LEAVE_NOTIFY_MASK);
if (Abaddon::Get().GetSettings().ShowAnimations) {
m_content_box_ev.signal_enter_notify_event().connect(on_enter_cb);
m_content_box_ev.signal_leave_notify_event().connect(on_leave_cb);
m_meta_ev.signal_enter_notify_event().connect(on_enter_cb);
m_meta_ev.signal_leave_notify_event().connect(on_leave_cb);
m_avatar_ev.signal_enter_notify_event().connect(on_enter_cb);
m_avatar_ev.signal_leave_notify_event().connect(on_leave_cb);
m_content_box_ev->add_events(Gdk::ENTER_NOTIFY_MASK | Gdk::LEAVE_NOTIFY_MASK);
m_meta_ev->add_events(Gdk::ENTER_NOTIFY_MASK | Gdk::LEAVE_NOTIFY_MASK);
m_avatar_ev->add_events(Gdk::ENTER_NOTIFY_MASK | Gdk::LEAVE_NOTIFY_MASK);
if (Abaddon::Get().GetSettings().GetShowAnimations()) {
m_content_box_ev->signal_enter_notify_event().connect(on_enter_cb);
m_content_box_ev->signal_leave_notify_event().connect(on_leave_cb);
m_meta_ev->signal_enter_notify_event().connect(on_enter_cb);
m_meta_ev->signal_leave_notify_event().connect(on_leave_cb);
m_avatar_ev->signal_enter_notify_event().connect(on_enter_cb);
m_avatar_ev->signal_leave_notify_event().connect(on_leave_cb);
}
m_meta_box.add(m_author);
m_meta_box->add(*m_author);
if (m_extra != nullptr)
m_meta_box.add(*m_extra);
m_meta_box->add(*m_extra);
m_meta_box.add(m_timestamp);
m_meta_ev.add(m_meta_box);
m_content_box.add(m_meta_ev);
m_avatar_ev.add(m_avatar);
m_main_box.add(m_avatar_ev);
m_content_box_ev.add(m_content_box);
m_main_box.add(m_content_box_ev);
add(m_main_box);
m_meta_box->add(*m_timestamp);
m_meta_ev->add(*m_meta_box);
m_content_box->add(*m_meta_ev);
m_avatar_ev->add(*m_avatar);
m_main_box->add(*m_avatar_ev);
m_content_box_ev->add(*m_content_box);
m_main_box->add(*m_content_box_ev);
add(*m_main_box);
set_margin_bottom(8);
show_all();
auto &discord = Abaddon::Get().GetDiscordClient();
auto role_update_cb = [this](...) { UpdateName(); };
auto role_update_cb = [this](...) { UpdateNameColor(); };
discord.signal_role_update().connect(sigc::track_obj(role_update_cb, *this));
auto guild_member_update_cb = [this](const auto &, const auto &) { UpdateName(); };
auto guild_member_update_cb = [this](const auto &, const auto &) { UpdateNameColor(); };
discord.signal_guild_member_update().connect(sigc::track_obj(guild_member_update_cb, *this));
UpdateName();
AttachUserMenuHandler(m_meta_ev);
AttachUserMenuHandler(m_avatar_ev);
UpdateNameColor();
AttachUserMenuHandler(*m_meta_ev);
AttachUserMenuHandler(*m_avatar_ev);
}
void ChatMessageHeader::UpdateName() {
void ChatMessageHeader::UpdateNameColor() {
const auto &discord = Abaddon::Get().GetDiscordClient();
const auto guild_id = discord.GetChannel(ChannelID)->GuildID;
const auto role_id = discord.GetMemberHoistedRole(*guild_id, UserID, true);
const auto user = discord.GetUser(UserID);
if (!user.has_value()) return;
const auto chan = discord.GetChannel(ChannelID);
bool is_guild = chan.has_value() && chan->GuildID.has_value();
if (is_guild) {
const auto member = discord.GetMember(UserID, *chan->GuildID);
const auto role_id = discord.GetMemberHoistedRole(*chan->GuildID, UserID, true);
const auto role = discord.GetRole(role_id);
const auto name = GetEscapedDisplayName(*user, member);
const auto role = discord.GetRole(role_id);
std::string md;
if (role.has_value())
m_author.set_markup("<span weight='bold' color='#" + IntToCSSColor(role->Color) + "'>" + name + "</span>");
else
m_author.set_markup("<span weight='bold'>" + name + "</span>");
} else
m_author.set_markup("<span weight='bold'>" + user->GetEscapedName() + "</span>");
std::string md;
if (role.has_value())
md = "<span weight='bold' color='#" + IntToCSSColor(role->Color) + "'>" + user->GetEscapedName() + "</span>";
else
md = "<span weight='bold'>" + user->GetEscapedName() + "</span>";
m_author->set_markup(md);
}
std::vector<Gtk::Widget *> ChatMessageHeader::GetChildContent() {
@@ -1231,11 +1215,7 @@ std::vector<Gtk::Widget *> ChatMessageHeader::GetChildContent() {
void ChatMessageHeader::AttachUserMenuHandler(Gtk::Widget &widget) {
widget.signal_button_press_event().connect([this](GdkEventButton *ev) -> bool {
if (ev->type == GDK_BUTTON_PRESS && ev->button == GDK_BUTTON_SECONDARY) {
auto info = Abaddon::Get().GetDiscordClient().GetChannel(ChannelID);
Snowflake guild_id;
if (info.has_value() && info->GuildID.has_value())
guild_id = *info->GuildID;
Abaddon::Get().ShowUserMenu(reinterpret_cast<GdkEvent *>(ev), UserID, guild_id);
m_signal_action_open_user_menu.emit(reinterpret_cast<GdkEvent *>(ev));
return true;
}
@@ -1243,13 +1223,6 @@ void ChatMessageHeader::AttachUserMenuHandler(Gtk::Widget &widget) {
});
}
Glib::ustring ChatMessageHeader::GetEscapedDisplayName(const UserData &user, const std::optional<GuildMember> &member) {
if (member.has_value() && !member->Nickname.empty())
return Glib::Markup::escape_text(member->Nickname);
else
return Glib::Markup::escape_text(user.GetEscapedName());
}
bool ChatMessageHeader::on_author_button_press(GdkEventButton *ev) {
if (ev->button == GDK_BUTTON_PRIMARY && (ev->state & GDK_SHIFT_MASK)) {
m_signal_action_insert_mention.emit();
@@ -1269,13 +1242,12 @@ ChatMessageHeader::type_signal_action_open_user_menu ChatMessageHeader::signal_a
void ChatMessageHeader::AddContent(Gtk::Widget *widget, bool prepend) {
m_content_widgets.push_back(widget);
const auto cb = [this, widget]() {
widget->signal_unmap().connect([this, widget]() {
m_content_widgets.erase(std::remove(m_content_widgets.begin(), m_content_widgets.end(), widget), m_content_widgets.end());
};
widget->signal_unmap().connect(sigc::track_obj(cb, *this, *widget), false);
m_content_box.add(*widget);
});
m_content_box->add(*widget);
if (prepend)
m_content_box.reorder_child(*widget, 1);
m_content_box->reorder_child(*widget, 1);
if (auto *x = dynamic_cast<ChatMessageItemContainer *>(widget)) {
if (x->ID > NewestID)
NewestID = x->ID;

View File

@@ -1,6 +1,6 @@
#pragma once
#include <gtkmm.h>
#include "discord/discord.hpp"
#include "../discord/discord.hpp"
class ChatMessageItemContainer : public Gtk::Box {
public:
@@ -10,7 +10,7 @@ public:
std::string Nonce;
ChatMessageItemContainer();
static ChatMessageItemContainer *FromMessage(const Message &data);
static ChatMessageItemContainer *FromMessage(Snowflake id);
// attributes = edited, deleted
void UpdateAttributes();
@@ -19,14 +19,13 @@ public:
void SetFailed();
protected:
static void AddClickHandler(Gtk::Widget *widget, const std::string &);
Gtk::TextView *CreateTextComponent(const Message &data); // Message.Content
void AddClickHandler(Gtk::Widget *widget, std::string);
Gtk::TextView *CreateTextComponent(const Message *data); // Message.Content
void UpdateTextComponent(Gtk::TextView *tv);
Gtk::Widget *CreateEmbedsComponent(const std::vector<EmbedData> &embeds);
static Gtk::Widget *CreateEmbedComponent(const EmbedData &data); // Message.Embeds[0]
Gtk::Widget *CreateEmbedComponent(const EmbedData &data); // Message.Embeds[0]
Gtk::Widget *CreateImageComponent(const std::string &proxy_url, const std::string &url, int inw, int inh);
Gtk::Widget *CreateAttachmentComponent(const AttachmentData &data); // non-image attachments
Gtk::Widget *CreateStickersComponent(const std::vector<StickerItem> &data);
Gtk::Widget *CreateStickerComponent(const StickerData &data);
Gtk::Widget *CreateReactionsComponent(const Message &data);
Gtk::Widget *CreateReplyComponent(const Message &data);
@@ -34,14 +33,13 @@ protected:
static bool IsEmbedImageOnly(const EmbedData &data);
static void HandleRoleMentions(const Glib::RefPtr<Gtk::TextBuffer> &buf);
void HandleUserMentions(const Glib::RefPtr<Gtk::TextBuffer> &buf) const;
static void HandleStockEmojis(Gtk::TextView &tv);
static void HandleCustomEmojis(Gtk::TextView &tv);
static void HandleEmojis(Gtk::TextView &tv);
static void CleanupEmojis(const Glib::RefPtr<Gtk::TextBuffer> &buf);
void HandleUserMentions(Glib::RefPtr<Gtk::TextBuffer> buf);
void HandleStockEmojis(Gtk::TextView &tv);
void HandleCustomEmojis(Gtk::TextView &tv);
void HandleEmojis(Gtk::TextView &tv);
void CleanupEmojis(Glib::RefPtr<Gtk::TextBuffer> buf);
void HandleChannelMentions(const Glib::RefPtr<Gtk::TextBuffer> &buf);
void HandleChannelMentions(Glib::RefPtr<Gtk::TextBuffer> buf);
void HandleChannelMentions(Gtk::TextView *tv);
bool OnClickChannel(GdkEventButton *ev);
@@ -58,9 +56,23 @@ protected:
std::map<Glib::RefPtr<Gtk::TextTag>, Snowflake> m_channel_tagmap;
void AttachEventHandlers(Gtk::Widget &widget);
void ShowMenu(GdkEvent *event);
Gtk::EventBox *_ev;
Gtk::Box m_main;
Gtk::Menu m_menu;
Gtk::MenuItem *m_menu_copy_id;
Gtk::MenuItem *m_menu_copy_content;
Gtk::MenuItem *m_menu_delete_message;
Gtk::MenuItem *m_menu_edit_message;
Gtk::MenuItem *m_menu_reply_to;
void on_menu_copy_id();
void on_menu_delete_message();
void on_menu_edit_message();
void on_menu_copy_content();
void on_menu_reply_to();
Gtk::EventBox *m_ev;
Gtk::Box *m_main;
Gtk::Label *m_attrib_label = nullptr;
Gtk::TextView *m_text_component = nullptr;
@@ -68,18 +80,29 @@ protected:
Gtk::Widget *m_reactions_component = nullptr;
public:
typedef sigc::signal<void> type_signal_action_delete;
typedef sigc::signal<void> type_signal_action_edit;
typedef sigc::signal<void, Snowflake> type_signal_channel_click;
typedef sigc::signal<void, Glib::ustring> type_signal_action_reaction_add;
typedef sigc::signal<void, Glib::ustring> type_signal_action_reaction_remove;
typedef sigc::signal<void, Snowflake> type_signal_action_reply_to;
typedef sigc::signal<void> type_signal_enter;
typedef sigc::signal<void> type_signal_leave;
type_signal_action_delete signal_action_delete();
type_signal_action_edit signal_action_edit();
type_signal_channel_click signal_action_channel_click();
type_signal_action_reaction_add signal_action_reaction_add();
type_signal_action_reaction_remove signal_action_reaction_remove();
type_signal_action_reply_to signal_action_reply_to();
private:
type_signal_action_delete m_signal_action_delete;
type_signal_action_edit m_signal_action_edit;
type_signal_channel_click m_signal_action_channel_click;
type_signal_action_reaction_add m_signal_action_reaction_add;
type_signal_action_reaction_remove m_signal_action_reaction_remove;
type_signal_action_reply_to m_signal_action_reply_to;
};
class ChatMessageHeader : public Gtk::ListBoxRow {
@@ -88,29 +111,28 @@ public:
Snowflake ChannelID;
Snowflake NewestID = 0;
ChatMessageHeader(const Message &data);
ChatMessageHeader(const Message *data);
void AddContent(Gtk::Widget *widget, bool prepend);
void UpdateName();
std::vector<Gtk::Widget *> GetChildContent();
void UpdateNameColor();
std::vector<Gtk::Widget*> GetChildContent();
protected:
void AttachUserMenuHandler(Gtk::Widget &widget);
static Glib::ustring GetEscapedDisplayName(const UserData &user, const std::optional<GuildMember> &member);
bool on_author_button_press(GdkEventButton *ev);
std::vector<Gtk::Widget *> m_content_widgets;
std::vector<Gtk::Widget*> m_content_widgets;
Gtk::Box m_main_box;
Gtk::Box m_content_box;
Gtk::EventBox m_content_box_ev;
Gtk::Box m_meta_box;
Gtk::EventBox m_meta_ev;
Gtk::Label m_author;
Gtk::Label m_timestamp;
Gtk::Box *m_main_box;
Gtk::Box *m_content_box;
Gtk::EventBox *m_content_box_ev;
Gtk::Box *m_meta_box;
Gtk::EventBox *m_meta_ev;
Gtk::Label *m_author;
Gtk::Label *m_timestamp;
Gtk::Label *m_extra = nullptr;
Gtk::Image m_avatar;
Gtk::EventBox m_avatar_ev;
Gtk::Image *m_avatar;
Gtk::EventBox *m_avatar_ev;
Glib::RefPtr<Gdk::Pixbuf> m_static_avatar;
Glib::RefPtr<Gdk::PixbufAnimation> m_anim_avatar;

411
components/chatwindow.cpp Normal file
View File

@@ -0,0 +1,411 @@
#include "chatwindow.hpp"
#include "chatmessage.hpp"
#include "../abaddon.hpp"
#include "chatinputindicator.hpp"
#include "ratelimitindicator.hpp"
#include "chatinput.hpp"
constexpr static uint64_t SnowflakeSplitDifference = 600;
ChatWindow::ChatWindow() {
Abaddon::Get().GetDiscordClient().signal_message_send_fail().connect(sigc::mem_fun(*this, &ChatWindow::OnMessageSendFail));
m_main = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_VERTICAL));
m_list = Gtk::manage(new Gtk::ListBox);
m_scroll = Gtk::manage(new Gtk::ScrolledWindow);
m_input = Gtk::manage(new ChatInput);
m_input_indicator = Gtk::manage(new ChatInputIndicator);
m_rate_limit_indicator = Gtk::manage(new RateLimitIndicator);
m_meta = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL));
m_rate_limit_indicator->set_margin_end(5);
m_rate_limit_indicator->set_hexpand(true);
m_rate_limit_indicator->set_halign(Gtk::ALIGN_END);
m_rate_limit_indicator->set_valign(Gtk::ALIGN_END);
m_rate_limit_indicator->show();
m_input_indicator->set_halign(Gtk::ALIGN_START);
m_input_indicator->set_valign(Gtk::ALIGN_END);
m_input_indicator->show();
m_main->get_style_context()->add_class("messages");
m_list->get_style_context()->add_class("messages");
m_main->set_hexpand(true);
m_main->set_vexpand(true);
m_scroll->signal_edge_reached().connect(sigc::mem_fun(*this, &ChatWindow::OnScrollEdgeOvershot));
auto v = m_scroll->get_vadjustment();
v->signal_value_changed().connect([this, v] {
m_should_scroll_to_bottom = v->get_upper() - v->get_page_size() <= v->get_value();
});
m_scroll->set_can_focus(false);
m_scroll->set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_ALWAYS);
m_scroll->show();
m_list->signal_size_allocate().connect([this](Gtk::Allocation &) {
if (m_should_scroll_to_bottom)
ScrollToBottom();
});
m_list->set_selection_mode(Gtk::SELECTION_NONE);
m_list->set_hexpand(true);
m_list->set_vexpand(true);
m_list->set_focus_hadjustment(m_scroll->get_hadjustment());
m_list->set_focus_vadjustment(m_scroll->get_vadjustment());
m_list->show();
m_input->signal_submit().connect(sigc::mem_fun(*this, &ChatWindow::OnInputSubmit));
m_input->signal_escape().connect([this]() {
if (m_is_replying)
StopReplying();
});
m_input->signal_key_press_event().connect(sigc::mem_fun(*this, &ChatWindow::OnKeyPressEvent), false);
m_input->show();
m_completer.SetBuffer(m_input->GetBuffer());
m_completer.SetGetChannelID([this]() -> auto {
return m_active_channel;
});
m_completer.SetGetRecentAuthors([this]() -> auto {
const auto &discord = Abaddon::Get().GetDiscordClient();
std::vector<Snowflake> ret;
std::map<Snowflake, Gtk::Widget *> ordered(m_id_to_widget.begin(), m_id_to_widget.end());
for (auto it = ordered.crbegin(); it != ordered.crend(); it++) {
const auto *widget = dynamic_cast<ChatMessageItemContainer *>(it->second);
if (widget == nullptr) continue;
const auto msg = discord.GetMessage(widget->ID);
if (!msg.has_value()) continue;
if (std::find(ret.begin(), ret.end(), msg->Author.ID) == ret.end())
ret.push_back(msg->Author.ID);
}
const auto chan = discord.GetChannel(m_active_channel);
if (chan->GuildID.has_value()) {
const auto others = discord.GetUsersInGuild(*chan->GuildID);
for (const auto id : others)
if (std::find(ret.begin(), ret.end(), id) == ret.end())
ret.push_back(id);
}
return ret;
});
m_completer.show();
m_meta->set_hexpand(true);
m_meta->set_halign(Gtk::ALIGN_FILL);
m_meta->show();
m_meta->add(*m_input_indicator);
m_meta->add(*m_rate_limit_indicator);
m_scroll->add(*m_list);
m_main->add(*m_scroll);
m_main->add(m_completer);
m_main->add(*m_input);
m_main->add(*m_meta);
m_main->show();
}
Gtk::Widget *ChatWindow::GetRoot() const {
return m_main;
}
void ChatWindow::Clear() {
SetMessages(std::set<Snowflake>());
}
void ChatWindow::SetMessages(const std::set<Snowflake> &msgs) {
// empty the listbox
auto children = m_list->get_children();
auto it = children.begin();
while (it != children.end()) {
delete *it;
it++;
}
m_num_rows = 0;
m_num_messages = 0;
m_id_to_widget.clear();
for (const auto &id : msgs) {
ProcessNewMessage(id, false);
}
}
void ChatWindow::SetActiveChannel(Snowflake id) {
m_active_channel = id;
m_input_indicator->SetActiveChannel(id);
m_rate_limit_indicator->SetActiveChannel(id);
if (m_is_replying)
StopReplying();
}
void ChatWindow::AddNewMessage(Snowflake id) {
ProcessNewMessage(id, false);
}
void ChatWindow::DeleteMessage(Snowflake id) {
auto widget = m_id_to_widget.find(id);
if (widget == m_id_to_widget.end()) return;
auto *x = dynamic_cast<ChatMessageItemContainer *>(widget->second);
if (x != nullptr)
x->UpdateAttributes();
}
void ChatWindow::UpdateMessage(Snowflake id) {
auto widget = m_id_to_widget.find(id);
if (widget == m_id_to_widget.end()) return;
auto *x = dynamic_cast<ChatMessageItemContainer *>(widget->second);
if (x != nullptr) {
x->UpdateContent();
x->UpdateAttributes();
}
}
void ChatWindow::AddNewHistory(const std::vector<Snowflake> &id) {
std::set<Snowflake> ids(id.begin(), id.end());
for (auto it = ids.rbegin(); it != ids.rend(); it++)
ProcessNewMessage(*it, true);
}
void ChatWindow::InsertChatInput(std::string text) {
m_input->InsertText(text);
}
Snowflake ChatWindow::GetOldestListedMessage() {
return m_id_to_widget.begin()->first;
}
void ChatWindow::UpdateReactions(Snowflake id) {
auto it = m_id_to_widget.find(id);
if (it == m_id_to_widget.end()) return;
auto *widget = dynamic_cast<ChatMessageItemContainer *>(it->second);
if (widget == nullptr) return;
widget->UpdateReactions();
}
Snowflake ChatWindow::GetActiveChannel() const {
return m_active_channel;
}
bool ChatWindow::OnInputSubmit(const Glib::ustring &text) {
if (!m_rate_limit_indicator->CanSpeak())
return false;
if (m_active_channel.IsValid())
m_signal_action_chat_submit.emit(text, m_active_channel, m_replying_to); // m_replying_to is checked for invalid in the handler
if (m_is_replying)
StopReplying();
return true;
}
bool ChatWindow::OnKeyPressEvent(GdkEventKey *e) {
if (m_completer.ProcessKeyPress(e))
return true;
if (m_input->ProcessKeyPress(e))
return true;
return false;
}
ChatMessageItemContainer *ChatWindow::CreateMessageComponent(Snowflake id) {
auto *container = ChatMessageItemContainer::FromMessage(id);
return container;
}
void ChatWindow::RemoveMessageAndHeader(Gtk::Widget *widget) {
ChatMessageHeader *header = dynamic_cast<ChatMessageHeader *>(widget->get_ancestor(Gtk::ListBoxRow::get_type()));
if (header != nullptr) {
if (header->GetChildContent().size() == 1) {
m_num_rows--;
delete header;
} else
delete widget;
} else
delete widget;
m_num_messages--;
}
constexpr static int MaxMessagesForCull = 50; // this has to be 50 cuz that magic number is used in a couple other places and i dont feel like replacing them
void ChatWindow::ProcessNewMessage(Snowflake id, bool prepend) {
const auto &client = Abaddon::Get().GetDiscordClient();
if (!client.IsStarted()) return; // e.g. load channel and then dc
const auto data = client.GetMessage(id);
if (!data.has_value()) return;
if (!data->IsPending && data->Nonce.has_value() && data->Author.ID == client.GetUserData().ID) {
for (auto [id, widget] : m_id_to_widget) {
if (dynamic_cast<ChatMessageItemContainer *>(widget)->Nonce == *data->Nonce) {
RemoveMessageAndHeader(widget);
m_id_to_widget.erase(id);
break;
}
}
}
ChatMessageHeader *last_row = nullptr;
bool should_attach = false;
if (m_num_rows > 0) {
if (prepend)
last_row = dynamic_cast<ChatMessageHeader *>(m_list->get_row_at_index(0));
else
last_row = dynamic_cast<ChatMessageHeader *>(m_list->get_row_at_index(m_num_rows - 1));
if (last_row != nullptr) {
const uint64_t diff = std::max(id, last_row->NewestID) - std::min(id, last_row->NewestID);
if (last_row->UserID == data->Author.ID && (prepend || (diff < SnowflakeSplitDifference * Snowflake::SecondsInterval)))
should_attach = true;
}
}
m_num_messages++;
if (m_should_scroll_to_bottom && !prepend)
while (m_num_messages > MaxMessagesForCull) {
auto first_it = m_id_to_widget.begin();
RemoveMessageAndHeader(first_it->second);
m_id_to_widget.erase(first_it);
}
ChatMessageHeader *header;
if (should_attach) {
header = last_row;
} else {
const auto guild_id = *client.GetChannel(m_active_channel)->GuildID;
const auto user_id = data->Author.ID;
const auto user = client.GetUser(user_id);
if (!user.has_value()) return;
header = Gtk::manage(new ChatMessageHeader(&*data));
header->signal_action_insert_mention().connect([this, user_id]() {
m_signal_action_insert_mention.emit(user_id);
});
header->signal_action_open_user_menu().connect([this, user_id, guild_id](const GdkEvent *event) {
m_signal_action_open_user_menu.emit(event, user_id, guild_id);
});
m_num_rows++;
}
auto *content = CreateMessageComponent(id);
if (content != nullptr) {
header->AddContent(content, prepend);
m_id_to_widget[id] = content;
if (!data->IsPending) {
content->signal_action_delete().connect([this, id] {
m_signal_action_message_delete.emit(m_active_channel, id);
});
content->signal_action_edit().connect([this, id] {
m_signal_action_message_edit.emit(m_active_channel, id);
});
content->signal_action_reaction_add().connect([this, id](const Glib::ustring &param) {
m_signal_action_reaction_add.emit(id, param);
});
content->signal_action_reaction_remove().connect([this, id](const Glib::ustring &param) {
m_signal_action_reaction_remove.emit(id, param);
});
content->signal_action_channel_click().connect([this](const Snowflake &id) {
m_signal_action_channel_click.emit(id);
});
content->signal_action_reply_to().connect(sigc::mem_fun(*this, &ChatWindow::StartReplying));
}
}
header->set_margin_left(5);
header->show_all();
if (!should_attach) {
if (prepend)
m_list->prepend(*header);
else
m_list->add(*header);
}
}
void ChatWindow::StartReplying(Snowflake message_id) {
const auto &discord = Abaddon::Get().GetDiscordClient();
const auto message = *discord.GetMessage(message_id);
const auto author = discord.GetUser(message.Author.ID);
m_replying_to = message_id;
m_is_replying = true;
m_input->grab_focus();
m_input->get_style_context()->add_class("replying");
if (author.has_value())
m_input_indicator->SetCustomMarkup("Replying to " + author->GetEscapedBoldString<false>());
else
m_input_indicator->SetCustomMarkup("Replying...");
}
void ChatWindow::StopReplying() {
m_is_replying = false;
m_replying_to = Snowflake::Invalid;
m_input->get_style_context()->remove_class("replying");
m_input_indicator->ClearCustom();
}
void ChatWindow::OnScrollEdgeOvershot(Gtk::PositionType pos) {
if (pos == Gtk::POS_TOP)
m_signal_action_chat_load_history.emit(m_active_channel);
}
void ChatWindow::ScrollToBottom() {
auto x = m_scroll->get_vadjustment();
x->set_value(x->get_upper());
}
void ChatWindow::OnMessageSendFail(const std::string &nonce, float retry_after) {
for (auto [id, widget] : m_id_to_widget) {
if (auto *container = dynamic_cast<ChatMessageItemContainer *>(widget); container->Nonce == nonce) {
container->SetFailed();
break;
}
}
}
ChatWindow::type_signal_action_message_delete ChatWindow::signal_action_message_delete() {
return m_signal_action_message_delete;
}
ChatWindow::type_signal_action_message_edit ChatWindow::signal_action_message_edit() {
return m_signal_action_message_edit;
}
ChatWindow::type_signal_action_chat_submit ChatWindow::signal_action_chat_submit() {
return m_signal_action_chat_submit;
}
ChatWindow::type_signal_action_chat_load_history ChatWindow::signal_action_chat_load_history() {
return m_signal_action_chat_load_history;
}
ChatWindow::type_signal_action_channel_click ChatWindow::signal_action_channel_click() {
return m_signal_action_channel_click;
}
ChatWindow::type_signal_action_insert_mention ChatWindow::signal_action_insert_mention() {
return m_signal_action_insert_mention;
}
ChatWindow::type_signal_action_open_user_menu ChatWindow::signal_action_open_user_menu() {
return m_signal_action_open_user_menu;
}
ChatWindow::type_signal_action_reaction_add ChatWindow::signal_action_reaction_add() {
return m_signal_action_reaction_add;
}
ChatWindow::type_signal_action_reaction_remove ChatWindow::signal_action_reaction_remove() {
return m_signal_action_reaction_remove;
}

View File

@@ -2,7 +2,7 @@
#include <gtkmm.h>
#include <string>
#include <set>
#include "discord/discord.hpp"
#include "../discord/discord.hpp"
#include "completer.hpp"
class ChatMessageHeader;
@@ -10,7 +10,6 @@ class ChatMessageItemContainer;
class ChatInput;
class ChatInputIndicator;
class RateLimitIndicator;
class ChatList;
class ChatWindow {
public:
ChatWindow();
@@ -19,24 +18,30 @@ public:
Snowflake GetActiveChannel() const;
void Clear();
void SetMessages(const std::vector<Message> &msgs); // clear contents and replace with given set
void SetMessages(const std::set<Snowflake> &msgs); // clear contents and replace with given set
void SetActiveChannel(Snowflake id);
void AddNewMessage(const Message &data); // append new message to bottom
void AddNewMessage(Snowflake id); // append new message to bottom
void DeleteMessage(Snowflake id); // add [deleted] indicator
void UpdateMessage(Snowflake id); // add [edited] indicator
void AddNewHistory(const std::vector<Message> &msgs); // prepend messages
void InsertChatInput(const std::string& text);
void AddNewHistory(const std::vector<Snowflake> &id); // prepend messages
void InsertChatInput(std::string text);
Snowflake GetOldestListedMessage(); // oldest message that is currently in the ListBox
void UpdateReactions(Snowflake id);
void SetTopic(const std::string &text);
protected:
ChatMessageItemContainer *CreateMessageComponent(Snowflake id); // to be inserted into header's content box
void ProcessNewMessage(Snowflake id, bool prepend); // creates and adds components
bool m_is_replying = false;
Snowflake m_replying_to;
void StartReplying(Snowflake message_id);
void StopReplying();
int m_num_messages = 0;
int m_num_rows = 0;
std::map<Snowflake, Gtk::Widget *> m_id_to_widget;
Snowflake m_active_channel;
bool OnInputSubmit(const Glib::ustring &text);
@@ -44,16 +49,16 @@ protected:
bool OnKeyPressEvent(GdkEventKey *e);
void OnScrollEdgeOvershot(Gtk::PositionType pos);
void RemoveMessageAndHeader(Gtk::Widget *widget);
void ScrollToBottom();
bool m_should_scroll_to_bottom = true;
void OnMessageSendFail(const std::string &nonce, float retry_after);
Gtk::Box *m_main;
// Gtk::ListBox *m_list;
// Gtk::ScrolledWindow *m_scroll;
Gtk::EventBox m_topic; // todo probably make everything else go on the stack
Gtk::Label m_topic_text;
ChatList *m_chat;
Gtk::ListBox *m_list;
Gtk::ScrolledWindow *m_scroll;
ChatInput *m_input;
@@ -63,28 +68,34 @@ protected:
Gtk::Box *m_meta;
public:
typedef sigc::signal<void, Snowflake, Snowflake> type_signal_action_message_delete;
typedef sigc::signal<void, Snowflake, Snowflake> type_signal_action_message_edit;
typedef sigc::signal<void, std::string, Snowflake, Snowflake> type_signal_action_chat_submit;
typedef sigc::signal<void, Snowflake> type_signal_action_chat_load_history;
typedef sigc::signal<void, Snowflake> type_signal_action_channel_click;
typedef sigc::signal<void, Snowflake> type_signal_action_insert_mention;
typedef sigc::signal<void, const GdkEvent *, Snowflake, Snowflake> type_signal_action_open_user_menu;
typedef sigc::signal<void, Snowflake, Glib::ustring> type_signal_action_reaction_add;
typedef sigc::signal<void, Snowflake, Glib::ustring> type_signal_action_reaction_remove;
type_signal_action_message_delete signal_action_message_delete();
type_signal_action_message_edit signal_action_message_edit();
type_signal_action_chat_submit signal_action_chat_submit();
type_signal_action_chat_load_history signal_action_chat_load_history();
type_signal_action_channel_click signal_action_channel_click();
type_signal_action_insert_mention signal_action_insert_mention();
type_signal_action_open_user_menu signal_action_open_user_menu();
type_signal_action_reaction_add signal_action_reaction_add();
type_signal_action_reaction_remove signal_action_reaction_remove();
private:
type_signal_action_message_delete m_signal_action_message_delete;
type_signal_action_message_edit m_signal_action_message_edit;
type_signal_action_chat_submit m_signal_action_chat_submit;
type_signal_action_chat_load_history m_signal_action_chat_load_history;
type_signal_action_channel_click m_signal_action_channel_click;
type_signal_action_insert_mention m_signal_action_insert_mention;
type_signal_action_open_user_menu m_signal_action_open_user_menu;
type_signal_action_reaction_add m_signal_action_reaction_add;
type_signal_action_reaction_remove m_signal_action_reaction_remove;
};

View File

@@ -1,8 +1,7 @@
#include <unordered_set>
#include <utility>
#include "completer.hpp"
#include "abaddon.hpp"
#include "util.hpp"
#include "../abaddon.hpp"
#include "../util.hpp"
constexpr const int CompleterHeight = 150;
constexpr const int MaxCompleterEntries = 30;
@@ -47,23 +46,23 @@ bool Completer::ProcessKeyPress(GdkEventKey *e) {
switch (e->keyval) {
case GDK_KEY_Down: {
if (m_entries.empty()) return true;
const auto index = static_cast<size_t>(m_list.get_selected_row()->get_index());
if (m_entries.size() == 0) return true;
const int index = m_list.get_selected_row()->get_index();
if (index >= m_entries.size() - 1) return true;
m_list.select_row(*m_entries[index + 1]);
ScrollListBoxToSelected(m_list);
}
return true;
case GDK_KEY_Up: {
if (m_entries.empty()) return true;
const auto index = static_cast<size_t>(m_list.get_selected_row()->get_index());
if (m_entries.size() == 0) return true;
const int index = m_list.get_selected_row()->get_index();
if (index == 0) return true;
m_list.select_row(*m_entries[index - 1]);
ScrollListBoxToSelected(m_list);
}
return true;
case GDK_KEY_Return: {
if (m_entries.empty()) return true;
if (m_entries.size() == 0) return true;
DoCompletion(m_list.get_selected_row());
}
return true;
@@ -75,11 +74,11 @@ bool Completer::ProcessKeyPress(GdkEventKey *e) {
}
void Completer::SetGetRecentAuthors(get_recent_authors_cb cb) {
m_recent_authors_cb = std::move(cb);
m_recent_authors_cb = cb;
}
void Completer::SetGetChannelID(get_channel_id_cb cb) {
m_channel_id_cb = std::move(cb);
m_channel_id_cb = cb;
}
bool Completer::IsShown() const {
@@ -87,7 +86,7 @@ bool Completer::IsShown() const {
}
CompleterEntry *Completer::CreateEntry(const Glib::ustring &completion) {
auto entry = Gtk::manage(new CompleterEntry(completion, static_cast<int>(m_entries.size())));
auto entry = Gtk::manage(new CompleterEntry(completion, m_entries.size()));
m_entries.push_back(entry);
entry->show_all();
m_list.add(*entry);
@@ -153,7 +152,7 @@ void Completer::CompleteEmojis(const Glib::ustring &term) {
const auto make_entry = [&](const Glib::ustring &name, const Glib::ustring &completion, const Glib::ustring &url = "", bool animated = false) -> CompleterEntry * {
const auto entry = CreateEntry(completion);
entry->SetText(name);
if (url.empty()) return entry;
if (url == "") return entry;
if (animated)
entry->SetAnimation(url);
else
@@ -170,12 +169,12 @@ void Completer::CompleteEmojis(const Glib::ustring &term) {
const auto guild = discord.GetGuild(*channel->GuildID);
if (guild.has_value() && guild->Emojis.has_value())
for (const auto &tmp : *guild->Emojis) {
for (const auto tmp : *guild->Emojis) {
const auto emoji = *discord.GetEmoji(tmp.ID);
if (emoji.IsAnimated.has_value() && *emoji.IsAnimated) continue;
if (emoji.IsAvailable.has_value() && !*emoji.IsAvailable) continue;
if (emoji.Roles.has_value() && !emoji.Roles->empty()) continue;
if (!term.empty())
if (emoji.Roles.has_value() && emoji.Roles->size() > 0) continue;
if (term.size() > 0)
if (!StringContainsCaseless(emoji.Name, term)) continue;
if (i++ > MaxCompleterEntries) break;
@@ -187,12 +186,12 @@ void Completer::CompleteEmojis(const Glib::ustring &term) {
for (const auto guild_id : discord.GetGuilds()) {
const auto guild = discord.GetGuild(guild_id);
if (!guild.has_value()) continue;
for (const auto &tmp : *guild->Emojis) {
for (const auto tmp : *guild->Emojis) {
const auto emoji = *discord.GetEmoji(tmp.ID);
const bool is_animated = emoji.IsAnimated.has_value() && *emoji.IsAnimated;
if (emoji.IsAvailable.has_value() && !*emoji.IsAvailable) continue;
if (emoji.Roles.has_value() && !emoji.Roles->empty()) continue;
if (!term.empty())
if (emoji.Roles.has_value() && emoji.Roles->size() > 0) continue;
if (term.size() > 0)
if (!StringContainsCaseless(emoji.Name, term)) continue;
if (i++ > MaxCompleterEntries) goto done;
@@ -276,7 +275,7 @@ void Completer::OnTextBufferChanged() {
default:
break;
}
if (!m_entries.empty()) {
if (m_entries.size() > 0) {
m_list.select_row(*m_entries[0]);
set_reveal_child(true);
} else {
@@ -292,7 +291,7 @@ bool MultiBackwardSearch(const Gtk::TextIter &iter, const Glib::ustring &chars,
if (!iter.backward_search(tmp, flags, tstart, tend)) continue;
// if previous found, compare to see if closer to out iter
if (any) {
if (tstart.get_offset() > out.get_offset())
if (tstart > out)
out = tstart;
} else
out = tstart;
@@ -309,7 +308,7 @@ bool MultiForwardSearch(const Gtk::TextIter &iter, const Glib::ustring &chars, G
if (!iter.forward_search(tmp, flags, tstart, tend)) continue;
// if previous found, compare to see if closer to out iter
if (any) {
if (tstart.get_offset() < out.get_offset())
if (tstart < out)
out = tstart;
} else
out = tstart;
@@ -330,9 +329,9 @@ Glib::ustring Completer::GetTerm() {
return m_start.get_text(m_end);
}
CompleterEntry::CompleterEntry(Glib::ustring completion, int index)
: m_completion(std::move(completion))
, m_index(index)
CompleterEntry::CompleterEntry(const Glib::ustring &completion, int index)
: m_index(index)
, m_completion(completion)
, m_box(Gtk::ORIENTATION_HORIZONTAL) {
set_halign(Gtk::ALIGN_START);
get_style_context()->add_class("completer-entry");

View File

@@ -2,13 +2,13 @@
#include <gtkmm.h>
#include <functional>
#include "lazyimage.hpp"
#include "discord/snowflake.hpp"
#include "../discord/snowflake.hpp"
constexpr static int CompleterImageSize = 24;
class CompleterEntry : public Gtk::ListBoxRow {
public:
CompleterEntry(Glib::ustring completion, int index);
CompleterEntry(const Glib::ustring &completion, int index);
void SetTextColor(int color); // SetText will reset
void SetText(const Glib::ustring &text);
void SetImage(const Glib::RefPtr<Gdk::Pixbuf> &pb);

View File

@@ -102,7 +102,7 @@ bool DragListBox::scroll() {
}
void DragListBox::on_drag_data_received(const Glib::RefPtr<Gdk::DragContext> &context, int x, int y, const Gtk::SelectionData &selection_data, guint info, guint time) {
int index;
int index = 0;
if (m_hover_row != nullptr) {
if (m_top) {
index = m_hover_row->get_index() - 1;
@@ -130,7 +130,7 @@ void DragListBox::on_drag_data_received(const Glib::RefPtr<Gdk::DragContext> &co
void DragListBox::add_draggable(Gtk::ListBoxRow *widget) {
widget->drag_source_set(m_entries, Gdk::BUTTON1_MASK, Gdk::ACTION_MOVE);
widget->signal_drag_begin().connect(sigc::bind<0>(sigc::mem_fun(*this, &DragListBox::row_drag_begin), widget));
widget->signal_drag_data_get().connect([widget](const Glib::RefPtr<Gdk::DragContext> &context, Gtk::SelectionData &selection_data, guint info, guint time) {
widget->signal_drag_data_get().connect([this, widget](const Glib::RefPtr<Gdk::DragContext> &context, Gtk::SelectionData &selection_data, guint info, guint time) {
selection_data.set("GTK_LIST_BOX_ROW", 32, reinterpret_cast<const guint8 *>(&widget), sizeof(&widget));
});
add(*widget);

View File

@@ -0,0 +1,8 @@
#pragma once
// for things that are used in stackswitchers to be able to be told when they're switched to
class INotifySwitched {
public:
virtual void on_switched_to() {};
};

View File

@@ -1,7 +1,5 @@
#include "lazyimage.hpp"
#include <utility>
#include "abaddon.hpp"
#include "../abaddon.hpp"
LazyImage::LazyImage(int w, int h, bool use_placeholder)
: m_width(w)
@@ -11,8 +9,8 @@ LazyImage::LazyImage(int w, int h, bool use_placeholder)
signal_draw().connect(sigc::mem_fun(*this, &LazyImage::OnDraw));
}
LazyImage::LazyImage(std::string url, int w, int h, bool use_placeholder)
: m_url(std::move(url))
LazyImage::LazyImage(const std::string &url, int w, int h, bool use_placeholder)
: m_url(url)
, m_width(w)
, m_height(h) {
if (use_placeholder)
@@ -29,7 +27,7 @@ void LazyImage::SetURL(const std::string &url) {
}
bool LazyImage::OnDraw(const Cairo::RefPtr<Cairo::Context> &context) {
if (!m_needs_request || m_url.empty()) return false;
if (!m_needs_request || m_url == "") return false;
m_needs_request = false;
if (m_animated) {

View File

@@ -5,7 +5,7 @@
class LazyImage : public Gtk::Image {
public:
LazyImage(int w, int h, bool use_placeholder = true);
LazyImage(std::string url, int w, int h, bool use_placeholder = true);
LazyImage(const std::string &url, int w, int h, bool use_placeholder = true);
void SetAnimated(bool is_animated);
void SetURL(const std::string &url);

View File

@@ -1,12 +1,12 @@
#include "memberlist.hpp"
#include "abaddon.hpp"
#include "util.hpp"
#include "../abaddon.hpp"
#include "../util.hpp"
#include "lazyimage.hpp"
#include "statusindicator.hpp"
constexpr static const int MaxMemberListRows = 200;
MemberListUserRow::MemberListUserRow(const std::optional<GuildData> &guild, const UserData &data) {
MemberListUserRow::MemberListUserRow(const GuildData *guild, const UserData &data) {
ID = data.ID;
m_ev = Gtk::manage(new Gtk::EventBox);
m_box = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL));
@@ -14,10 +14,10 @@ MemberListUserRow::MemberListUserRow(const std::optional<GuildData> &guild, cons
m_avatar = Gtk::manage(new LazyImage(16, 16));
m_status_indicator = Gtk::manage(new StatusIndicator(ID));
if (Abaddon::Get().GetSettings().ShowOwnerCrown && guild.has_value() && guild->OwnerID == data.ID) {
static bool crown = Abaddon::Get().GetSettings().GetShowOwnerCrown();
if (crown && guild != nullptr && guild->OwnerID == data.ID) {
try {
const static auto crown_path = Abaddon::GetResPath("/crown.png");
auto pixbuf = Gdk::Pixbuf::create_from_file(crown_path, 12, 12);
auto pixbuf = Gdk::Pixbuf::create_from_file("./res/crown.png", 12, 12);
m_crown = Gtk::manage(new Gtk::Image(pixbuf));
m_crown->set_valign(Gtk::ALIGN_CENTER);
m_crown->set_margin_end(8);
@@ -26,10 +26,7 @@ MemberListUserRow::MemberListUserRow(const std::optional<GuildData> &guild, cons
m_status_indicator->set_margin_start(3);
if (guild.has_value())
m_avatar->SetURL(data.GetAvatarURL(guild->ID, "png"));
else
m_avatar->SetURL(data.GetAvatarURL("png"));
m_avatar->SetURL(data.GetAvatarURL("png"));
get_style_context()->add_class("members-row");
get_style_context()->add_class("members-row-member");
@@ -39,10 +36,11 @@ MemberListUserRow::MemberListUserRow(const std::optional<GuildData> &guild, cons
m_label->set_single_line_mode(true);
m_label->set_ellipsize(Pango::ELLIPSIZE_END);
static bool show_discriminator = Abaddon::Get().GetSettings().GetShowMemberListDiscriminators();
std::string display = data.Username;
if (Abaddon::Get().GetSettings().ShowMemberListDiscriminators)
if (show_discriminator)
display += "#" + data.Discriminator;
if (guild.has_value()) {
if (guild != nullptr) {
if (const auto col_id = data.GetHoistedRole(guild->ID, true); col_id.IsValid()) {
auto color = Abaddon::Get().GetDiscordClient().GetRole(col_id)->Color;
m_label->set_use_markup(true);
@@ -92,7 +90,7 @@ void MemberList::SetActiveChannel(Snowflake id) {
m_guild_id = Snowflake::Invalid;
if (m_chan_id.IsValid()) {
const auto chan = Abaddon::Get().GetDiscordClient().GetChannel(id);
if (chan.has_value() && chan->GuildID.has_value()) m_guild_id = *chan->GuildID;
if (chan.has_value()) m_guild_id = *chan->GuildID;
}
}
@@ -116,7 +114,7 @@ void MemberList::UpdateMemberList() {
int num_rows = 0;
for (const auto &user : chan->GetDMRecipients()) {
if (num_rows++ > MaxMemberListRows) break;
auto *row = Gtk::manage(new MemberListUserRow(std::nullopt, user));
auto *row = Gtk::manage(new MemberListUserRow(nullptr, user));
m_id_to_row[user.ID] = row;
AttachUserMenuHandler(row, user.ID);
m_listbox->add(*row);
@@ -125,12 +123,7 @@ void MemberList::UpdateMemberList() {
return;
}
std::set<Snowflake> ids;
if (chan->IsThread()) {
const auto x = discord.GetUsersInThread(m_chan_id);
ids = { x.begin(), x.end() };
} else
ids = discord.GetUsersInGuild(m_guild_id);
auto ids = discord.GetUsersInGuild(m_guild_id);
// process all the shit first so its in proper order
std::map<int, RoleData> pos_to_role;
@@ -151,7 +144,7 @@ void MemberList::UpdateMemberList() {
if (!pos_role.has_value()) {
roleless_users.push_back(id);
continue;
}
};
pos_to_role[pos_role->Position] = *pos_role;
pos_to_users[pos_role->Position].push_back(std::move(*user));
@@ -161,16 +154,16 @@ void MemberList::UpdateMemberList() {
int num_rows = 0;
const auto guild = *discord.GetGuild(m_guild_id);
auto add_user = [this, &num_rows, guild](const UserData &data) -> bool {
auto add_user = [this, &user_to_color, &num_rows, guild](const UserData &data) -> bool {
if (num_rows++ > MaxMemberListRows) return false;
auto *row = Gtk::manage(new MemberListUserRow(guild, data));
auto *row = Gtk::manage(new MemberListUserRow(&guild, data));
m_id_to_row[data.ID] = row;
AttachUserMenuHandler(row, data.ID);
m_listbox->add(*row);
return true;
};
auto add_role = [this](const std::string &name) {
auto add_role = [this](std::string name) {
auto *role_row = Gtk::manage(new Gtk::ListBoxRow);
auto *role_lbl = Gtk::manage(new Gtk::Label);
@@ -215,12 +208,16 @@ void MemberList::UpdateMemberList() {
}
void MemberList::AttachUserMenuHandler(Gtk::ListBoxRow *row, Snowflake id) {
row->signal_button_press_event().connect([this, id](GdkEventButton *e) -> bool {
row->signal_button_press_event().connect([this, row, id](GdkEventButton *e) -> bool {
if (e->type == GDK_BUTTON_PRESS && e->button == GDK_BUTTON_SECONDARY) {
Abaddon::Get().ShowUserMenu(reinterpret_cast<const GdkEvent *>(e), id, m_guild_id);
m_signal_action_show_user_menu.emit(reinterpret_cast<const GdkEvent *>(e), id, m_guild_id);
return true;
}
return false;
});
}
MemberList::type_signal_action_show_user_menu MemberList::signal_action_show_user_menu() {
return m_signal_action_show_user_menu;
}

View File

@@ -3,13 +3,13 @@
#include <mutex>
#include <unordered_map>
#include <optional>
#include "discord/discord.hpp"
#include "../discord/discord.hpp"
class LazyImage;
class StatusIndicator;
class MemberListUserRow : public Gtk::ListBoxRow {
public:
MemberListUserRow(const std::optional<GuildData> &guild, const UserData &data);
MemberListUserRow(const GuildData *guild, const UserData &data);
Snowflake ID;
@@ -41,4 +41,12 @@ private:
Snowflake m_chan_id;
std::unordered_map<Snowflake, Gtk::ListBoxRow *> m_id_to_row;
public:
typedef sigc::signal<void, const GdkEvent *, Snowflake, Snowflake> type_signal_action_show_user_menu;
type_signal_action_show_user_menu signal_action_show_user_menu();
private:
type_signal_action_show_user_menu m_signal_action_show_user_menu;
};

View File

@@ -1,5 +1,5 @@
#include "ratelimitindicator.hpp"
#include "abaddon.hpp"
#include "../abaddon.hpp"
#include <filesystem>
RateLimitIndicator::RateLimitIndicator()
@@ -15,10 +15,9 @@ RateLimitIndicator::RateLimitIndicator()
add(m_img);
m_label.show();
const static auto clock_path = Abaddon::GetResPath("/clock.png");
if (std::filesystem::exists(clock_path)) {
if (std::filesystem::exists("./res/clock.png")) {
try {
const auto pixbuf = Gdk::Pixbuf::create_from_file(clock_path);
const auto pixbuf = Gdk::Pixbuf::create_from_file("./res/clock.png");
int w, h;
GetImageDimensions(pixbuf->get_width(), pixbuf->get_height(), w, h, 20, 10);
m_img.property_pixbuf() = pixbuf->scale_simple(w, h, Gdk::INTERP_BILINEAR);
@@ -32,9 +31,9 @@ RateLimitIndicator::RateLimitIndicator()
void RateLimitIndicator::SetActiveChannel(Snowflake id) {
m_active_channel = id;
const auto channel = Abaddon::Get().GetDiscordClient().GetChannel(m_active_channel);
if (channel.has_value() && channel->RateLimitPerUser.has_value())
m_rate_limit = *channel->RateLimitPerUser;
const auto channel = *Abaddon::Get().GetDiscordClient().GetChannel(m_active_channel);
if (channel.RateLimitPerUser.has_value())
m_rate_limit = *channel.RateLimitPerUser;
else
m_rate_limit = 0;
@@ -66,7 +65,7 @@ int RateLimitIndicator::GetTimeLeft() const {
if (sec_diff <= 0)
return 0;
else
return static_cast<int>(sec_diff);
return sec_diff;
}
int RateLimitIndicator::GetRateLimit() const {
@@ -106,7 +105,6 @@ bool RateLimitIndicator::UpdateIndicator() {
void RateLimitIndicator::OnMessageCreate(const Message &message) {
auto &discord = Abaddon::Get().GetDiscordClient();
if (message.Author.ID != discord.GetUserData().ID) return;
if (!message.GuildID.has_value()) return;
const bool can_bypass = discord.HasAnyChannelPermission(discord.GetUserData().ID, m_active_channel, Permission::MANAGE_MESSAGES | Permission::MANAGE_CHANNELS);
const auto rate_limit = GetRateLimit();
if (rate_limit > 0 && !can_bypass) {
@@ -125,10 +123,7 @@ void RateLimitIndicator::OnMessageSendFail(const std::string &nonce, float retry
}
void RateLimitIndicator::OnChannelUpdate(Snowflake channel_id) {
if (channel_id != m_active_channel) return;
const auto chan = Abaddon::Get().GetDiscordClient().GetChannel(m_active_channel);
if (!chan.has_value()) return;
const auto r = chan->RateLimitPerUser;
const auto r = Abaddon::Get().GetDiscordClient().GetChannel(channel_id)->RateLimitPerUser;
if (r.has_value())
m_rate_limit = *r;
else

View File

@@ -2,7 +2,7 @@
#include <gtkmm.h>
#include <unordered_map>
#include <chrono>
#include "discord/message.hpp"
#include "../discord/message.hpp"
class RateLimitIndicator : public Gtk::Box {
public:

View File

@@ -1,7 +1,11 @@
#include "statusindicator.hpp"
#include "abaddon.hpp"
#include "../abaddon.hpp"
static const constexpr int Diameter = 8;
static const auto OnlineColor = Gdk::RGBA("#43B581");
static const auto IdleColor = Gdk::RGBA("#FAA61A");
static const auto DNDColor = Gdk::RGBA("#982929");
static const auto OfflineColor = Gdk::RGBA("#808080");
StatusIndicator::StatusIndicator(Snowflake user_id)
: Glib::ObjectBase("statusindicator")
@@ -14,14 +18,17 @@ StatusIndicator::StatusIndicator(Snowflake user_id)
get_style_context()->add_class("status-indicator");
Abaddon::Get().GetDiscordClient().signal_guild_member_list_update().connect(sigc::hide(sigc::mem_fun(*this, &StatusIndicator::CheckStatus)));
auto cb = [this](const UserData &user, PresenceStatus status) {
if (user.ID == m_id) CheckStatus();
auto cb = [this](Snowflake id, PresenceStatus status) {
if (id == m_id) CheckStatus();
};
Abaddon::Get().GetDiscordClient().signal_presence_update().connect(sigc::track_obj(cb, *this));
CheckStatus();
}
StatusIndicator::~StatusIndicator() {
}
void StatusIndicator::CheckStatus() {
const auto status = Abaddon::Get().GetDiscordClient().GetUserStatus(m_id);
const auto last_status = m_status;
@@ -114,7 +121,7 @@ bool StatusIndicator::on_draw(const Cairo::RefPtr<Cairo::Context> &cr) {
const auto color = get_style_context()->get_color(Gtk::STATE_FLAG_NORMAL);
cr->set_source_rgb(color.get_red(), color.get_green(), color.get_blue());
cr->arc(width / 2.0, height / 2.0, width / 3.0, 0.0, 2 * (4 * std::atan(1)));
cr->arc(width / 2, height / 2, width / 3, 0.0, 2 * (4 * std::atan(1)));
cr->close_path();
cr->fill_preserve();
cr->stroke();

View File

@@ -1,12 +1,12 @@
#pragma once
#include <gtkmm.h>
#include "discord/snowflake.hpp"
#include "discord/activity.hpp"
#include "../discord/snowflake.hpp"
#include "../discord/activity.hpp"
class StatusIndicator : public Gtk::Widget {
public:
StatusIndicator(Snowflake user_id);
~StatusIndicator() override = default;
virtual ~StatusIndicator();
protected:
Gtk::SizeRequestMode get_request_mode_vfunc() const override;

View File

@@ -57,8 +57,14 @@
padding: 15px;
}
.message-container + .message-container {
margin-top: 10px;
}
.message-container-extra {
color: #78909c;
margin-left: -5px;
margin-right: -5px;
}
.message-container-timestamp {
@@ -66,8 +72,7 @@
}
.message-text {
/* this isnt stricly necessary but it fixes emoji clipping */
padding-bottom: 5px;
padding-top: 5px;
}
.message-text:not(.failed) text, .message-reply {
@@ -136,26 +141,6 @@
margin: 5px;
}
.message-component {
margin: 5px;
}
.message-component.primary {
background: #5865F2;
}
.message-component.secondary, .message-component.link {
background: #4F545C;
}
.message-component.success {
background: #43B581;
}
.message-component.danger {
background: #F04747;
}
.reaction-box {
padding: 2px 5px 2px 5px;
margin: 0px 0px 0px 0px;
@@ -191,6 +176,62 @@
color: @text_color;
}
.app-window label:not(:disabled) {
color: @text_color;
}
.app-window entry {
background: @secondary_color;
color: @text_color;
border: 1px solid #1c2e40;
}
.app-window button {
background: @secondary_color;
color: @text_color;
text-shadow: none;
box-shadow: none;
}
.app-window button:checked {
border-top: 0px;
border-left: 0px;
border-right: 0px;
border-bottom: 3px solid #39a2ed;
color: #ffffff;
}
.app-window button:not(:checked) {
border: 3px #0000ff;
}
.app-window.background {
background: @background_color;
}
.app-window treeview {
color: @text_color;
background: @secondary_color;
}
.app-popup list {
background: @secondary_color;
}
.app-window paned separator {
background: @background_color;
}
.app-window scrollbar {
background: @background_color;
border-left: 1px solid transparent;
}
.app-window menubar, menu {
background: @background_color;
color: #cccccc;
}
.status-indicator.dnd {
color: #982929;
}
@@ -262,10 +303,37 @@
padding-left: 5px;
}
.app-window textview text {
caret-color: #ababab;
}
.guild-members-pane-info {
padding: 10px;
}
.app-window check,
.app-window radio {
background-clip: padding-box;
background: @secondary_color;
border-color: #070707;
box-shadow: 0 1px rgba(0, 0, 0, 0);
color: #dddddd;
}
.app-window check:checked,
.app-window radio:checked {
background-clip: border-box;
background: #0b4285;
border-color: #092444;
box-shadow: 0 1px rgba(0, 0, 0, 0);
color: #dddddd;
}
.app-window colorswatch {
box-shadow: 0 1px rgba(0, 0, 0, 0);
}
.drag-hover-top {
background: linear-gradient(to bottom, rgba(255, 66, 66, 0.65) 0%, rgba(0, 0, 0, 0) 35%);
}
@@ -273,12 +341,3 @@
.drag-hover-bottom {
background: linear-gradient(to bottom, rgba(0, 0, 0, 0) 65%, rgba(255, 66, 66, 0.65) 100%);
}
.friends-list list {
background: @background_color;
padding-left: 10px;
}
.friends-list-row-bot {
color: #ff0000;
}

View File

@@ -3,9 +3,9 @@
ConfirmDialog::ConfirmDialog(Gtk::Window &parent)
: Gtk::Dialog("Confirm", parent, true)
, m_layout(Gtk::ORIENTATION_VERTICAL)
, m_bbox(Gtk::ORIENTATION_HORIZONTAL)
, m_ok("OK")
, m_cancel("Cancel")
, m_bbox(Gtk::ORIENTATION_HORIZONTAL) {
, m_cancel("Cancel") {
set_default_size(300, 50);
get_style_context()->add_class("app-window");
get_style_context()->add_class("app-popup");

View File

@@ -3,9 +3,9 @@
EditMessageDialog::EditMessageDialog(Gtk::Window &parent)
: Gtk::Dialog("Edit Message", parent, true)
, m_layout(Gtk::ORIENTATION_VERTICAL)
, m_bbox(Gtk::ORIENTATION_HORIZONTAL)
, m_ok("OK")
, m_cancel("Cancel")
, m_bbox(Gtk::ORIENTATION_HORIZONTAL) {
, m_cancel("Cancel") {
set_default_size(300, 50);
get_style_context()->add_class("app-window");
get_style_context()->add_class("app-popup");

View File

@@ -1,5 +1,5 @@
#include "friendpicker.hpp"
#include "abaddon.hpp"
#include "../abaddon.hpp"
FriendPickerDialog::FriendPickerDialog(Gtk::Window &parent)
: Gtk::Dialog("Pick a friend", parent, true)
@@ -67,7 +67,7 @@ FriendPickerDialogItem::FriendPickerDialogItem(Snowflake user_id)
m_name.set_single_line_mode(true);
m_avatar.property_pixbuf() = Abaddon::Get().GetImageManager().GetPlaceholder(32);
if (user.HasAnimatedAvatar() && Abaddon::Get().GetSettings().ShowAnimations) {
if (user.HasAnimatedAvatar() && Abaddon::Get().GetSettings().GetShowAnimations()) {
auto cb = [this](const Glib::RefPtr<Gdk::PixbufAnimation> &pb) {
m_avatar.property_pixbuf_animation() = pb;
};

View File

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

View File

@@ -1,5 +1,5 @@
#include "joinguild.hpp"
#include "abaddon.hpp"
#include "../abaddon.hpp"
#include <nlohmann/json.hpp>
#include <regex>

View File

@@ -10,7 +10,7 @@ public:
protected:
void on_entry_changed();
static bool IsCode(std::string str);
bool IsCode(std::string str);
Gtk::Box m_layout;
Gtk::Button m_ok;

View File

@@ -3,10 +3,10 @@
SetStatusDialog::SetStatusDialog(Gtk::Window &parent)
: Gtk::Dialog("Set Status", parent, true)
, m_layout(Gtk::ORIENTATION_VERTICAL)
, m_bbox(Gtk::ORIENTATION_HORIZONTAL)
, m_bottom(Gtk::ORIENTATION_HORIZONTAL)
, m_ok("OK")
, m_cancel("Cancel")
, m_bbox(Gtk::ORIENTATION_HORIZONTAL) {
, m_cancel("Cancel") {
set_default_size(300, 50);
get_style_context()->add_class("app-window");
get_style_context()->add_class("app-popup");

View File

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

View File

@@ -1,24 +1,17 @@
#include "token.hpp"
std::string trim(const std::string &str) {
const auto first = str.find_first_not_of(' ');
if (first == std::string::npos) return str;
const auto last = str.find_last_not_of(' ');
return str.substr(first, last - first + 1);
}
TokenDialog::TokenDialog(Gtk::Window &parent)
: Gtk::Dialog("Set Token", parent, true)
, m_layout(Gtk::ORIENTATION_VERTICAL)
, m_bbox(Gtk::ORIENTATION_HORIZONTAL)
, m_ok("OK")
, m_cancel("Cancel")
, m_bbox(Gtk::ORIENTATION_HORIZONTAL) {
, m_cancel("Cancel") {
set_default_size(300, 50);
get_style_context()->add_class("app-window");
get_style_context()->add_class("app-popup");
m_ok.signal_clicked().connect([&]() {
m_token = trim(m_entry.get_text());
m_token = m_entry.get_text();
response(Gtk::RESPONSE_OK);
});
@@ -30,8 +23,6 @@ TokenDialog::TokenDialog(Gtk::Window &parent)
m_bbox.pack_start(m_cancel, Gtk::PACK_SHRINK);
m_bbox.set_layout(Gtk::BUTTONBOX_END);
m_entry.set_input_purpose(Gtk::INPUT_PURPOSE_PASSWORD);
m_entry.set_visibility(false);
m_entry.set_hexpand(true);
m_layout.add(m_entry);
m_layout.add(m_bbox);

View File

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

View File

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

View File

@@ -1,7 +1,7 @@
#pragma once
#include <string>
#include <optional>
#include "util.hpp"
#include "../util.hpp"
#include "json.hpp"
#include "snowflake.hpp"
@@ -26,20 +26,6 @@ constexpr inline const char *GetPresenceString(PresenceStatus s) {
return "";
}
constexpr inline const char *GetPresenceDisplayString(PresenceStatus s) {
switch (s) {
case PresenceStatus::Online:
return "Online";
case PresenceStatus::Offline:
return "Offline";
case PresenceStatus::Idle:
return "Away";
case PresenceStatus::DND:
return "Do Not Disturb";
}
return "";
}
enum class ActivityType : int {
Game = 0,
Streaming = 1,

View File

@@ -20,7 +20,7 @@ void from_json(const nlohmann::json &j, AuditLogOptions &m) {
void from_json(const nlohmann::json &j, AuditLogEntry &m) {
JS_N("target_id", m.TargetID);
JS_O("changes", m.Changes);
JS_N("user_id", m.UserID);
JS_D("user_id", m.UserID);
JS_D("id", m.ID);
JS_D("action_type", m.Type);
JS_O("options", m.Options);

View File

@@ -40,15 +40,6 @@ enum class AuditLogActionType {
INTEGRATION_CREATE = 80,
INTEGRATION_UPDATE = 81,
INTEGRATION_DELETE = 82,
STAGE_INSTANCE_CREATE = 83,
STAGE_INSTANCE_UPDATE = 84,
STAGE_INSTANCE_DELETE = 85,
STICKER_CREATE = 90,
STICKER_UPDATE = 91,
STICKER_DELETE = 92,
THREAD_CREATE = 110,
THREAD_UPDATE = 111,
THREAD_DELETE = 112,
};
struct AuditLogChange {
@@ -75,7 +66,7 @@ struct AuditLogOptions {
struct AuditLogEntry {
Snowflake ID;
std::string TargetID; // null
std::optional<Snowflake> UserID;
Snowflake UserID;
AuditLogActionType Type;
std::optional<std::string> Reason;
std::optional<std::vector<AuditLogChange>> Changes;

View File

@@ -4,7 +4,7 @@
struct BanData {
std::string Reason; // null
UserData User; // access id
UserData User; // access id
friend void from_json(const nlohmann::json &j, BanData &m);
};

View File

@@ -1,22 +1,6 @@
#include "abaddon.hpp"
#include "../abaddon.hpp"
#include "channel.hpp"
void from_json(const nlohmann::json &j, ThreadMetadataData &m) {
JS_D("archived", m.IsArchived);
JS_D("auto_archive_duration", m.AutoArchiveDuration);
JS_D("archive_timestamp", m.ArchiveTimestamp);
JS_O("locked", m.IsLocked);
}
void from_json(const nlohmann::json &j, ThreadMemberObject &m) {
JS_O("id", m.ThreadID);
JS_O("user_id", m.UserID);
JS_D("join_timestamp", m.JoinTimestamp);
JS_D("flags", m.Flags);
JS_O("muted", m.IsMuted);
JS_ON("mute_config", m.MuteConfig);
}
void from_json(const nlohmann::json &j, ChannelData &m) {
JS_D("id", m.ID);
JS_D("type", m.Type);
@@ -37,8 +21,6 @@ void from_json(const nlohmann::json &j, ChannelData &m) {
JS_O("application_id", m.ApplicationID);
JS_ON("parent_id", m.ParentID);
JS_ON("last_pin_timestamp", m.LastPinTimestamp);
JS_O("thread_metadata", m.ThreadMetadata);
JS_O("member", m.ThreadMember);
}
void ChannelData::update_from_json(const nlohmann::json &j) {
@@ -61,41 +43,6 @@ void ChannelData::update_from_json(const nlohmann::json &j) {
JS_RD("last_pin_timestamp", LastPinTimestamp);
}
bool ChannelData::NSFW() const {
return IsNSFW.has_value() && *IsNSFW;
}
bool ChannelData::IsDM() const noexcept {
return Type == ChannelType::DM ||
Type == ChannelType::GROUP_DM;
}
bool ChannelData::IsThread() const noexcept {
return Type == ChannelType::GUILD_PUBLIC_THREAD ||
Type == ChannelType::GUILD_PRIVATE_THREAD ||
Type == ChannelType::GUILD_NEWS_THREAD;
}
bool ChannelData::IsJoinedThread() const {
return Abaddon::Get().GetDiscordClient().IsThreadJoined(ID);
}
bool ChannelData::IsCategory() const noexcept {
return Type == ChannelType::GUILD_CATEGORY;
}
bool ChannelData::HasIcon() const noexcept {
return Icon.has_value();
}
std::string ChannelData::GetIconURL() const {
return "https://cdn.discordapp.com/channel-icons/" + std::to_string(ID) + "/" + *Icon + ".png";
}
std::vector<Snowflake> ChannelData::GetChildIDs() const {
return Abaddon::Get().GetDiscordClient().GetChildChannelIDs(ID);
}
std::optional<PermissionOverwrite> ChannelData::GetOverwrite(Snowflake id) const {
return Abaddon::Get().GetDiscordClient().GetPermissionOverwrite(ID, id);
}
@@ -116,8 +63,5 @@ std::vector<UserData> ChannelData::GetDMRecipients() const {
return ret;
}
return {};
}
bool ChannelData::IsText() const noexcept {
return Type == ChannelType::GUILD_TEXT || Type == ChannelType::GUILD_NEWS;
return std::vector<UserData>();
}

50
discord/channel.hpp Normal file
View File

@@ -0,0 +1,50 @@
#pragma once
#include "snowflake.hpp"
#include "json.hpp"
#include "user.hpp"
#include "permissions.hpp"
#include <string>
#include <vector>
enum class ChannelType : int {
GUILD_TEXT = 0,
DM = 1,
GUILD_VOICE = 2,
GROUP_DM = 3,
GUILD_CATEGORY = 4,
GUILD_NEWS = 5,
GUILD_STORE = 6,
/* 7 and 8 were used for LFG */
/* 9 and 10 were used for threads */
PUBLIC_THREAD = 11,
PRIVATE_THREAD = 12,
GUILD_STAGE_VOICE = 13,
};
struct ChannelData {
Snowflake ID;
ChannelType Type;
std::optional<Snowflake> GuildID;
std::optional<int> Position;
std::optional<std::vector<PermissionOverwrite>> PermissionOverwrites; // shouldnt be accessed
std::optional<std::string> Name; // null for dm's
std::optional<std::string> Topic; // null
std::optional<bool> IsNSFW;
std::optional<Snowflake> LastMessageID; // null
std::optional<int> Bitrate;
std::optional<int> UserLimit;
std::optional<int> RateLimitPerUser;
std::optional<std::vector<UserData>> Recipients; // only access id
std::optional<std::vector<Snowflake>> RecipientIDs;
std::optional<std::string> Icon; // null
std::optional<Snowflake> OwnerID;
std::optional<Snowflake> ApplicationID;
std::optional<Snowflake> ParentID; // null
std::optional<std::string> LastPinTimestamp; // null
friend void from_json(const nlohmann::json &j, ChannelData &m);
void update_from_json(const nlohmann::json &j);
std::optional<PermissionOverwrite> GetOverwrite(Snowflake id) const;
std::vector<UserData> GetDMRecipients() const;
};

File diff suppressed because it is too large Load Diff

View File

@@ -6,17 +6,20 @@
#include <sigc++/sigc++.h>
#include <nlohmann/json.hpp>
#include <thread>
#include <map>
#include <unordered_map>
#include <set>
#include <unordered_set>
#include <mutex>
#include <zlib.h>
#include <glibmm.h>
#include <queue>
// bruh
#ifdef GetMessage
#undef GetMessage
#endif
// https://stackoverflow.com/questions/29775153/stopping-long-sleep-threads/29775639#29775639
class HeartbeatWaiter {
public:
template<class R, class P>
@@ -46,6 +49,10 @@ class Abaddon;
class DiscordClient {
friend class Abaddon;
public:
static const constexpr char *DiscordGateway = "wss://gateway.discord.gg/?v=9&encoding=json&compress=zlib-stream";
static const constexpr char *DiscordAPI = "https://discord.com/api/v9";
public:
DiscordClient(bool mem_store = false);
void Start();
@@ -53,17 +60,25 @@ public:
bool IsStarted() const;
bool IsStoreValid() const;
using guilds_type = Store::guilds_type;
using channels_type = Store::channels_type;
using messages_type = Store::messages_type;
using users_type = Store::users_type;
using roles_type = Store::roles_type;
using members_type = Store::members_type;
using permission_overwrites_type = Store::permission_overwrites_type;
std::unordered_set<Snowflake> GetGuilds() const;
const UserData &GetUserData() const;
const UserSettings &GetUserSettings() const;
std::vector<Snowflake> GetUserSortedGuilds() 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::set<Snowflake> GetMessagesForChannel(Snowflake id) const;
std::set<Snowflake> GetPrivateChannels() const;
EPremiumType GetSelfPremiumType() const;
void FetchMessagesInChannel(Snowflake id, const sigc::slot<void(const std::vector<Message> &)> &cb);
void FetchMessagesInChannelBefore(Snowflake channel_id, Snowflake before_id, const sigc::slot<void(const std::vector<Message> &)> &cb);
void FetchMessagesInChannel(Snowflake id, std::function<void(const std::vector<Snowflake> &)> cb);
void FetchMessagesInChannelBefore(Snowflake channel_id, Snowflake before_id, std::function<void(const std::vector<Snowflake> &)> cb);
std::optional<Message> GetMessage(Snowflake id) const;
std::optional<ChannelData> GetChannel(Snowflake id) const;
std::optional<EmojiData> GetEmoji(Snowflake id) const;
@@ -72,28 +87,12 @@ public:
std::optional<RoleData> GetRole(Snowflake id) const;
std::optional<GuildData> GetGuild(Snowflake id) const;
std::optional<GuildMember> GetMember(Snowflake user_id, Snowflake guild_id) const;
std::optional<BanData> GetBan(Snowflake guild_id, Snowflake user_id) const;
Snowflake GetMemberHoistedRole(Snowflake guild_id, Snowflake user_id, bool with_color = false) const;
std::optional<RoleData> GetMemberHighestRole(Snowflake guild_id, Snowflake user_id) const;
std::set<Snowflake> GetUsersInGuild(Snowflake id) const;
std::set<Snowflake> GetChannelsInGuild(Snowflake id) const;
std::vector<Snowflake> GetUsersInThread(Snowflake id) const;
std::vector<ChannelData> GetActiveThreads(Snowflake channel_id) const;
void GetArchivedPublicThreads(Snowflake channel_id, const sigc::slot<void(DiscordError, const ArchivedThreadsResponseData &)> &callback);
void GetArchivedPrivateThreads(Snowflake channel_id, const sigc::slot<void(DiscordError, const ArchivedThreadsResponseData &)> &callback);
std::vector<Snowflake> GetChildChannelIDs(Snowflake parent_id) const;
std::unordered_set<Snowflake> GetUsersInGuild(Snowflake id) const;
std::unordered_set<Snowflake> GetChannelsInGuild(Snowflake id) const;
// get ids of given list of members for who we do not have the member data
template<typename Iter>
std::unordered_set<Snowflake> FilterUnknownMembersFrom(Snowflake guild_id, Iter begin, Iter end) {
std::unordered_set<Snowflake> ret;
const auto known = m_store.GetMembersInGuild(guild_id);
for (auto iter = begin; iter != end; iter++)
if (known.find(*iter) == known.end())
ret.insert(*iter);
return ret;
}
bool IsThreadJoined(Snowflake thread_id) const;
bool HasGuildPermission(Snowflake user_id, Snowflake guild_id, Permission perm) const;
bool HasAnyChannelPermission(Snowflake user_id, Snowflake channel_id, Permission perm) const;
@@ -102,120 +101,84 @@ public:
Permission ComputeOverwrites(Permission base, Snowflake member_id, Snowflake channel_id) const;
bool CanManageMember(Snowflake guild_id, Snowflake actor, Snowflake target) const; // kick, ban, edit nickname (cant think of a better name)
void ChatMessageCallback(const std::string &nonce, const http::response_type &response);
void ChatMessageCallback(std::string nonce, const http::response_type &response);
void SendChatMessage(const std::string &content, Snowflake channel);
void SendChatMessage(const std::string &content, Snowflake channel, Snowflake referenced_message);
void DeleteMessage(Snowflake channel_id, Snowflake id);
void EditMessage(Snowflake channel_id, Snowflake id, std::string content);
void SendLazyLoad(Snowflake id);
void SendThreadLazyLoad(Snowflake id);
void JoinGuild(const std::string &code);
void JoinGuild(std::string code);
void LeaveGuild(Snowflake id);
void KickUser(Snowflake user_id, Snowflake guild_id);
void BanUser(Snowflake user_id, Snowflake guild_id); // todo: reason, delete messages
void UpdateStatus(PresenceStatus status, bool is_afk);
void UpdateStatus(PresenceStatus status, bool is_afk, const ActivityData &obj);
void CreateDM(Snowflake user_id, const sigc::slot<void(DiscordError code, Snowflake channel_id)> &callback);
void CreateDM(Snowflake user_id);
void CreateDM(Snowflake user_id, sigc::slot<void(bool success, Snowflake channel_id)> callback);
void CloseDM(Snowflake channel_id);
std::optional<Snowflake> FindDM(Snowflake user_id); // wont find group dms
void AddReaction(Snowflake id, Glib::ustring param);
void RemoveReaction(Snowflake id, Glib::ustring param);
void SetGuildName(Snowflake id, const Glib::ustring &name, const sigc::slot<void(DiscordError code)> &callback);
void SetGuildIcon(Snowflake id, const std::string &data, const sigc::slot<void(DiscordError code)> &callback);
void UnbanUser(Snowflake guild_id, Snowflake user_id, const sigc::slot<void(DiscordError code)> &callback);
void DeleteInvite(const std::string &code, const sigc::slot<void(DiscordError code)> &callback);
void SetGuildName(Snowflake id, const Glib::ustring &name);
void SetGuildName(Snowflake id, const Glib::ustring &name, sigc::slot<void(bool success)> callback);
void SetGuildIcon(Snowflake id, const std::string &data);
void SetGuildIcon(Snowflake id, const std::string &data, sigc::slot<void(bool success)> callback);
void UnbanUser(Snowflake guild_id, Snowflake user_id);
void UnbanUser(Snowflake guild_id, Snowflake user_id, sigc::slot<void(bool success)> callback);
void DeleteInvite(const std::string &code);
void DeleteInvite(const std::string &code, sigc::slot<void(bool success)> callback);
void AddGroupDMRecipient(Snowflake channel_id, Snowflake user_id);
void RemoveGroupDMRecipient(Snowflake channel_id, Snowflake user_id);
void ModifyRolePermissions(Snowflake guild_id, Snowflake role_id, Permission permissions, const sigc::slot<void(DiscordError code)> &callback);
void ModifyRoleName(Snowflake guild_id, Snowflake role_id, const Glib::ustring &name, const sigc::slot<void(DiscordError code)> &callback);
void ModifyRoleColor(Snowflake guild_id, Snowflake role_id, uint32_t color, const sigc::slot<void(DiscordError code)> &callback);
void ModifyRoleColor(Snowflake guild_id, Snowflake role_id, const Gdk::RGBA &color, const sigc::slot<void(DiscordError code)> &callback);
void ModifyRolePosition(Snowflake guild_id, Snowflake role_id, int position, const sigc::slot<void(DiscordError code)> &callback);
void ModifyEmojiName(Snowflake guild_id, Snowflake emoji_id, const Glib::ustring &name, const sigc::slot<void(DiscordError code)> &callback);
void DeleteEmoji(Snowflake guild_id, Snowflake emoji_id, const sigc::slot<void(DiscordError code)> &callback);
void RemoveRelationship(Snowflake id, const sigc::slot<void(DiscordError code)> &callback);
void SendFriendRequest(const Glib::ustring &username, int discriminator, const sigc::slot<void(DiscordError code)> &callback);
void PutRelationship(Snowflake id, const sigc::slot<void(DiscordError code)> &callback); // send fr by id, accept incoming
void Pin(Snowflake channel_id, Snowflake message_id, const sigc::slot<void(DiscordError code)> &callback);
void Unpin(Snowflake channel_id, Snowflake message_id, const sigc::slot<void(DiscordError code)> &callback);
void LeaveThread(Snowflake channel_id, const std::string &location, const sigc::slot<void(DiscordError code)> &callback);
void ArchiveThread(Snowflake channel_id, const sigc::slot<void(DiscordError code)> &callback);
void UnArchiveThread(Snowflake channel_id, const sigc::slot<void(DiscordError code)> &callback);
void MarkChannelAsRead(Snowflake channel_id, const sigc::slot<void(DiscordError code)> &callback);
void MarkGuildAsRead(Snowflake guild_id, const sigc::slot<void(DiscordError code)> &callback);
void MuteChannel(Snowflake channel_id, const sigc::slot<void(DiscordError code)> &callback);
void UnmuteChannel(Snowflake channel_id, const sigc::slot<void(DiscordError code)> &callback);
void MuteGuild(Snowflake id, const sigc::slot<void(DiscordError code)> &callback);
void UnmuteGuild(Snowflake id, const sigc::slot<void(DiscordError code)> &callback);
void MuteThread(Snowflake id, const sigc::slot<void(DiscordError code)> &callback);
void UnmuteThread(Snowflake id, const sigc::slot<void(DiscordError code)> &callback);
void ModifyRolePermissions(Snowflake guild_id, Snowflake role_id, Permission permissions, sigc::slot<void(bool success)> callback);
void ModifyRoleName(Snowflake guild_id, Snowflake role_id, const Glib::ustring &name, sigc::slot<void(bool success)> callback);
void ModifyRoleColor(Snowflake guild_id, Snowflake role_id, uint32_t color, sigc::slot<void(bool success)> callback);
void ModifyRoleColor(Snowflake guild_id, Snowflake role_id, Gdk::RGBA color, sigc::slot<void(bool success)> callback);
void ModifyRolePosition(Snowflake guild_id, Snowflake role_id, int position, sigc::slot<void(bool success)> callback);
void ModifyEmojiName(Snowflake guild_id, Snowflake emoji_id, const Glib::ustring &name, sigc::slot<void(bool success)> callback);
void DeleteEmoji(Snowflake guild_id, Snowflake emoji_id, sigc::slot<void(bool success)> callback);
std::optional<GuildApplicationData> GetGuildApplication(Snowflake guild_id) const;
bool CanModifyRole(Snowflake guild_id, Snowflake role_id) const;
bool CanModifyRole(Snowflake guild_id, Snowflake role_id, Snowflake user_id) const;
// send op 8 to get member data for unknown members
template<typename Iter>
void RequestMembers(Snowflake guild_id, Iter begin, Iter end) {
if (std::distance(begin, end) == 0) return;
RequestGuildMembersMessage obj;
obj.GuildID = guild_id;
obj.Presences = false;
obj.UserIDs = { begin, end };
m_websocket.Send(obj);
}
// real client doesn't seem to use the single role endpoints so neither do we
template<typename Iter>
auto SetMemberRoles(Snowflake guild_id, Snowflake user_id, Iter begin, Iter end, const sigc::slot<void(DiscordError code)> &callback) {
auto SetMemberRoles(Snowflake guild_id, Snowflake user_id, Iter begin, Iter end, sigc::slot<void(bool success)> callback) {
ModifyGuildMemberObject obj;
obj.Roles = { begin, end };
m_http.MakePATCH("/guilds/" + std::to_string(guild_id) + "/members/" + std::to_string(user_id), nlohmann::json(obj).dump(), [this, callback](const http::response_type &response) {
if (CheckCode(response))
callback(DiscordError::NONE);
else
callback(GetCodeFromResponse(response));
callback(CheckCode(response, 200));
});
}
// FetchGuildBans fetches all bans+reasons via api, this func fetches stored bans (so usually just GUILD_BAN_ADD data)
std::vector<BanData> GetBansInGuild(Snowflake guild_id);
void FetchGuildBan(Snowflake guild_id, Snowflake user_id, const sigc::slot<void(BanData)> &callback);
void FetchGuildBans(Snowflake guild_id, const sigc::slot<void(std::vector<BanData>)> &callback);
void FetchGuildBan(Snowflake guild_id, Snowflake user_id, sigc::slot<void(BanData)> callback);
void FetchGuildBans(Snowflake guild_id, sigc::slot<void(std::vector<BanData>)> callback);
void FetchInvite(const std::string &code, const sigc::slot<void(std::optional<InviteData>)> &callback);
void FetchGuildInvites(Snowflake guild_id, const sigc::slot<void(std::vector<InviteData>)> &callback);
void FetchInvite(std::string code, sigc::slot<void(std::optional<InviteData>)> callback);
void FetchGuildInvites(Snowflake guild_id, sigc::slot<void(std::vector<InviteData>)> callback);
void FetchAuditLog(Snowflake guild_id, const sigc::slot<void(AuditLogData)> &callback);
void FetchAuditLog(Snowflake guild_id, sigc::slot<void(AuditLogData)> callback);
void FetchGuildEmojis(Snowflake guild_id, const sigc::slot<void(std::vector<EmojiData>)> &callback);
void FetchGuildEmojis(Snowflake guild_id, sigc::slot<void(std::vector<EmojiData>)> callback);
void FetchUserProfile(Snowflake user_id, const sigc::slot<void(UserProfileData)> &callback);
void FetchUserNote(Snowflake user_id, const sigc::slot<void(std::string note)> &callback);
void SetUserNote(Snowflake user_id, std::string note, const sigc::slot<void(DiscordError code)> &callback);
void FetchUserRelationships(Snowflake user_id, const sigc::slot<void(std::vector<UserData>)> &callback);
void FetchUserProfile(Snowflake user_id, sigc::slot<void(UserProfileData)> callback);
void FetchUserNote(Snowflake user_id, sigc::slot<void(std::string note)> callback);
void SetUserNote(Snowflake user_id, std::string note);
void SetUserNote(Snowflake user_id, std::string note, sigc::slot<void(bool success)> callback);
void FetchUserRelationships(Snowflake user_id, sigc::slot<void(std::vector<UserData>)> callback);
void FetchPinned(Snowflake id, const sigc::slot<void(std::vector<Message>, DiscordError code)> &callback);
void GetVerificationGateInfo(Snowflake guild_id, sigc::slot<void(std::optional<VerificationGateInfoObject>)> callback);
void AcceptVerificationGate(Snowflake guild_id, VerificationGateInfoObject info, sigc::slot<void(bool success)> callback);
bool IsVerificationRequired(Snowflake guild_id) const;
void GetVerificationGateInfo(Snowflake guild_id, const sigc::slot<void(std::optional<VerificationGateInfoObject>)> &callback);
void AcceptVerificationGate(Snowflake guild_id, VerificationGateInfoObject info, const sigc::slot<void(DiscordError code)> &callback);
void UpdateToken(const std::string &token);
void SetUserAgent(const std::string &agent);
bool IsChannelMuted(Snowflake id) const noexcept;
bool IsGuildMuted(Snowflake id) const noexcept;
int GetUnreadStateForChannel(Snowflake id) const noexcept;
bool GetUnreadStateForGuild(Snowflake id, int &total_mentions) const noexcept;
int GetUnreadDMsCount() const;
void UpdateToken(std::string token);
void SetUserAgent(std::string agent);
PresenceStatus GetUserStatus(Snowflake id) const;
std::map<Snowflake, RelationshipType> GetRelationships() const;
std::set<Snowflake> GetRelationships(RelationshipType type) const;
std::optional<RelationshipType> GetRelationship(Snowflake id) const;
std::unordered_set<Snowflake> GetRelationships(RelationshipType type) const;
private:
static const constexpr int InflateChunkSize = 0x10000;
@@ -223,11 +186,6 @@ private:
std::vector<uint8_t> m_decompress_buf;
z_stream m_zstream;
static std::string GetAPIURL();
static std::string GetGatewayURL();
static DiscordError GetCodeFromResponse(const http::response_type &response);
void ProcessNewGuild(GuildData &guild);
void HandleGatewayMessageRaw(std::string str);
@@ -264,18 +222,6 @@ private:
void HandleGatewayGuildJoinRequestCreate(const GatewayMessage &msg);
void HandleGatewayGuildJoinRequestUpdate(const GatewayMessage &msg);
void HandleGatewayGuildJoinRequestDelete(const GatewayMessage &msg);
void HandleGatewayRelationshipRemove(const GatewayMessage &msg);
void HandleGatewayRelationshipAdd(const GatewayMessage &msg);
void HandleGatewayThreadCreate(const GatewayMessage &msg);
void HandleGatewayThreadDelete(const GatewayMessage &msg);
void HandleGatewayThreadListSync(const GatewayMessage &msg);
void HandleGatewayThreadMembersUpdate(const GatewayMessage &msg);
void HandleGatewayThreadMemberUpdate(const GatewayMessage &msg);
void HandleGatewayThreadUpdate(const GatewayMessage &msg);
void HandleGatewayThreadMemberListUpdate(const GatewayMessage &msg);
void HandleGatewayMessageAck(const GatewayMessage &msg);
void HandleGatewayUserGuildSettingsUpdate(const GatewayMessage &msg);
void HandleGatewayGuildMembersChunk(const GatewayMessage &msg);
void HandleGatewayReadySupplemental(const GatewayMessage &msg);
void HandleGatewayReconnect(const GatewayMessage &msg);
void HandleGatewayInvalidSession(const GatewayMessage &msg);
@@ -286,29 +232,25 @@ private:
void HandleSocketOpen();
void HandleSocketClose(uint16_t code);
static bool CheckCode(const http::response_type &r);
static bool CheckCode(const http::response_type &r, int expected);
bool CheckCode(const http::response_type &r);
bool CheckCode(const http::response_type &r, int expected);
void StoreMessageData(Message &msg);
void HandleReadyReadState(const ReadyEventData &data);
void HandleReadyGuildSettings(const ReadyEventData &data);
std::string m_token;
void AddMessageToChannel(Snowflake msg_id, Snowflake channel_id);
std::unordered_map<Snowflake, std::unordered_set<Snowflake>> m_chan_to_message_map;
void AddUserToGuild(Snowflake user_id, Snowflake guild_id);
std::map<Snowflake, std::set<Snowflake>> m_guild_to_users;
std::map<Snowflake, std::set<Snowflake>> m_guild_to_channels;
std::map<Snowflake, GuildApplicationData> m_guild_join_requests;
std::map<Snowflake, PresenceStatus> m_user_to_status;
std::map<Snowflake, RelationshipType> m_user_relationships;
std::set<Snowflake> m_joined_threads;
std::map<Snowflake, std::vector<Snowflake>> m_thread_members;
std::map<Snowflake, Snowflake> m_last_message_id;
std::unordered_set<Snowflake> m_muted_guilds;
std::unordered_set<Snowflake> m_muted_channels;
std::unordered_map<Snowflake, int> m_unread;
std::unordered_set<Snowflake> m_channel_muted_parent;
std::unordered_map<Snowflake, std::unordered_set<Snowflake>> m_guild_to_users;
std::unordered_map<Snowflake, std::unordered_set<Snowflake>> m_guild_to_channels;
std::unordered_map<Snowflake, GuildApplicationData> m_guild_join_requests;
std::unordered_map<Snowflake, PresenceStatus> m_user_to_status;
std::unordered_map<Snowflake, RelationshipType> m_user_relationships;
UserData m_user_data;
UserSettings m_user_settings;
@@ -318,9 +260,8 @@ private:
Websocket m_websocket;
std::atomic<bool> m_client_connected = false;
std::atomic<bool> m_ready_received = false;
bool m_client_started = false;
std::map<std::string, GatewayEvent> m_event_map;
std::unordered_map<std::string, GatewayEvent> m_event_map;
void LoadEventMap();
std::thread m_heartbeat_thread;
@@ -342,9 +283,6 @@ private:
Glib::Dispatcher m_generic_dispatch;
std::queue<std::function<void()>> m_generic_queue;
std::set<Snowflake> m_channels_pinned_requested;
std::set<Snowflake> m_channels_lazy_loaded;
// signals
public:
typedef sigc::signal<void> type_signal_gateway_ready;
@@ -356,7 +294,7 @@ public:
typedef sigc::signal<void, Snowflake> type_signal_guild_delete;
typedef sigc::signal<void, Snowflake> type_signal_channel_delete;
typedef sigc::signal<void, Snowflake> type_signal_channel_update;
typedef sigc::signal<void, ChannelData> type_signal_channel_create;
typedef sigc::signal<void, Snowflake> type_signal_channel_create;
typedef sigc::signal<void, Snowflake> type_signal_guild_update;
typedef sigc::signal<void, Snowflake, Snowflake> type_signal_role_update; // guild id, role id
typedef sigc::signal<void, Snowflake, Snowflake> type_signal_role_create; // guild id, role id
@@ -369,36 +307,15 @@ public:
typedef sigc::signal<void, Snowflake, Snowflake> type_signal_guild_ban_add; // guild id, user id
typedef sigc::signal<void, InviteData> type_signal_invite_create;
typedef sigc::signal<void, InviteDeleteObject> type_signal_invite_delete;
typedef sigc::signal<void, UserData, PresenceStatus> type_signal_presence_update;
typedef sigc::signal<void, Snowflake, PresenceStatus> type_signal_presence_update;
typedef sigc::signal<void, Snowflake, std::string> type_signal_note_update;
typedef sigc::signal<void, Snowflake, std::vector<EmojiData>> type_signal_guild_emojis_update; // guild id
typedef sigc::signal<void, GuildJoinRequestCreateData> type_signal_guild_join_request_create;
typedef sigc::signal<void, GuildJoinRequestUpdateData> type_signal_guild_join_request_update;
typedef sigc::signal<void, GuildJoinRequestDeleteData> type_signal_guild_join_request_delete;
typedef sigc::signal<void, Snowflake, RelationshipType> type_signal_relationship_remove;
typedef sigc::signal<void, RelationshipAddData> type_signal_relationship_add;
typedef sigc::signal<void, ChannelData> type_signal_thread_create;
typedef sigc::signal<void, ThreadDeleteData> type_signal_thread_delete;
typedef sigc::signal<void, ThreadListSyncData> type_signal_thread_list_sync;
typedef sigc::signal<void, ThreadMembersUpdateData> type_signal_thread_members_update;
typedef sigc::signal<void, ThreadUpdateData> type_signal_thread_update;
typedef sigc::signal<void, ThreadMemberListUpdateData> type_signal_thread_member_list_update;
typedef sigc::signal<void, MessageAckData> type_signal_message_ack;
typedef sigc::signal<void, GuildMembersChunkData> type_signal_guild_members_chunk;
// not discord dispatch events
typedef sigc::signal<void, Snowflake> type_signal_added_to_thread;
typedef sigc::signal<void, Snowflake> type_signal_removed_from_thread;
typedef sigc::signal<void, Message> type_signal_message_unpinned;
typedef sigc::signal<void, Message> type_signal_message_pinned;
typedef sigc::signal<void, Message> type_signal_message_sent;
typedef sigc::signal<void, Snowflake> type_signal_channel_muted;
typedef sigc::signal<void, Snowflake> type_signal_channel_unmuted;
typedef sigc::signal<void, Snowflake> type_signal_guild_muted;
typedef sigc::signal<void, Snowflake> type_signal_guild_unmuted;
typedef sigc::signal<void, std::string /* nonce */, float /* retry_after */> type_signal_message_send_fail; // retry after param will be 0 if it failed for a reason that isnt slowmode
typedef sigc::signal<void, bool, GatewayCloseCode> type_signal_disconnected; // bool true if reconnecting
typedef sigc::signal<void, bool, GatewayCloseCode> type_signal_disconnected; // bool true if reconnecting
typedef sigc::signal<void> type_signal_connected;
type_signal_gateway_ready signal_gateway_ready();
@@ -429,26 +346,7 @@ public:
type_signal_guild_join_request_create signal_guild_join_request_create();
type_signal_guild_join_request_update signal_guild_join_request_update();
type_signal_guild_join_request_delete signal_guild_join_request_delete();
type_signal_relationship_remove signal_relationship_remove();
type_signal_relationship_add signal_relationship_add();
type_signal_message_unpinned signal_message_unpinned();
type_signal_message_pinned signal_message_pinned();
type_signal_thread_create signal_thread_create();
type_signal_thread_delete signal_thread_delete();
type_signal_thread_list_sync signal_thread_list_sync();
type_signal_thread_members_update signal_thread_members_update();
type_signal_thread_update signal_thread_update();
type_signal_thread_member_list_update signal_thread_member_list_update();
type_signal_message_ack signal_message_ack();
type_signal_guild_members_chunk signal_guild_members_chunk();
type_signal_added_to_thread signal_added_to_thread();
type_signal_removed_from_thread signal_removed_from_thread();
type_signal_message_sent signal_message_sent();
type_signal_channel_muted signal_channel_muted();
type_signal_channel_unmuted signal_channel_unmuted();
type_signal_guild_muted signal_guild_muted();
type_signal_guild_unmuted signal_guild_unmuted();
type_signal_message_send_fail signal_message_send_fail();
type_signal_disconnected signal_disconnected();
type_signal_connected signal_connected();
@@ -482,26 +380,7 @@ protected:
type_signal_guild_join_request_create m_signal_guild_join_request_create;
type_signal_guild_join_request_update m_signal_guild_join_request_update;
type_signal_guild_join_request_delete m_signal_guild_join_request_delete;
type_signal_relationship_remove m_signal_relationship_remove;
type_signal_relationship_add m_signal_relationship_add;
type_signal_message_unpinned m_signal_message_unpinned;
type_signal_message_pinned m_signal_message_pinned;
type_signal_thread_create m_signal_thread_create;
type_signal_thread_delete m_signal_thread_delete;
type_signal_thread_list_sync m_signal_thread_list_sync;
type_signal_thread_members_update m_signal_thread_members_update;
type_signal_thread_update m_signal_thread_update;
type_signal_thread_member_list_update m_signal_thread_member_list_update;
type_signal_message_ack m_signal_message_ack;
type_signal_guild_members_chunk m_signal_guild_members_chunk;
type_signal_removed_from_thread m_signal_removed_from_thread;
type_signal_added_to_thread m_signal_added_to_thread;
type_signal_message_sent m_signal_message_sent;
type_signal_channel_muted m_signal_channel_muted;
type_signal_channel_unmuted m_signal_channel_unmuted;
type_signal_guild_muted m_signal_guild_muted;
type_signal_guild_unmuted m_signal_guild_unmuted;
type_signal_message_send_fail m_signal_message_send_fail;
type_signal_disconnected m_signal_disconnected;
type_signal_connected m_signal_connected;

View File

@@ -16,7 +16,7 @@ void to_json(nlohmann::json &j, const EmojiData &m) {
j["id"] = m.ID;
else
j["id"] = nullptr;
if (!m.Name.empty())
if (m.Name != "")
j["name"] = m.Name;
else
j["name"] = nullptr;

View File

@@ -1,5 +1,5 @@
#include "guild.hpp"
#include "abaddon.hpp"
#include "../abaddon.hpp"
void from_json(const nlohmann::json &j, GuildData &m) {
JS_D("id", m.ID);
@@ -43,7 +43,6 @@ void from_json(const nlohmann::json &j, GuildData &m) {
// JS_O("voice_states", m.VoiceStates);
// JS_O("members", m.Members);
JS_O("channels", m.Channels);
JS_O("threads", m.Threads);
// JS_O("presences", m.Presences);
JS_ON("max_presences", m.MaxPresences);
JS_O("max_members", m.MaxMembers);
@@ -77,7 +76,7 @@ void GuildData::update_from_json(const nlohmann::json &j) {
JS_RD("owner_id", OwnerID);
std::string tmp;
JS_RD("permissions", tmp);
if (!tmp.empty())
if (tmp != "")
Permissions = std::stoull(tmp);
JS_RD("region", VoiceRegion);
JS_RD("afk_channel_id", AFKChannelID);
@@ -119,7 +118,7 @@ void GuildData::update_from_json(const nlohmann::json &j) {
JS_RD("approximate_presence_count", ApproximatePresenceCount);
}
bool GuildData::HasFeature(const std::string &search_feature) const {
bool GuildData::HasFeature(const std::string &search_feature) {
if (!Features.has_value()) return false;
for (const auto &feature : *Features)
if (search_feature == feature)
@@ -128,17 +127,77 @@ bool GuildData::HasFeature(const std::string &search_feature) const {
}
bool GuildData::HasIcon() const {
return !Icon.empty();
return Icon != "";
}
bool GuildData::HasAnimatedIcon() const {
return HasIcon() && Icon[0] == 'a' && Icon[1] == '_';
}
std::string GuildData::GetIconURL(const std::string &ext, const std::string &size) const {
std::string GuildData::GetIconURL(std::string ext, std::string size) const {
return "https://cdn.discordapp.com/icons/" + std::to_string(ID) + "/" + Icon + "." + ext + "?size=" + size;
}
std::vector<Snowflake> GuildData::GetSortedChannels(Snowflake ignore) const {
std::vector<Snowflake> ret;
const auto &discord = Abaddon::Get().GetDiscordClient();
auto channels = discord.GetChannelsInGuild(ID);
std::unordered_map<Snowflake, std::vector<ChannelData>> category_to_channels;
std::map<int, std::vector<ChannelData>> position_to_categories;
std::map<int, std::vector<ChannelData>> orphan_channels;
for (const auto &channel_id : channels) {
const auto data = discord.GetChannel(channel_id);
if (!data->ParentID.has_value() && (data->Type == ChannelType::GUILD_TEXT || data->Type == ChannelType::GUILD_NEWS))
orphan_channels[*data->Position].push_back(*data);
else if (data->ParentID.has_value() && (data->Type == ChannelType::GUILD_TEXT || data->Type == ChannelType::GUILD_NEWS))
category_to_channels[*data->ParentID].push_back(*data);
else if (data->Type == ChannelType::GUILD_CATEGORY)
position_to_categories[*data->Position].push_back(*data);
}
for (auto &[pos, channels] : orphan_channels) {
std::sort(channels.begin(), channels.end(), [&](const ChannelData &a, const ChannelData &b) -> bool {
return a.ID < b.ID;
});
for (const auto &chan : channels)
ret.push_back(chan.ID);
}
for (auto &[pos, categories] : position_to_categories) {
std::sort(categories.begin(), categories.end(), [&](const ChannelData &a, const ChannelData &b) -> bool {
return a.ID < b.ID;
});
for (const auto &category : categories) {
ret.push_back(category.ID);
if (ignore == category.ID) continue; // stupid hack to save me some time
auto it = category_to_channels.find(category.ID);
if (it == category_to_channels.end()) continue;
auto &channels = it->second;
std::sort(channels.begin(), channels.end(), [&](const ChannelData &a, const ChannelData &b) -> bool {
return a.Position < b.Position;
});
for (auto &channel : channels) {
ret.push_back(channel.ID);
}
}
}
return ret;
}
std::vector<RoleData> GuildData::FetchRoles() const {
if (!Roles.has_value()) return {};
std::vector<RoleData> ret;
for (const auto thing : *Roles)
ret.push_back(*Abaddon::Get().GetDiscordClient().GetRole(thing.ID));
std::sort(ret.begin(), ret.end(), [](const RoleData &a, const RoleData &b) -> bool {
return a.Position > b.Position;
});
return ret;
}
void from_json(const nlohmann::json &j, GuildApplicationData &m) {
JS_D("user_id", m.UserID);
JS_D("guild_id", m.GuildID);

View File

@@ -50,7 +50,7 @@ struct GuildData {
std::optional<int> VerificationLevel;
std::optional<int> DefaultMessageNotifications;
std::optional<int> ExplicitContentFilter;
std::optional<std::vector<RoleData>> Roles;
std::optional<std::vector<RoleData>> Roles; // only access id
std::optional<std::vector<EmojiData>> Emojis; // only access id
std::optional<std::unordered_set<std::string>> Features;
std::optional<int> MFALevel;
@@ -80,7 +80,6 @@ struct GuildData {
std::optional<int> MaxVideoChannelUsers;
std::optional<int> ApproximateMemberCount;
std::optional<int> ApproximatePresenceCount;
std::optional<std::vector<ChannelData>> Threads; // only with permissions to view, id only
// undocumented
// std::map<std::string, Unknown> GuildHashes;
@@ -91,8 +90,10 @@ struct GuildData {
friend void from_json(const nlohmann::json &j, GuildData &m);
void update_from_json(const nlohmann::json &j);
bool HasFeature(const std::string &feature) const;
bool HasFeature(const std::string &feature);
bool HasIcon() const;
bool HasAnimatedIcon() const;
std::string GetIconURL(const std::string &ext = "png", const std::string &size = "32") const;
std::string GetIconURL(std::string ext = "png", std::string size = "32") const;
std::vector<Snowflake> GetSortedChannels(Snowflake ignore = Snowflake::Invalid) const;
std::vector<RoleData> FetchRoles() const; // sorted
};

View File

@@ -1,30 +1,25 @@
#include "httpclient.hpp"
#include <utility>
//#define USE_LOCAL_PROXY
HTTPClient::HTTPClient() {
HTTPClient::HTTPClient(std::string api_base)
: m_api_base(api_base) {
m_dispatcher.connect(sigc::mem_fun(*this, &HTTPClient::RunCallbacks));
}
void HTTPClient::SetBase(const std::string &url) {
m_api_base = url;
}
void HTTPClient::SetUserAgent(std::string agent) {
m_agent = std::move(agent);
m_agent = agent;
}
void HTTPClient::SetAuth(std::string auth) {
m_authorization = std::move(auth);
m_authorization = auth;
}
void HTTPClient::MakeDELETE(const std::string &path, const std::function<void(http::response_type r)> &cb) {
void HTTPClient::MakeDELETE(const std::string &path, std::function<void(http::response_type r)> cb) {
printf("DELETE %s\n", path.c_str());
m_futures.push_back(std::async(std::launch::async, [this, path, cb] {
http::request req(http::REQUEST_DELETE, m_api_base + path);
req.set_header("Authorization", m_authorization);
req.set_user_agent(!m_agent.empty() ? m_agent : "Abaddon");
req.set_user_agent(m_agent != "" ? m_agent : "Abaddon");
#ifdef USE_LOCAL_PROXY
req.set_proxy("http://127.0.0.1:8888");
req.set_verify_ssl(false);
@@ -36,13 +31,13 @@ void HTTPClient::MakeDELETE(const std::string &path, const std::function<void(ht
}));
}
void HTTPClient::MakePATCH(const std::string &path, const std::string &payload, const std::function<void(http::response_type r)> &cb) {
void HTTPClient::MakePATCH(const std::string &path, const std::string &payload, std::function<void(http::response_type r)> cb) {
printf("PATCH %s\n", path.c_str());
m_futures.push_back(std::async(std::launch::async, [this, path, cb, payload] {
http::request req(http::REQUEST_PATCH, m_api_base + path);
req.set_header("Authorization", m_authorization);
req.set_header("Content-Type", "application/json");
req.set_user_agent(!m_agent.empty() ? m_agent : "Abaddon");
req.set_user_agent(m_agent != "" ? m_agent : "Abaddon");
req.set_body(payload);
#ifdef USE_LOCAL_PROXY
req.set_proxy("http://127.0.0.1:8888");
@@ -55,13 +50,13 @@ void HTTPClient::MakePATCH(const std::string &path, const std::string &payload,
}));
}
void HTTPClient::MakePOST(const std::string &path, const std::string &payload, const std::function<void(http::response_type r)> &cb) {
void HTTPClient::MakePOST(const std::string &path, const std::string &payload, std::function<void(http::response_type r)> cb) {
printf("POST %s\n", path.c_str());
m_futures.push_back(std::async(std::launch::async, [this, path, cb, payload] {
http::request req(http::REQUEST_POST, m_api_base + path);
req.set_header("Authorization", m_authorization);
req.set_header("Content-Type", "application/json");
req.set_user_agent(!m_agent.empty() ? m_agent : "Abaddon");
req.set_user_agent(m_agent != "" ? m_agent : "Abaddon");
req.set_body(payload);
#ifdef USE_LOCAL_PROXY
req.set_proxy("http://127.0.0.1:8888");
@@ -74,14 +69,14 @@ void HTTPClient::MakePOST(const std::string &path, const std::string &payload, c
}));
}
void HTTPClient::MakePUT(const std::string &path, const std::string &payload, const std::function<void(http::response_type r)> &cb) {
void HTTPClient::MakePUT(const std::string &path, const std::string &payload, std::function<void(http::response_type r)> cb) {
printf("PUT %s\n", path.c_str());
m_futures.push_back(std::async(std::launch::async, [this, path, cb, payload] {
http::request req(http::REQUEST_PUT, m_api_base + path);
req.set_header("Authorization", m_authorization);
if (!payload.empty())
if (payload != "")
req.set_header("Content-Type", "application/json");
req.set_user_agent(!m_agent.empty() ? m_agent : "Abaddon");
req.set_user_agent(m_agent != "" ? m_agent : "Abaddon");
req.set_body(payload);
#ifdef USE_LOCAL_PROXY
req.set_proxy("http://127.0.0.1:8888");
@@ -94,13 +89,13 @@ void HTTPClient::MakePUT(const std::string &path, const std::string &payload, co
}));
}
void HTTPClient::MakeGET(const std::string &path, const std::function<void(http::response_type r)> &cb) {
void HTTPClient::MakeGET(const std::string &path, std::function<void(http::response_type r)> cb) {
printf("GET %s\n", path.c_str());
m_futures.push_back(std::async(std::launch::async, [this, path, cb] {
http::request req(http::REQUEST_GET, m_api_base + path);
req.set_header("Authorization", m_authorization);
req.set_header("Content-Type", "application/json");
req.set_user_agent(!m_agent.empty() ? m_agent : "Abaddon");
req.set_user_agent(m_agent != "" ? m_agent : "Abaddon");
#ifdef USE_LOCAL_PROXY
req.set_proxy("http://127.0.0.1:8888");
req.set_verify_ssl(false);
@@ -128,11 +123,11 @@ void HTTPClient::RunCallbacks() {
m_mutex.unlock();
}
void HTTPClient::OnResponse(const http::response_type &r, const std::function<void(http::response_type r)> &cb) {
void HTTPClient::OnResponse(const http::response_type &r, std::function<void(http::response_type r)> cb) {
CleanupFutures();
try {
m_mutex.lock();
m_queue.push([r, cb] { cb(r); });
m_queue.push([this, r, cb] { cb(r); });
m_dispatcher.emit();
m_mutex.unlock();
} catch (const std::exception &e) {

View File

@@ -7,24 +7,22 @@
#include <mutex>
#include <queue>
#include <glibmm.h>
#include "http.hpp"
#include "../http.hpp"
class HTTPClient {
public:
HTTPClient();
void SetBase(const std::string &url);
HTTPClient(std::string api_base);
void SetUserAgent(std::string agent);
void SetAuth(std::string auth);
void MakeDELETE(const std::string &path, const std::function<void(http::response_type r)> &cb);
void MakeGET(const std::string &path, const std::function<void(http::response_type r)> &cb);
void MakePATCH(const std::string &path, const std::string &payload, const std::function<void(http::response_type r)> &cb);
void MakePOST(const std::string &path, const std::string &payload, const std::function<void(http::response_type r)> &cb);
void MakePUT(const std::string &path, const std::string &payload, const std::function<void(http::response_type r)> &cb);
void MakeDELETE(const std::string &path, std::function<void(http::response_type r)> cb);
void MakeGET(const std::string &path, std::function<void(http::response_type r)> cb);
void MakePATCH(const std::string &path, const std::string &payload, std::function<void(http::response_type r)> cb);
void MakePOST(const std::string &path, const std::string &payload, std::function<void(http::response_type r)> cb);
void MakePUT(const std::string &path, const std::string &payload, std::function<void(http::response_type r)> cb);
private:
void OnResponse(const http::response_type &r, const std::function<void(http::response_type r)> &cb);
void OnResponse(const http::response_type &r, std::function<void(http::response_type r)> cb);
void CleanupFutures();
mutable std::mutex m_mutex;

View File

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

View File

@@ -14,10 +14,10 @@ enum class InteractionType {
};
struct MessageInteractionData {
Snowflake ID; // id of the interaction
InteractionType Type; // the type of interaction
std::string Name; // the name of the ApplicationCommand
UserData User; // the user who invoked the interaction
Snowflake ID; // id of the interaction
InteractionType Type; // the type of interaction
std::string Name; // the name of the ApplicationCommand
UserData User; // the user who invoked the interaction
// undocumented???
std::optional<GuildMember> Member; // the member who invoked the interaction (in a guild)

View File

@@ -1,7 +1,7 @@
#pragma once
#include <nlohmann/json.hpp>
#include <optional>
#include "util.hpp"
#include "../util.hpp"
namespace detail { // more or less because idk what to name this stuff
template<typename T>
@@ -141,8 +141,8 @@ inline void json_update_optional_nullable_default(const ::nlohmann::json &j, con
} while (0)
// set a json value from a std::optional only if it has a value
#define JS_IF(k, v) \
do { \
if ((v).has_value()) \
j[k] = *(v); \
#define JS_IF(k, v) \
do { \
if (v.has_value()) \
j[k] = *v; \
} while (0)

View File

@@ -1,5 +1,5 @@
#include "member.hpp"
#include "abaddon.hpp"
#include "../abaddon.hpp"
void from_json(const nlohmann::json &j, GuildMember &m) {
JS_O("user", m.User);
@@ -10,8 +10,6 @@ void from_json(const nlohmann::json &j, GuildMember &m) {
JS_D("deaf", m.IsDeafened);
JS_D("mute", m.IsMuted);
JS_O("user_id", m.UserID);
JS_ON("avatar", m.Avatar);
JS_O("pending", m.IsPending);
}
std::vector<RoleData> GuildMember::GetSortedRoles() const {
@@ -19,7 +17,7 @@ std::vector<RoleData> GuildMember::GetSortedRoles() const {
for (const auto role_id : Roles) {
const auto role = Abaddon::Get().GetDiscordClient().GetRole(role_id);
if (!role.has_value()) continue;
roles.push_back(*role);
roles.push_back(std::move(*role));
}
std::sort(roles.begin(), roles.end(), [](const RoleData &a, const RoleData &b) {
@@ -35,6 +33,4 @@ void GuildMember::update_from_json(const nlohmann::json &j) {
JS_RD("nick", Nickname);
JS_RD("joined_at", JoinedAt);
JS_RD("premium_since", PremiumSince);
JS_RD("avatar", Avatar);
JS_RD("pending", IsPending);
}

View File

@@ -8,19 +8,15 @@
struct GuildMember {
std::optional<UserData> User; // only reliable to access id. only opt in MESSAGE_*
std::string Nickname;
std::string Nickname; // null
std::vector<Snowflake> Roles;
std::string JoinedAt;
std::optional<std::string> PremiumSince; // null
bool IsDeafened;
bool IsMuted;
std::optional<Snowflake> UserID; // present in merged_members
std::optional<bool> IsPending; // this uses `pending` not `is_pending`
// undocuemtned moment !!!1
std::optional<std::string> Avatar;
[[nodiscard]] std::vector<RoleData> GetSortedRoles() const;
std::vector<RoleData> GetSortedRoles() const;
void update_from_json(const nlohmann::json &j);
friend void from_json(const nlohmann::json &j, GuildMember &m);

View File

@@ -176,7 +176,7 @@ void to_json(nlohmann::json &j, const MessageApplicationData &m) {
j["id"] = m.ID;
JS_IF("cover_image", m.CoverImage);
j["description"] = m.Description;
if (m.Icon.empty())
if (m.Icon == "")
j["icon"] = nullptr;
else
j["icon"] = m.Icon;
@@ -218,7 +218,6 @@ void from_json(const nlohmann::json &j, Message &m) {
m.ReferencedMessage = nullptr;
}
JS_O("interaction", m.Interaction);
JS_O("sticker_items", m.StickerItems);
}
void Message::from_json_edited(const nlohmann::json &j) {
@@ -230,7 +229,7 @@ void Message::from_json_edited(const nlohmann::json &j) {
JS_O("content", Content);
JS_O("timestamp", Timestamp);
JS_ON("edited_timestamp", EditedTimestamp);
if (!EditedTimestamp.empty())
if (EditedTimestamp.size() > 0)
SetEdited();
JS_O("tts", IsTTS);
JS_O("mention_everyone", DoesMentionEveryone);
@@ -245,7 +244,6 @@ void Message::from_json_edited(const nlohmann::json &j) {
JS_O("flags", Flags);
JS_O("stickers", Stickers);
JS_O("interaction", Interaction);
JS_O("sticker_items", StickerItems);
}
void Message::SetDeleted() {
@@ -263,9 +261,3 @@ bool Message::IsDeleted() const {
bool Message::IsEdited() const {
return m_edited;
}
bool Message::DoesMention(Snowflake id) const noexcept {
return std::any_of(Mentions.begin(), Mentions.end(), [id](const UserData &user) {
return user.ID == id;
});
}

View File

@@ -27,7 +27,7 @@ enum class MessageType {
GUILD_DISCOVERY_REQUALIFIED = 15, // yep
GUILD_DISCOVERY_GRACE_PERIOD_INITIAL_WARNING = 16, // yep
GUILD_DISCOVERY_GRACE_PERIOD_FINAL_WARNING = 17, // yep
THREAD_CREATED = 18, // yep
THREAD_CREATED = 18, // nope
INLINE_REPLY = 19, // yep
APPLICATION_COMMAND = 20, // yep
THREAD_STARTER_MESSAGE = 21, // nope
@@ -199,7 +199,6 @@ struct Message {
std::optional<std::vector<StickerData>> Stickers;
std::optional<std::shared_ptr<Message>> ReferencedMessage; // has_value && null means deleted
std::optional<MessageInteractionData> Interaction;
std::optional<std::vector<StickerItem>> StickerItems;
friend void from_json(const nlohmann::json &j, Message &m);
void from_json_edited(const nlohmann::json &j); // for MESSAGE_UPDATE
@@ -209,10 +208,8 @@ struct Message {
void SetDeleted();
void SetEdited();
[[nodiscard]] bool IsDeleted() const;
[[nodiscard]] bool IsEdited() const;
[[nodiscard]] bool DoesMention(Snowflake id) const noexcept;
bool IsDeleted() const;
bool IsEdited() const;
private:
bool m_deleted = false;

View File

@@ -77,7 +77,7 @@ void from_json(const nlohmann::json &j, GuildMemberListUpdateMessage &m) {
}
void to_json(nlohmann::json &j, const LazyLoadRequestMessage &m) {
j["op"] = GatewayOp::GuildSubscriptions;
j["op"] = GatewayOp::LazyLoadRequest;
j["d"] = nlohmann::json::object();
j["d"]["guild_id"] = m.GuildID;
if (m.Channels.has_value()) {
@@ -85,20 +85,14 @@ void to_json(nlohmann::json &j, const LazyLoadRequestMessage &m) {
for (const auto &[key, chans] : *m.Channels)
j["d"]["channels"][std::to_string(key)] = chans;
}
if (m.ShouldGetTyping)
j["d"]["typing"] = *m.ShouldGetTyping;
if (m.ShouldGetActivities)
j["d"]["activities"] = *m.ShouldGetActivities;
if (m.ShouldGetThreads)
j["d"]["threads"] = *m.ShouldGetThreads;
j["d"]["typing"] = m.ShouldGetTyping;
j["d"]["activities"] = m.ShouldGetActivities;
if (m.Members.has_value())
j["d"]["members"] = *m.Members;
if (m.ThreadIDs.has_value())
j["d"]["thread_member_lists"] = *m.ThreadIDs;
}
void to_json(nlohmann::json &j, const UpdateStatusMessage &m) {
j["op"] = GatewayOp::PresenceUpdate;
j["op"] = GatewayOp::UpdateStatus;
j["d"] = nlohmann::json::object();
j["d"]["since"] = m.Since;
j["d"]["activities"] = m.Activities;
@@ -108,7 +102,7 @@ void to_json(nlohmann::json &j, const UpdateStatusMessage &m) {
j["d"]["status"] = "online";
break;
case PresenceStatus::Offline:
j["d"]["status"] = "invisible";
j["d"]["status"] = "offline";
break;
case PresenceStatus::Idle:
j["d"]["status"] = "idle";
@@ -119,92 +113,6 @@ void to_json(nlohmann::json &j, const UpdateStatusMessage &m) {
}
}
void to_json(nlohmann::json &j, const RequestGuildMembersMessage &m) {
j["op"] = GatewayOp::RequestGuildMembers;
j["d"] = nlohmann::json::object();
j["d"]["guild_id"] = m.GuildID;
j["d"]["presences"] = m.Presences;
j["d"]["user_ids"] = m.UserIDs;
}
void from_json(const nlohmann::json &j, ReadStateEntry &m) {
JS_ON("mention_count", m.MentionCount);
JS_ON("last_message_id", m.LastMessageID);
JS_D("id", m.ID);
}
void to_json(nlohmann::json &j, const ReadStateEntry &m) {
j["channel_id"] = m.ID;
j["message_id"] = m.LastMessageID;
}
void from_json(const nlohmann::json &j, ReadStateData &m) {
JS_ON("version", m.Version);
JS_ON("partial", m.IsPartial);
JS_ON("entries", m.Entries);
}
void from_json(const nlohmann::json &j, UserGuildSettingsChannelOverride &m) {
JS_D("muted", m.Muted);
JS_D("message_notifications", m.MessageNotifications);
JS_D("collapsed", m.Collapsed);
JS_D("channel_id", m.ChannelID);
JS_N("mute_config", m.MuteConfig);
}
void to_json(nlohmann::json &j, const UserGuildSettingsChannelOverride &m) {
j["channel_id"] = m.ChannelID;
j["collapsed"] = m.Collapsed;
j["message_notifications"] = m.MessageNotifications;
j["mute_config"] = m.MuteConfig;
j["muted"] = m.Muted;
}
void from_json(const nlohmann::json &j, MuteConfigData &m) {
JS_ON("end_time", m.EndTime);
JS_ON("selected_time_window", m.SelectedTimeWindow);
}
void to_json(nlohmann::json &j, const MuteConfigData &m) {
if (m.EndTime.has_value())
j["end_time"] = *m.EndTime;
else
j["end_time"] = nullptr;
j["selected_time_window"] = m.SelectedTimeWindow;
}
void from_json(const nlohmann::json &j, UserGuildSettingsEntry &m) {
JS_D("version", m.Version);
JS_D("suppress_roles", m.SuppressRoles);
JS_D("suppress_everyone", m.SuppressEveryone);
JS_D("muted", m.Muted);
JS_D("mobile_push", m.MobilePush);
JS_D("message_notifications", m.MessageNotifications);
JS_D("hide_muted_channels", m.HideMutedChannels);
JS_N("guild_id", m.GuildID);
JS_D("channel_overrides", m.ChannelOverrides);
JS_N("mute_config", m.MuteConfig);
}
void to_json(nlohmann::json &j, const UserGuildSettingsEntry &m) {
j["channel_overrides"] = m.ChannelOverrides;
j["guild_id"] = m.GuildID;
j["hide_muted_channels"] = m.HideMutedChannels;
j["message_notifications"] = m.MessageNotifications;
j["mobile_push"] = m.MobilePush;
j["mute_config"] = m.MuteConfig;
j["muted"] = m.Muted;
j["suppress_everyone"] = m.SuppressEveryone;
j["suppress_roles"] = m.SuppressRoles;
j["version"] = m.Version;
}
void from_json(const nlohmann::json &j, UserGuildSettingsData &m) {
JS_D("version", m.Version);
JS_D("partial", m.IsPartial);
JS_D("entries", m.Entries);
}
void from_json(const nlohmann::json &j, ReadyEventData &m) {
JS_D("v", m.GatewayVersion);
JS_D("user", m.SelfUser);
@@ -218,8 +126,6 @@ void from_json(const nlohmann::json &j, ReadyEventData &m) {
JS_ON("merged_members", m.MergedMembers);
JS_O("relationships", m.Relationships);
JS_O("guild_join_requests", m.GuildJoinRequests);
JS_O("read_state", m.ReadState);
JS_D("user_guild_settings", m.GuildSettings);
}
void from_json(const nlohmann::json &j, MergedPresence &m) {
@@ -251,7 +157,7 @@ void to_json(nlohmann::json &j, const IdentifyProperties &m) {
j["referring_domain_current"] = m.ReferringDomainCurrent;
j["release_channel"] = m.ReleaseChannel;
j["client_build_number"] = m.ClientBuildNumber;
if (m.ClientEventSource.empty())
if (m.ClientEventSource == "")
j["client_event_source"] = nullptr;
else
j["client_event_source"] = m.ClientEventSource;
@@ -290,7 +196,7 @@ void to_json(nlohmann::json &j, const CreateMessageObject &m) {
}
void to_json(nlohmann::json &j, const MessageEditObject &m) {
if (!m.Content.empty())
if (m.Content.size() > 0)
j["content"] = m.Content;
// todo EmbedData to_json
@@ -546,96 +452,3 @@ void from_json(const nlohmann::json &j, RateLimitedResponse &m) {
JS_O("message", m.Message);
JS_D("retry_after", m.RetryAfter);
}
void from_json(const nlohmann::json &j, RelationshipRemoveData &m) {
JS_D("id", m.ID);
JS_D("type", m.Type);
}
void from_json(const nlohmann::json &j, RelationshipAddData &m) {
JS_D("id", m.ID);
JS_D("type", m.Type);
JS_D("user", m.User);
}
void to_json(nlohmann::json &j, const FriendRequestObject &m) {
j["username"] = m.Username;
j["discriminator"] = m.Discriminator;
}
void to_json(nlohmann::json &j, const PutRelationshipObject &m) {
JS_IF("type", m.Type);
}
void from_json(const nlohmann::json &j, ThreadCreateData &m) {
j.get_to(m.Channel);
}
void from_json(const nlohmann::json &j, ThreadDeleteData &m) {
JS_D("id", m.ID);
JS_D("guild_id", m.GuildID);
JS_D("parent_id", m.ParentID);
JS_D("type", m.Type);
}
void from_json(const nlohmann::json &j, ThreadListSyncData &m) {
JS_D("threads", m.Threads);
JS_D("guild_id", m.GuildID);
}
void from_json(const nlohmann::json &j, ThreadMembersUpdateData &m) {
JS_D("id", m.ID);
JS_D("guild_id", m.GuildID);
JS_D("member_count", m.MemberCount);
JS_O("added_members", m.AddedMembers);
JS_O("removed_member_ids", m.RemovedMemberIDs);
}
void from_json(const nlohmann::json &j, ArchivedThreadsResponseData &m) {
JS_D("threads", m.Threads);
JS_D("members", m.Members);
JS_D("has_more", m.HasMore);
}
void from_json(const nlohmann::json &j, ThreadMemberUpdateData &m) {
m.Member = j;
}
void from_json(const nlohmann::json &j, ThreadUpdateData &m) {
m.Thread = j;
}
void from_json(const nlohmann::json &j, ThreadMemberListUpdateData::UserEntry &m) {
JS_D("user_id", m.UserID);
JS_D("member", m.Member);
}
void from_json(const nlohmann::json &j, ThreadMemberListUpdateData &m) {
JS_D("thread_id", m.ThreadID);
JS_D("guild_id", m.GuildID);
JS_D("members", m.Members);
}
void to_json(nlohmann::json &j, const ModifyChannelObject &m) {
JS_IF("archived", m.Archived);
JS_IF("locked", m.Locked);
}
void from_json(const nlohmann::json &j, MessageAckData &m) {
// JS_D("version", m.Version);
JS_D("message_id", m.MessageID);
JS_D("channel_id", m.ChannelID);
}
void to_json(nlohmann::json &j, const AckBulkData &m) {
j["read_states"] = m.ReadStates;
}
void from_json(const nlohmann::json &j, UserGuildSettingsUpdateData &m) {
m.Settings = j;
}
void from_json(const nlohmann::json &j, GuildMembersChunkData &m) {
JS_D("members", m.Members);
JS_D("guild_id", m.GuildID);
}

View File

@@ -19,40 +19,20 @@
#include "ban.hpp"
#include "auditlog.hpp"
#include "relationship.hpp"
#include "errors.hpp"
// most stuff below should just be objects that get processed and thrown away immediately
enum class GatewayOp : int {
Dispatch = 0,
Event = 0,
Heartbeat = 1,
Identify = 2,
PresenceUpdate = 3,
VoiceStateUpdate = 4,
VoiceServerPing = 5,
UpdateStatus = 3,
Resume = 6,
Reconnect = 7,
RequestGuildMembers = 8,
InvalidSession = 9,
Hello = 10,
HeartbeatAck = 11,
// 12 unused
CallConnect = 13,
GuildSubscriptions = 14,
LobbyConnect = 15,
LobbyDisconnect = 16,
LobbyVoiceStatesUpdate = 17,
StreamCreate = 18,
StreamDelete = 19,
StreamWatch = 20,
StreamPing = 21,
StreamSetPaused = 22,
// 23 unused
RequestGuildApplicationCommands = 24,
EmbeddedActivityLaunch = 25,
EmbeddedActivityClose = 26,
EmbeddedActivityUpdate = 27,
RequestForumUnreads = 28,
LazyLoadRequest = 14,
};
enum class GatewayEvent : int {
@@ -88,18 +68,6 @@ enum class GatewayEvent : int {
GUILD_JOIN_REQUEST_CREATE,
GUILD_JOIN_REQUEST_UPDATE,
GUILD_JOIN_REQUEST_DELETE,
RELATIONSHIP_REMOVE,
RELATIONSHIP_ADD,
THREAD_CREATE,
THREAD_UPDATE,
THREAD_DELETE,
THREAD_LIST_SYNC,
THREAD_MEMBER_UPDATE,
THREAD_MEMBERS_UPDATE,
THREAD_MEMBER_LIST_UPDATE,
MESSAGE_ACK,
USER_GUILD_SETTINGS_UPDATE,
GUILD_MEMBERS_CHUNK,
};
enum class GatewayCloseCode : uint16_t {
@@ -197,7 +165,7 @@ struct GuildMemberListUpdateMessage {
std::string HoistedRole; // null
bool IsDefeaned;
[[nodiscard]] GuildMember GetAsMemberData() const;
GuildMember GetAsMemberData() const;
friend void from_json(const nlohmann::json &j, MemberItem &m);
@@ -227,12 +195,10 @@ struct GuildMemberListUpdateMessage {
struct LazyLoadRequestMessage {
Snowflake GuildID;
std::optional<bool> ShouldGetTyping;
std::optional<bool> ShouldGetActivities;
std::optional<bool> ShouldGetThreads;
std::optional<std::vector<std::string>> Members; // snowflake?
std::optional<std::map<Snowflake, std::vector<std::pair<int, int>>>> Channels; // channel ID -> range of sidebar
std::optional<std::vector<Snowflake>> ThreadIDs;
bool ShouldGetTyping = false;
bool ShouldGetActivities = false;
std::optional<std::vector<std::string>> Members; // snowflake?
std::optional<std::unordered_map<Snowflake, std::vector<std::pair<int, int>>>> Channels; // channel ID -> range of sidebar
friend void to_json(nlohmann::json &j, const LazyLoadRequestMessage &m);
};
@@ -246,67 +212,6 @@ struct UpdateStatusMessage {
friend void to_json(nlohmann::json &j, const UpdateStatusMessage &m);
};
struct RequestGuildMembersMessage {
Snowflake GuildID;
bool Presences;
std::vector<Snowflake> UserIDs;
friend void to_json(nlohmann::json &j, const RequestGuildMembersMessage &m);
};
struct ReadStateEntry {
int MentionCount;
Snowflake LastMessageID;
Snowflake ID;
// std::string LastPinTimestamp; iso
friend void from_json(const nlohmann::json &j, ReadStateEntry &m);
friend void to_json(nlohmann::json &j, const ReadStateEntry &m);
};
struct ReadStateData {
int Version;
bool IsPartial;
std::vector<ReadStateEntry> Entries;
friend void from_json(const nlohmann::json &j, ReadStateData &m);
};
struct UserGuildSettingsChannelOverride {
bool Muted;
MuteConfigData MuteConfig;
int MessageNotifications;
bool Collapsed;
Snowflake ChannelID;
friend void from_json(const nlohmann::json &j, UserGuildSettingsChannelOverride &m);
friend void to_json(nlohmann::json &j, const UserGuildSettingsChannelOverride &m);
};
struct UserGuildSettingsEntry {
int Version;
bool SuppressRoles;
bool SuppressEveryone;
bool Muted;
MuteConfigData MuteConfig;
bool MobilePush;
int MessageNotifications;
bool HideMutedChannels;
Snowflake GuildID;
std::vector<UserGuildSettingsChannelOverride> ChannelOverrides;
friend void from_json(const nlohmann::json &j, UserGuildSettingsEntry &m);
friend void to_json(nlohmann::json &j, const UserGuildSettingsEntry &m);
};
struct UserGuildSettingsData {
int Version;
bool IsPartial;
std::vector<UserGuildSettingsEntry> Entries;
friend void from_json(const nlohmann::json &j, UserGuildSettingsData &m);
};
struct ReadyEventData {
int GatewayVersion;
UserData SelfUser;
@@ -322,8 +227,6 @@ struct ReadyEventData {
std::optional<std::vector<std::vector<GuildMember>>> MergedMembers;
std::optional<std::vector<RelationshipData>> Relationships;
std::optional<std::vector<GuildApplicationData>> GuildJoinRequests;
ReadStateData ReadState;
UserGuildSettingsData GuildSettings;
// std::vector<Unknown> ConnectedAccounts; // opt
// std::map<std::string, Unknown> Consents; // opt
// std::vector<Unknown> Experiments; // opt
@@ -723,143 +626,3 @@ struct RateLimitedResponse {
friend void from_json(const nlohmann::json &j, RateLimitedResponse &m);
};
struct RelationshipRemoveData {
Snowflake ID;
RelationshipType Type;
friend void from_json(const nlohmann::json &j, RelationshipRemoveData &m);
};
struct RelationshipAddData {
Snowflake ID;
// Nickname; same deal as the other comment somewhere else
RelationshipType Type;
UserData User;
// std::optional<bool> ShouldNotify; // i guess if the client should send a notification. not worth caring about
friend void from_json(const nlohmann::json &j, RelationshipAddData &m);
};
struct FriendRequestObject {
std::string Username;
int Discriminator;
friend void to_json(nlohmann::json &j, const FriendRequestObject &m);
};
struct PutRelationshipObject {
std::optional<RelationshipType> Type;
friend void to_json(nlohmann::json &j, const PutRelationshipObject &m);
};
struct ThreadCreateData {
ChannelData Channel;
friend void from_json(const nlohmann::json &j, ThreadCreateData &m);
};
struct ThreadDeleteData {
Snowflake ID;
Snowflake GuildID;
Snowflake ParentID;
ChannelType Type;
friend void from_json(const nlohmann::json &j, ThreadDeleteData &m);
};
// pretty different from docs
struct ThreadListSyncData {
std::vector<ChannelData> Threads;
Snowflake GuildID;
// std::optional<std::vector<???>> MostRecentMessages;
friend void from_json(const nlohmann::json &j, ThreadListSyncData &m);
};
struct ThreadMembersUpdateData {
Snowflake ID;
Snowflake GuildID;
int MemberCount;
std::optional<std::vector<ThreadMemberObject>> AddedMembers;
std::optional<std::vector<Snowflake>> RemovedMemberIDs;
friend void from_json(const nlohmann::json &j, ThreadMembersUpdateData &m);
};
struct ArchivedThreadsResponseData {
std::vector<ChannelData> Threads;
std::vector<ThreadMemberObject> Members;
bool HasMore;
friend void from_json(const nlohmann::json &j, ArchivedThreadsResponseData &m);
};
struct ThreadMemberUpdateData {
ThreadMemberObject Member;
friend void from_json(const nlohmann::json &j, ThreadMemberUpdateData &m);
};
struct ThreadUpdateData {
ChannelData Thread;
friend void from_json(const nlohmann::json &j, ThreadUpdateData &m);
};
struct ThreadMemberListUpdateData {
struct UserEntry {
Snowflake UserID;
// PresenceData Presence;
GuildMember Member;
friend void from_json(const nlohmann::json &j, UserEntry &m);
};
Snowflake ThreadID;
Snowflake GuildID;
std::vector<UserEntry> Members;
friend void from_json(const nlohmann::json &j, ThreadMemberListUpdateData &m);
};
struct ModifyChannelObject {
std::optional<bool> Archived;
std::optional<bool> Locked;
friend void to_json(nlohmann::json &j, const ModifyChannelObject &m);
};
struct MessageAckData {
// int Version; // what is this ?!?!?!!?
Snowflake MessageID;
Snowflake ChannelID;
friend void from_json(const nlohmann::json &j, MessageAckData &m);
};
struct AckBulkData {
std::vector<ReadStateEntry> ReadStates;
friend void to_json(nlohmann::json &j, const AckBulkData &m);
};
struct UserGuildSettingsUpdateData {
UserGuildSettingsEntry Settings;
friend void from_json(const nlohmann::json &j, UserGuildSettingsUpdateData &m);
};
struct GuildMembersChunkData {
/*
not needed so not deserialized
int ChunkCount;
int ChunkIndex;
std::vector<?> NotFound;
*/
Snowflake GuildID;
std::vector<GuildMember> Members;
friend void from_json(const nlohmann::json &j, GuildMembersChunkData &m);
};

View File

@@ -2,9 +2,9 @@
#include <cstdint>
#include "snowflake.hpp"
#include "json.hpp"
#include "util.hpp"
#include "../util.hpp"
constexpr static uint64_t PERMISSION_MAX_BIT = 36;
constexpr static uint64_t PERMISSION_MAX_BIT = 31;
enum class Permission : uint64_t {
NONE = 0,
CREATE_INSTANT_INVITE = (1ULL << 0), // Allows creation of instant invites
@@ -40,11 +40,10 @@ enum class Permission : uint64_t {
MANAGE_EMOJIS = (1ULL << 30), // Allows management and editing of emojis
USE_SLASH_COMMANDS = (1ULL << 31), // Allows members to use slash commands in text channels
REQUEST_TO_SPEAK = (1ULL << 32), // Allows for requesting to speak in stage channels
MANAGE_THREADS = (1ULL << 34), // Allows for deleting and archiving threads, and viewing all private threads
USE_PUBLIC_THREADS = (1ULL << 35), // Allows for creating and participating in threads
USE_PRIVATE_THREADS = (1ULL << 36), // Allows for creating and participating in private threads
USE_THREADS = (1ULL << 33), // Allows for creating and participating in threads
USE_PRIVATE_THREADS = (1ULL << 34), // Allows for creating and participating in private threads
ALL = 0x1FFFFFFFFFULL,
ALL = 0x7FFFFFFFFULL,
};
template<>
struct Bitwise<Permission> {
@@ -108,7 +107,7 @@ constexpr const char *GetPermissionString(Permission perm) {
case Permission::USE_EXTERNAL_EMOJIS:
return "Use External Emojis";
case Permission::VIEW_GUILD_INSIGHTS:
return "View Server Insights";
return "View Guild Insights";
case Permission::CONNECT:
return "Connect to Voice";
case Permission::SPEAK:
@@ -133,12 +132,6 @@ constexpr const char *GetPermissionString(Permission perm) {
return "Manage Emojis";
case Permission::USE_SLASH_COMMANDS:
return "Use Slash Commands";
case Permission::MANAGE_THREADS:
return "Manage Threads";
case Permission::USE_PUBLIC_THREADS:
return "Use Public Threads";
case Permission::USE_PRIVATE_THREADS:
return "Use Private Threads";
default:
return "Unknown Permission";
}
@@ -187,7 +180,7 @@ constexpr const char *GetPermissionDescription(Permission perm) {
case Permission::USE_EXTERNAL_EMOJIS:
return "Allows members to use emoji from other servers, if they're a Discord Nitro member";
case Permission::VIEW_GUILD_INSIGHTS:
return "Allows members to view Server Insights, which shows data on community growth, engagement, and more.";
return "";
case Permission::CONNECT:
return "Allows members to join voice channels and hear others.";
case Permission::SPEAK:
@@ -212,12 +205,6 @@ constexpr const char *GetPermissionDescription(Permission perm) {
return "Allows members to add or remove custom emojis in this server.";
case Permission::USE_SLASH_COMMANDS:
return "Allows members to use slash commands in text channels.";
case Permission::MANAGE_THREADS:
return "Allows members to rename, delete, archive/unarchive, and turn on slow mode for threads.";
case Permission::USE_PUBLIC_THREADS:
return "Allows members to talk in threads. The \"Send Messages\" permission must be enabled for members to start new threads; if it's disabled, they can only respond to existing threads.";
case Permission::USE_PRIVATE_THREADS:
return "Allows members to create and chat in private threads. The \"Send Messages\" permission must be enabled for members to start new private threads; if it's disabled, they can only respond to private threads they're added to.";
default:
return "";
}

View File

@@ -3,19 +3,19 @@
#include "user.hpp"
enum class RelationshipType {
None = 0,
Friend = 1,
Blocked = 2,
PendingIncoming = 3,
PendingOutgoing = 4,
Implicit = 5,
None = 0,
Friend = 1,
Blocked = 2,
PendingIncoming = 3,
PendingOutgoing = 4,
Implicit = 5,
};
struct RelationshipData {
// Snowflake UserID; this is the same as ID apparently but it looks new so i wont touch it
RelationshipType Type;
Snowflake ID;
// Unknown Nickname; // null
// Unknown Nickname; // null
friend void from_json(const nlohmann::json &j, RelationshipData &m);
friend void from_json(const nlohmann::json &j, RelationshipData &m);
};

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