forked from OpenGamers/abaddon
Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ad0d4e7d4d | ||
|
|
1ca6235e09 | ||
|
|
c04fc2e60c | ||
|
|
a7bf9a2404 | ||
|
|
2065ef4940 | ||
|
|
3aefab652e |
29
.github/workflows/ci.yml
vendored
29
.github/workflows/ci.yml
vendored
@@ -8,24 +8,24 @@ jobs:
|
||||
runs-on: windows-latest
|
||||
strategy:
|
||||
matrix:
|
||||
buildtype: [Debug, RelWithDebInfo, MinSizeRel]
|
||||
buildtype: [Debug, RelWithDebInfo]
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
with:
|
||||
submodules: true
|
||||
|
||||
- name: Fetch CMake
|
||||
uses: lukka/get-cmake@v3.21.2
|
||||
uses: lukka/get-cmake@latest
|
||||
|
||||
- name: Fetch dependencies
|
||||
uses: lukka/run-vcpkg@v7
|
||||
uses: lukka/run-vcpkg@main
|
||||
with:
|
||||
vcpkgArguments: gtkmm nlohmann-json zlib sqlite3 glibmm openssl ixwebsocket curl
|
||||
vcpkgDirectory: ${{ github.workspace }}/ci/vcpkg/
|
||||
vcpkgTriplet: x64-windows
|
||||
|
||||
- name: Build
|
||||
uses: lukka/run-cmake@v3
|
||||
uses: lukka/run-cmake@main
|
||||
with:
|
||||
useVcpkgToolchainFile: true
|
||||
vcpkgTriplet: x64-windows
|
||||
@@ -41,9 +41,8 @@ jobs:
|
||||
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 }}\res\css" "${{ runner.workspace }}\build\css"
|
||||
xcopy /E /I "${{ github.workspace }}\res\res" "${{ runner.workspace }}\build\res"
|
||||
xcopy /E /I "${{ github.workspace }}\res\fonts" "${{ runner.workspace }}\build\fonts"
|
||||
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"
|
||||
@@ -66,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: |
|
||||
@@ -74,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 }}
|
||||
@@ -83,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
|
||||
@@ -104,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: |
|
||||
@@ -123,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
|
||||
@@ -136,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
2
.gitignore
vendored
@@ -354,5 +354,3 @@ testdata/
|
||||
|
||||
build/
|
||||
out/
|
||||
|
||||
fonts/fonts.conf
|
||||
|
||||
9
.gitmodules
vendored
9
.gitmodules
vendored
@@ -1,12 +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
|
||||
|
||||
@@ -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)
|
||||
|
||||
55
README.md
55
README.md
@@ -4,13 +4,11 @@ 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
|
||||
* Start new DMs and group DMs
|
||||
* View user profiles (notes, mutual servers, mutual friends)
|
||||
@@ -20,19 +18,15 @@ 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:
|
||||
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 sqlite3:x64-windows openssl:x64-windows curl:x64-windows`
|
||||
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 -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
|
||||
@@ -53,21 +47,12 @@ Or, do steps 1 and 2, and open CMakeLists.txt in Visual Studio if `vcpkg integra
|
||||
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
|
||||
|
||||
⚠️ If you use Windows, make sure to start from the directory containing `css` and `res`
|
||||
|
||||
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
|
||||
⚠️ 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/)
|
||||
@@ -75,6 +60,7 @@ On Linux, `css` and `res` can also be loaded from `~/.local/share/abaddon` or `/
|
||||
* [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:
|
||||
@@ -92,6 +78,12 @@ On Linux, `css` and `res` can also be loaded from `~/.local/share/abaddon` or `/
|
||||
.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
|
||||
@@ -177,40 +169,25 @@ Used in profile popup:
|
||||
|
||||
### 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
|
||||
|
||||
#### 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
|
||||
* owner_crown (true or false, default true) - show a crown next to the owner
|
||||
|
||||
#### 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
|
||||
|
||||
### 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
|
||||
|
||||
@@ -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,6 +68,7 @@ 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([this](const Glib::RefPtr<const Gtk::CssSection> §ion, const Glib::Error &error) {
|
||||
Gtk::MessageDialog dlg(*m_main_window, "css failed parsing (" + error.what() + ")", false, Gtk::MESSAGE_ERROR, Gtk::BUTTONS_OK, true);
|
||||
@@ -69,19 +76,46 @@ int Abaddon::StartGTK() {
|
||||
dlg.run();
|
||||
});
|
||||
|
||||
m_css_low_provider = Gtk::CssProvider::create();
|
||||
m_css_low_provider->signal_parsing_error().connect([this](const Glib::RefPtr<const Gtk::CssSection> §ion, const Glib::Error &error) {
|
||||
Gtk::MessageDialog dlg(*m_main_window, "low-priority css failed parsing (" + error.what() + ")", false, Gtk::MESSAGE_ERROR, Gtk::BUTTONS_OK, true);
|
||||
dlg.set_position(Gtk::WIN_POS_CENTER);
|
||||
dlg.run();
|
||||
});
|
||||
|
||||
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();
|
||||
}
|
||||
@@ -99,49 +133,12 @@ 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;
|
||||
std::string token = m_settings.GetDiscordToken();
|
||||
if (token.size()) {
|
||||
m_discord_token = token;
|
||||
m_discord.UpdateToken(m_discord_token);
|
||||
@@ -154,7 +151,6 @@ void Abaddon::StartDiscord() {
|
||||
|
||||
void Abaddon::StopDiscord() {
|
||||
m_discord.Stop();
|
||||
SaveState();
|
||||
}
|
||||
|
||||
bool Abaddon::IsDiscordActive() const {
|
||||
@@ -177,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) {
|
||||
@@ -196,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 ¶m) {
|
||||
@@ -216,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) {
|
||||
@@ -240,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() {
|
||||
@@ -270,7 +276,7 @@ void Abaddon::ShowUserMenu(const GdkEvent *event, Snowflake id, Snowflake guild_
|
||||
if (guild.has_value() && user.has_value()) {
|
||||
const auto roles = user->GetSortedRoles();
|
||||
m_user_menu_roles->set_visible(roles.size() > 0);
|
||||
for (const auto &role : roles) {
|
||||
for (const auto role : roles) {
|
||||
auto *item = Gtk::manage(new Gtk::MenuItem(role.Name));
|
||||
if (role.Color != 0) {
|
||||
Gdk::RGBA color;
|
||||
@@ -314,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();
|
||||
@@ -367,40 +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) 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;
|
||||
@@ -433,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);
|
||||
@@ -448,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();
|
||||
@@ -482,7 +427,8 @@ void Abaddon::ActionConnect() {
|
||||
}
|
||||
|
||||
void Abaddon::ActionDisconnect() {
|
||||
StopDiscord();
|
||||
if (m_discord.IsStarted())
|
||||
StopDiscord();
|
||||
}
|
||||
|
||||
void Abaddon::ActionSetToken() {
|
||||
@@ -492,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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -506,12 +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;
|
||||
if (channel->Type == ChannelType::GUILD_TEXT || channel->Type == ChannelType::GUILD_NEWS)
|
||||
m_main_window->set_title(std::string(APP_TITLE) + " - #" + *channel->Name);
|
||||
else {
|
||||
@@ -527,7 +470,7 @@ void Abaddon::ActionChannelOpened(Snowflake id) {
|
||||
}
|
||||
m_main_window->UpdateChatActiveChannel(id);
|
||||
if (m_channels_requested.find(id) == m_channels_requested.end()) {
|
||||
m_discord.FetchMessagesInChannel(id, [this, id](const std::vector<Message> &msgs) {
|
||||
m_discord.FetchMessagesInChannel(id, [this, id](const std::vector<Snowflake> &msgs) {
|
||||
m_main_window->UpdateChatWindowContents();
|
||||
m_channels_requested.insert(id);
|
||||
});
|
||||
@@ -535,14 +478,11 @@ void Abaddon::ActionChannelOpened(Snowflake id) {
|
||||
m_main_window->UpdateChatWindowContents();
|
||||
}
|
||||
|
||||
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()) {
|
||||
if (channel->Type != ChannelType::DM && channel->Type != ChannelType::GROUP_DM) {
|
||||
m_discord.SendLazyLoad(id);
|
||||
|
||||
if (m_discord.IsVerificationRequired(*channel->GuildID))
|
||||
const auto request = m_discord.GetGuildApplication(*channel->GuildID);
|
||||
if (request.has_value() && request->ApplicationStatus == GuildApplicationStatus::STARTED)
|
||||
ShowGuildVerificationGateDialog(*channel->GuildID);
|
||||
}
|
||||
}
|
||||
@@ -555,16 +495,20 @@ 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);
|
||||
|
||||
if (msgs.size() == 0) {
|
||||
@@ -584,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();
|
||||
@@ -670,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);
|
||||
@@ -721,8 +648,6 @@ EmojiResource &Abaddon::GetEmojis() {
|
||||
}
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
if (std::getenv("ABADDON_NO_FC") == nullptr)
|
||||
Platform::SetupFonts();
|
||||
#if defined(_WIN32) && defined(_MSC_VER)
|
||||
TCHAR buf[2] { 0 };
|
||||
GetEnvironmentVariableA("GTK_CSD", buf, sizeof(buf));
|
||||
@@ -14,6 +14,7 @@
|
||||
class Abaddon {
|
||||
private:
|
||||
Abaddon();
|
||||
~Abaddon();
|
||||
Abaddon(const Abaddon &) = delete;
|
||||
Abaddon &operator=(const Abaddon &) = delete;
|
||||
Abaddon(Abaddon &&) = delete;
|
||||
@@ -23,8 +24,6 @@ public:
|
||||
static Abaddon &Get();
|
||||
|
||||
int StartGTK();
|
||||
void OnShutdown();
|
||||
|
||||
void StartDiscord();
|
||||
void StopDiscord();
|
||||
|
||||
@@ -37,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);
|
||||
@@ -47,11 +47,10 @@ public:
|
||||
void ActionReactionRemove(Snowflake id, const Glib::ustring ¶m);
|
||||
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();
|
||||
@@ -67,35 +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 ¶m);
|
||||
void DiscordOnReactionRemove(Snowflake message_id, const Glib::ustring ¶m);
|
||||
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 SetupUserMenu();
|
||||
void SaveState();
|
||||
void LoadState();
|
||||
|
||||
void ManageHeapWindow(Gtk::Window *window);
|
||||
|
||||
Snowflake m_shown_user_menu_id;
|
||||
Snowflake m_shown_user_menu_guild_id;
|
||||
@@ -134,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
|
||||
};
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -50,6 +50,7 @@ endif()
|
||||
|
||||
include(FindPackageHandleStandardArgs)
|
||||
find_package_handle_standard_args(glib
|
||||
FOUND_VAR GLIB_FOUND
|
||||
REQUIRED_VARS
|
||||
GLIB_LIBRARIES
|
||||
GLIB_INCLUDE_DIRS
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,30 +1,17 @@
|
||||
set(IXWebSocket_LIBRARY_NAME ixwebsocket)
|
||||
find_path(IXWEBSOCKET_INCLUDE_DIR
|
||||
NAMES ixwebsocket/IXWebSocket.h)
|
||||
|
||||
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
|
||||
HINTS ${IXWEBSOCKET_LIBRARY_ROOT})
|
||||
|
||||
|
||||
find_library(IXWebSocket_LIBRARY
|
||||
NAMES ${IXWebSocket_LIBRARY_NAME}
|
||||
PATH_SUFFIXES ${IXWebSocket_LIBRARY_NAME}
|
||||
${IXWebSocket_LIBRARY_NAME}/include)
|
||||
set(IXWEBSOCKET_LIBRARIES ${IXWEBSOCKET_LIBRARY})
|
||||
set(IXWEBSOCKET_INCLUDE_DIRS ${IXWEBSOCKET_INCLUDE_DIR})
|
||||
|
||||
include(FindPackageHandleStandardArgs)
|
||||
|
||||
find_package_handle_standard_args(IXWebSocket
|
||||
find_package_handle_standard_args(ixwebsocket
|
||||
FOUND_VAR IXWEBSOCKET_FOUND
|
||||
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()
|
||||
IXWEBSOCKET_LIBRARY
|
||||
IXWEBSOCKET_INCLUDE_DIR
|
||||
VERSION_VAR IXWEBSOCKET_VERSION)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -61,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;
|
||||
813
components/channels.cpp
Normal file
813
components/channels.cpp
Normal 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
191
components/channels.hpp
Normal 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;
|
||||
};
|
||||
@@ -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,9 +21,8 @@ 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;
|
||||
@@ -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
|
||||
@@ -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:
|
||||
@@ -1,6 +1,6 @@
|
||||
#include "chatmessage.hpp"
|
||||
#include "abaddon.hpp"
|
||||
#include "util.hpp"
|
||||
#include "../abaddon.hpp"
|
||||
#include "../util.hpp"
|
||||
#include "lazyimage.hpp"
|
||||
#include <unordered_map>
|
||||
|
||||
@@ -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,74 +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.size() > 0 || 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 (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);
|
||||
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);
|
||||
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->size() > 0) {
|
||||
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();
|
||||
@@ -111,29 +126,25 @@ void ChatMessageItemContainer::UpdateContent() {
|
||||
if (data->Embeds.size() == 1) {
|
||||
m_embed_component = CreateEmbedComponent(data->Embeds[0]);
|
||||
AttachEventHandlers(*m_embed_component);
|
||||
m_main.add(*m_embed_component);
|
||||
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->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() {
|
||||
@@ -149,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)
|
||||
@@ -170,10 +181,10 @@ void ChatMessageItemContainer::AddClickHandler(Gtk::Widget *widget, std::string
|
||||
// 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);
|
||||
@@ -231,13 +242,11 @@ void ChatMessageItemContainer::UpdateTextComponent(Gtk::TextView *tv) {
|
||||
}
|
||||
} break;
|
||||
case MessageType::RECIPIENT_ADD: {
|
||||
if (data->Mentions.size() == 0) 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.size() == 0) break;
|
||||
const auto &adder = Abaddon::Get().GetDiscordClient().GetUser(data->Author.ID);
|
||||
const auto &added = data->Mentions[0];
|
||||
if (adder->ID == added.ID)
|
||||
@@ -280,20 +289,6 @@ 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;
|
||||
}
|
||||
}
|
||||
@@ -310,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);
|
||||
@@ -357,7 +354,7 @@ Gtk::Widget *ChatMessageItemContainer::CreateEmbedComponent(const EmbedData &emb
|
||||
}
|
||||
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>");
|
||||
}
|
||||
@@ -505,7 +502,7 @@ Gtk::Widget *ChatMessageItemContainer::CreateAttachmentComponent(const Attachmen
|
||||
return ev;
|
||||
}
|
||||
|
||||
Gtk::Widget *ChatMessageItemContainer::CreateStickerComponentDeprecated(const StickerData &data) {
|
||||
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);
|
||||
@@ -522,27 +519,6 @@ Gtk::Widget *ChatMessageItemContainer::CreateStickerComponentDeprecated(const St
|
||||
return box;
|
||||
}
|
||||
|
||||
Gtk::Widget *ChatMessageItemContainer::CreateStickersComponent(const std::vector<StickerItem> &data) {
|
||||
auto *box = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_VERTICAL));
|
||||
|
||||
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_size_request(StickerComponentSize, StickerComponentSize); // should this go in LazyImage ?
|
||||
img->show();
|
||||
ev->show();
|
||||
ev->add(*img);
|
||||
box->add(*ev);
|
||||
}
|
||||
|
||||
box->show();
|
||||
|
||||
AttachEventHandlers(*box);
|
||||
return box;
|
||||
}
|
||||
|
||||
Gtk::Widget *ChatMessageItemContainer::CreateReactionsComponent(const Message &data) {
|
||||
auto *flow = Gtk::manage(new Gtk::FlowBox);
|
||||
flow->set_orientation(Gtk::ORIENTATION_HORIZONTAL);
|
||||
@@ -625,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);
|
||||
@@ -655,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);
|
||||
|
||||
@@ -674,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 {
|
||||
@@ -702,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>");
|
||||
@@ -798,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);
|
||||
@@ -805,7 +769,7 @@ 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);
|
||||
@@ -844,8 +808,11 @@ 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(Glib::RefPtr<Gtk::TextBuffer> buf) {
|
||||
@@ -899,10 +866,8 @@ void ChatMessageItemContainer::HandleChannelMentions(Glib::RefPtr<Gtk::TextBuffe
|
||||
}
|
||||
|
||||
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);
|
||||
@@ -965,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)) {
|
||||
@@ -973,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);
|
||||
@@ -1021,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;
|
||||
}
|
||||
@@ -1033,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;
|
||||
}
|
||||
|
||||
@@ -1045,22 +1063,28 @@ 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()) {
|
||||
auto cb = [this](const Glib::RefPtr<Gdk::PixbufAnimation> &pb) {
|
||||
@@ -1070,23 +1094,23 @@ ChatMessageHeader::ChatMessageHeader(const Message &data)
|
||||
}
|
||||
|
||||
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_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_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);
|
||||
@@ -1096,62 +1120,62 @@ 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);
|
||||
|
||||
@@ -1163,27 +1187,25 @@ ChatMessageHeader::ChatMessageHeader(const Message &data)
|
||||
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));
|
||||
UpdateNameColor();
|
||||
AttachUserMenuHandler(m_meta_ev);
|
||||
AttachUserMenuHandler(m_avatar_ev);
|
||||
AttachUserMenuHandler(*m_meta_ev);
|
||||
AttachUserMenuHandler(*m_avatar_ev);
|
||||
}
|
||||
|
||||
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 role_id = discord.GetMemberHoistedRole(*chan->GuildID, UserID, true);
|
||||
const auto role = discord.GetRole(role_id);
|
||||
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) + "'>" + user->GetEscapedName() + "</span>");
|
||||
else
|
||||
m_author.set_markup("<span weight='bold'>" + user->GetEscapedName() + "</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() {
|
||||
@@ -1193,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;
|
||||
}
|
||||
|
||||
@@ -1224,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;
|
||||
@@ -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();
|
||||
@@ -20,13 +20,12 @@ public:
|
||||
|
||||
protected:
|
||||
void AddClickHandler(Gtk::Widget *widget, std::string);
|
||||
Gtk::TextView *CreateTextComponent(const Message &data); // Message.Content
|
||||
Gtk::TextView *CreateTextComponent(const Message *data); // Message.Content
|
||||
void UpdateTextComponent(Gtk::TextView *tv);
|
||||
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 *CreateStickerComponentDeprecated(const StickerData &data);
|
||||
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);
|
||||
|
||||
@@ -57,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;
|
||||
@@ -67,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 {
|
||||
@@ -87,28 +111,28 @@ public:
|
||||
Snowflake ChannelID;
|
||||
Snowflake NewestID = 0;
|
||||
|
||||
ChatMessageHeader(const Message &data);
|
||||
ChatMessageHeader(const Message *data);
|
||||
void AddContent(Gtk::Widget *widget, bool prepend);
|
||||
void UpdateNameColor();
|
||||
std::vector<Gtk::Widget *> GetChildContent();
|
||||
std::vector<Gtk::Widget*> GetChildContent();
|
||||
|
||||
protected:
|
||||
void AttachUserMenuHandler(Gtk::Widget &widget);
|
||||
|
||||
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
411
components/chatwindow.cpp
Normal 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 ¶m) {
|
||||
m_signal_action_reaction_add.emit(id, param);
|
||||
});
|
||||
content->signal_action_reaction_remove().connect([this, id](const Glib::ustring ¶m) {
|
||||
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;
|
||||
}
|
||||
@@ -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 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;
|
||||
};
|
||||
@@ -1,7 +1,7 @@
|
||||
#include <unordered_set>
|
||||
#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,7 +47,7 @@ bool Completer::ProcessKeyPress(GdkEventKey *e) {
|
||||
switch (e->keyval) {
|
||||
case GDK_KEY_Down: {
|
||||
if (m_entries.size() == 0) return true;
|
||||
const auto index = static_cast<size_t>(m_list.get_selected_row()->get_index());
|
||||
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);
|
||||
@@ -55,7 +55,7 @@ bool Completer::ProcessKeyPress(GdkEventKey *e) {
|
||||
return true;
|
||||
case GDK_KEY_Up: {
|
||||
if (m_entries.size() == 0) return true;
|
||||
const auto index = static_cast<size_t>(m_list.get_selected_row()->get_index());
|
||||
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);
|
||||
@@ -169,7 +169,7 @@ 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;
|
||||
@@ -186,7 +186,7 @@ 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;
|
||||
@@ -291,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;
|
||||
@@ -308,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,8 +330,8 @@ Glib::ustring Completer::GetTerm() {
|
||||
}
|
||||
|
||||
CompleterEntry::CompleterEntry(const Glib::ustring &completion, int index)
|
||||
: m_completion(completion)
|
||||
, m_index(index)
|
||||
: m_index(index)
|
||||
, m_completion(completion)
|
||||
, m_box(Gtk::ORIENTATION_HORIZONTAL) {
|
||||
set_halign(Gtk::ALIGN_START);
|
||||
get_style_context()->add_class("completer-entry");
|
||||
@@ -2,7 +2,7 @@
|
||||
#include <gtkmm.h>
|
||||
#include <functional>
|
||||
#include "lazyimage.hpp"
|
||||
#include "discord/snowflake.hpp"
|
||||
#include "../discord/snowflake.hpp"
|
||||
|
||||
constexpr static int CompleterImageSize = 24;
|
||||
|
||||
8
components/inotifyswitched.hpp
Normal file
8
components/inotifyswitched.hpp
Normal 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() {};
|
||||
};
|
||||
@@ -1,5 +1,5 @@
|
||||
#include "lazyimage.hpp"
|
||||
#include "abaddon.hpp"
|
||||
#include "../abaddon.hpp"
|
||||
|
||||
LazyImage::LazyImage(int w, int h, bool use_placeholder)
|
||||
: m_width(w)
|
||||
@@ -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;
|
||||
@@ -163,7 +156,7 @@ void MemberList::UpdateMemberList() {
|
||||
const auto guild = *discord.GetGuild(m_guild_id);
|
||||
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);
|
||||
@@ -217,10 +210,14 @@ void MemberList::UpdateMemberList() {
|
||||
void MemberList::AttachUserMenuHandler(Gtk::ListBoxRow *row, Snowflake id) {
|
||||
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;
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
@@ -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:
|
||||
@@ -1,5 +1,5 @@
|
||||
#include "statusindicator.hpp"
|
||||
#include "abaddon.hpp"
|
||||
#include "../abaddon.hpp"
|
||||
|
||||
static const constexpr int Diameter = 8;
|
||||
static const auto OnlineColor = Gdk::RGBA("#43B581");
|
||||
@@ -18,8 +18,8 @@ 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));
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#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:
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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");
|
||||
@@ -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");
|
||||
@@ -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;
|
||||
};
|
||||
@@ -1,6 +1,6 @@
|
||||
#pragma once
|
||||
#include <gtkmm.h>
|
||||
#include "discord/snowflake.hpp"
|
||||
#include "../discord/snowflake.hpp"
|
||||
|
||||
class FriendPickerDialog : public Gtk::Dialog {
|
||||
public:
|
||||
@@ -1,5 +1,5 @@
|
||||
#include "joinguild.hpp"
|
||||
#include "abaddon.hpp"
|
||||
#include "../abaddon.hpp"
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <regex>
|
||||
|
||||
@@ -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");
|
||||
@@ -1,6 +1,6 @@
|
||||
#pragma once
|
||||
#include <gtkmm.h>
|
||||
#include "discord/objects.hpp"
|
||||
#include "../discord/objects.hpp"
|
||||
|
||||
class SetStatusDialog : public Gtk::Dialog {
|
||||
public:
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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)
|
||||
@@ -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:
|
||||
@@ -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,
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
@@ -1,20 +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);
|
||||
}
|
||||
|
||||
void from_json(const nlohmann::json &j, ChannelData &m) {
|
||||
JS_D("id", m.ID);
|
||||
JS_D("type", m.Type);
|
||||
@@ -35,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) {
|
||||
@@ -59,20 +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::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);
|
||||
}
|
||||
|
||||
std::optional<PermissionOverwrite> ChannelData::GetOverwrite(Snowflake id) const {
|
||||
return Abaddon::Get().GetDiscordClient().GetPermissionOverwrite(ID, id);
|
||||
}
|
||||
@@ -15,49 +15,12 @@ enum class ChannelType : int {
|
||||
GUILD_NEWS = 5,
|
||||
GUILD_STORE = 6,
|
||||
/* 7 and 8 were used for LFG */
|
||||
/* 9 was used for threads */
|
||||
GUILD_NEWS_THREAD = 10,
|
||||
GUILD_PUBLIC_THREAD = 11,
|
||||
GUILD_PRIVATE_THREAD = 12,
|
||||
/* 9 and 10 were used for threads */
|
||||
PUBLIC_THREAD = 11,
|
||||
PRIVATE_THREAD = 12,
|
||||
GUILD_STAGE_VOICE = 13,
|
||||
};
|
||||
|
||||
enum class StagePrivacy {
|
||||
PUBLIC = 1,
|
||||
GUILD_ONLY = 2,
|
||||
};
|
||||
|
||||
constexpr const char *GetStagePrivacyDisplayString(StagePrivacy e) {
|
||||
switch (e) {
|
||||
case StagePrivacy::PUBLIC:
|
||||
return "Public";
|
||||
case StagePrivacy::GUILD_ONLY:
|
||||
return "Guild Only";
|
||||
default:
|
||||
return "Unknown";
|
||||
}
|
||||
}
|
||||
|
||||
// should be moved somewhere?
|
||||
|
||||
struct ThreadMetadataData {
|
||||
bool IsArchived;
|
||||
int AutoArchiveDuration;
|
||||
std::string ArchiveTimestamp;
|
||||
std::optional<bool> IsLocked;
|
||||
|
||||
friend void from_json(const nlohmann::json &j, ThreadMetadataData &m);
|
||||
};
|
||||
|
||||
struct ThreadMemberObject {
|
||||
std::optional<Snowflake> ThreadID;
|
||||
std::optional<Snowflake> UserID;
|
||||
std::string JoinTimestamp;
|
||||
int Flags;
|
||||
|
||||
friend void from_json(const nlohmann::json &j, ThreadMemberObject &m);
|
||||
};
|
||||
|
||||
struct ChannelData {
|
||||
Snowflake ID;
|
||||
ChannelType Type;
|
||||
@@ -78,15 +41,10 @@ struct ChannelData {
|
||||
std::optional<Snowflake> ApplicationID;
|
||||
std::optional<Snowflake> ParentID; // null
|
||||
std::optional<std::string> LastPinTimestamp; // null
|
||||
std::optional<ThreadMetadataData> ThreadMetadata;
|
||||
std::optional<ThreadMemberObject> ThreadMember;
|
||||
|
||||
friend void from_json(const nlohmann::json &j, ChannelData &m);
|
||||
void update_from_json(const nlohmann::json &j);
|
||||
|
||||
bool NSFW() const;
|
||||
bool IsThread() const noexcept;
|
||||
bool IsJoinedThread() const;
|
||||
std::optional<PermissionOverwrite> GetOverwrite(Snowflake id) const;
|
||||
std::vector<UserData> GetDMRecipients() const;
|
||||
};
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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,18 +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, sigc::slot<void(const std::vector<Message> &)> cb);
|
||||
void FetchMessagesInChannelBefore(Snowflake channel_id, Snowflake before_id, 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;
|
||||
@@ -76,14 +90,9 @@ public:
|
||||
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, sigc::slot<void(DiscordError, const ArchivedThreadsResponseData &)> callback);
|
||||
void GetArchivedPrivateThreads(Snowflake channel_id, sigc::slot<void(DiscordError, const ArchivedThreadsResponseData &)> callback);
|
||||
std::unordered_set<Snowflake> GetUsersInGuild(Snowflake id) const;
|
||||
std::unordered_set<Snowflake> GetChannelsInGuild(Snowflake id) const;
|
||||
|
||||
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;
|
||||
@@ -99,7 +108,6 @@ public:
|
||||
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(std::string code);
|
||||
void LeaveGuild(Snowflake id);
|
||||
void KickUser(Snowflake user_id, Snowflake guild_id);
|
||||
@@ -107,51 +115,40 @@ public:
|
||||
void UpdateStatus(PresenceStatus status, bool is_afk);
|
||||
void UpdateStatus(PresenceStatus status, bool is_afk, const ActivityData &obj);
|
||||
void CreateDM(Snowflake user_id);
|
||||
void CreateDM(Snowflake user_id, sigc::slot<void(DiscordError code, Snowflake channel_id)> callback);
|
||||
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);
|
||||
void SetGuildName(Snowflake id, const Glib::ustring &name, sigc::slot<void(DiscordError code)> callback);
|
||||
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(DiscordError code)> callback);
|
||||
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(DiscordError code)> callback);
|
||||
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(DiscordError code)> callback);
|
||||
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, sigc::slot<void(DiscordError code)> callback);
|
||||
void ModifyRoleName(Snowflake guild_id, Snowflake role_id, const Glib::ustring &name, sigc::slot<void(DiscordError code)> callback);
|
||||
void ModifyRoleColor(Snowflake guild_id, Snowflake role_id, uint32_t color, sigc::slot<void(DiscordError code)> callback);
|
||||
void ModifyRoleColor(Snowflake guild_id, Snowflake role_id, Gdk::RGBA color, sigc::slot<void(DiscordError code)> callback);
|
||||
void ModifyRolePosition(Snowflake guild_id, Snowflake role_id, int position, sigc::slot<void(DiscordError code)> callback);
|
||||
void ModifyEmojiName(Snowflake guild_id, Snowflake emoji_id, const Glib::ustring &name, sigc::slot<void(DiscordError code)> callback);
|
||||
void DeleteEmoji(Snowflake guild_id, Snowflake emoji_id, 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;
|
||||
void RemoveRelationship(Snowflake id, sigc::slot<void(DiscordError code)> callback);
|
||||
void SendFriendRequest(const Glib::ustring &username, int discriminator, sigc::slot<void(DiscordError code)> callback);
|
||||
void PutRelationship(Snowflake id, sigc::slot<void(DiscordError code)> callback); // send fr by id, accept incoming
|
||||
void Pin(Snowflake channel_id, Snowflake message_id, sigc::slot<void(DiscordError code)> callback);
|
||||
void Unpin(Snowflake channel_id, Snowflake message_id, sigc::slot<void(DiscordError code)> callback);
|
||||
void LeaveThread(Snowflake channel_id, const std::string &location, sigc::slot<void(DiscordError code)> callback);
|
||||
void ArchiveThread(Snowflake channel_id, sigc::slot<void(DiscordError code)> callback);
|
||||
void UnArchiveThread(Snowflake channel_id, sigc::slot<void(DiscordError code)> callback);
|
||||
|
||||
bool CanModifyRole(Snowflake guild_id, Snowflake role_id) const;
|
||||
bool CanModifyRole(Snowflake guild_id, Snowflake role_id, Snowflake user_id) const;
|
||||
|
||||
// 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, 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));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -170,23 +167,18 @@ public:
|
||||
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(DiscordError code)> callback);
|
||||
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, sigc::slot<void(std::vector<Message>, DiscordError code)> callback);
|
||||
|
||||
bool IsVerificationRequired(Snowflake guild_id);
|
||||
void GetVerificationGateInfo(Snowflake guild_id, sigc::slot<void(std::optional<VerificationGateInfoObject>)> callback);
|
||||
void AcceptVerificationGate(Snowflake guild_id, VerificationGateInfoObject info, sigc::slot<void(DiscordError code)> callback);
|
||||
void AcceptVerificationGate(Snowflake guild_id, VerificationGateInfoObject info, sigc::slot<void(bool success)> callback);
|
||||
|
||||
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;
|
||||
@@ -194,11 +186,6 @@ private:
|
||||
std::vector<uint8_t> m_decompress_buf;
|
||||
z_stream m_zstream;
|
||||
|
||||
std::string GetAPIURL();
|
||||
std::string GetGatewayURL();
|
||||
|
||||
static DiscordError GetCodeFromResponse(const http::response_type &response);
|
||||
|
||||
void ProcessNewGuild(GuildData &guild);
|
||||
|
||||
void HandleGatewayMessageRaw(std::string str);
|
||||
@@ -235,15 +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 HandleGatewayReadySupplemental(const GatewayMessage &msg);
|
||||
void HandleGatewayReconnect(const GatewayMessage &msg);
|
||||
void HandleGatewayInvalidSession(const GatewayMessage &msg);
|
||||
@@ -261,14 +239,18 @@ private:
|
||||
|
||||
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::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;
|
||||
@@ -278,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;
|
||||
@@ -302,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;
|
||||
@@ -316,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
|
||||
@@ -329,30 +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;
|
||||
|
||||
// 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, 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();
|
||||
@@ -383,19 +346,6 @@ 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_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_message_send_fail signal_message_send_fail();
|
||||
type_signal_disconnected signal_disconnected();
|
||||
@@ -430,19 +380,6 @@ 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_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_message_send_fail m_signal_message_send_fail;
|
||||
type_signal_disconnected m_signal_disconnected;
|
||||
@@ -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);
|
||||
@@ -191,12 +190,8 @@ std::vector<Snowflake> GuildData::GetSortedChannels(Snowflake ignore) const {
|
||||
std::vector<RoleData> GuildData::FetchRoles() const {
|
||||
if (!Roles.has_value()) return {};
|
||||
std::vector<RoleData> ret;
|
||||
ret.reserve(Roles->size());
|
||||
for (const auto thing : *Roles) {
|
||||
auto r = Abaddon::Get().GetDiscordClient().GetRole(thing.ID);
|
||||
if (r.has_value())
|
||||
ret.push_back(*r);
|
||||
}
|
||||
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;
|
||||
});
|
||||
@@ -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;
|
||||
@@ -1,14 +1,11 @@
|
||||
#include "httpclient.hpp"
|
||||
|
||||
//#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 = agent;
|
||||
}
|
||||
@@ -7,13 +7,11 @@
|
||||
#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);
|
||||
@@ -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);
|
||||
@@ -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>
|
||||
@@ -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 {
|
||||
@@ -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);
|
||||
}
|
||||
@@ -8,17 +8,13 @@
|
||||
|
||||
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;
|
||||
|
||||
std::vector<RoleData> GetSortedRoles() const;
|
||||
|
||||
@@ -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) {
|
||||
@@ -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() {
|
||||
@@ -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
|
||||
@@ -85,16 +85,10 @@ 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) {
|
||||
@@ -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";
|
||||
@@ -458,77 +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);
|
||||
}
|
||||
@@ -19,7 +19,6 @@
|
||||
#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
|
||||
|
||||
@@ -69,15 +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,
|
||||
};
|
||||
|
||||
enum class GatewayCloseCode : uint16_t {
|
||||
@@ -205,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);
|
||||
};
|
||||
@@ -638,110 +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);
|
||||
};
|
||||
@@ -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 "";
|
||||
}
|
||||
@@ -5,8 +5,6 @@
|
||||
|
||||
constexpr static uint64_t DiscordEpochSeconds = 1420070400;
|
||||
|
||||
const Snowflake Snowflake::Invalid = -1ULL;
|
||||
|
||||
Snowflake::Snowflake()
|
||||
: m_num(Invalid) {}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user