87 Commits

Author SHA1 Message Date
ouwou
f9864a24ed update readme 2022-01-27 00:15:26 -05:00
ouwou
738d50dd43 add setting to not show unread stuff 2022-01-26 18:44:31 -05:00
ouwou
7d49f934bc muted dms dont contribute to unread count 2022-01-26 18:43:47 -05:00
ouwou
fbb5522861 bump vcpkg 2022-01-23 20:27:08 -05:00
ouwou
0ce509f80e add settings for some colors 2022-01-21 00:41:35 -05:00
ouwou
b6b215ee6f add mark as unread/toggle mute for threads 2022-01-20 02:45:28 -05:00
ouwou
d7f3ee9f98 handle mute/unmute updates for threads 2022-01-20 01:52:48 -05:00
ouwou
2328c8bafe handle initial muted state for threads 2022-01-20 01:40:27 -05:00
ouwou
dfd642bb82 show unread indicators for threads 2022-01-20 01:34:36 -05:00
ouwou
6c9bf4ff81 add toggle mute dm menu item 2022-01-15 01:51:11 -05:00
ouwou
604f2ffe3d show count of unread dms in header 2022-01-08 20:03:12 -05:00
ouwou
4e0b22375f handle mute/unmute for dms 2022-01-08 18:35:46 -05:00
ouwou
9d0c7691d8 fix initial read state for dms 2022-01-05 20:34:44 -05:00
ouwou
cef28e94ea add missing reset 2022-01-05 04:06:02 -05:00
ouwou
40106ddeb1 handle mutable categories 2022-01-05 03:52:20 -05:00
ouwou
8695562cb4 Merge branch 'master' into unread 2022-01-02 00:07:32 -05:00
ouwou
5338eab3a5 speed up connection speed a good bit
loading save state was slow so now theres a temporary lookup table
2021-12-31 16:42:06 -05:00
ouwou
d7bb6049e1 add mute/unmute guild menu item 2021-12-30 01:24:55 -05:00
ouwou
ea7464722b handle change of mute state for guilds 2021-12-29 23:51:12 -05:00
ouwou
d6da646d87 validate iso8601 when parsing to snowflake 2021-12-29 22:15:04 -05:00
ouwou
17c1f913df actually deserialize mute_config 2021-12-28 03:11:59 -05:00
ouwou
6c94e75513 take mute_config.end_time into account for muted entries 2021-12-28 02:58:31 -05:00
ouwou
801894abc6 messages sent by user shouldnt count as new unreads 2021-12-28 02:21:46 -05:00
ouwou
207c004228 take muted channels into account for unread guild indicator 2021-12-25 03:07:11 -05:00
ouwou
36f73a6106 check view permissions for channels in read state 2021-12-25 02:59:01 -05:00
ouwou
41d80af128 mark more channels as unread properly 2021-12-25 02:37:31 -05:00
ouwou
145504bdd6 add mark all as read 2021-12-22 01:44:26 -05:00
ouwou
9fd0d404a1 mark channel being switched off as read when switching 2021-12-20 02:13:18 -05:00
ouwou
b75599e55d fix bad if statement causing UB 2021-12-20 01:45:43 -05:00
ouwou
67062d6ed8 unread indicator for dm channels 2021-12-18 03:24:44 -05:00
ouwou
c43d49ed54 grey out muted channels in list 2021-12-18 02:17:43 -05:00
ouwou
e9867173c9 inline unread rendering 2021-12-18 02:06:16 -05:00
ouwou
f580535d35 add mute/unmute channel menu item 2021-12-18 01:58:29 -05:00
ouwou
1d7529e609 handle mute/unmute of channels (USER_GUILD_SETTINGS_UPDATE) 2021-12-17 02:34:14 -05:00
ouwou
1fb7ca0007 hide unread indicator for muted channels 2021-12-16 00:58:17 -05:00
ouwou
b576bd0fcc make fallback for config file go in home directory if possible (#52) 2021-12-15 17:43:11 -05:00
ouwou
a5332efcfb fix compile 2021-12-12 21:14:20 -05:00
ouwou
15954830e2 hide guild unread indicator for muted guilds 2021-12-10 03:26:33 -05:00
ouwou
46ab760a56 render total mentions on guild, redraw on message create 2021-12-10 01:41:19 -05:00
ouwou
0b0135268e basic channel mentions count indicator 2021-12-10 00:15:39 -05:00
ouwou
511fb445d1 rudimentary guild unread indicator 2021-12-09 02:54:59 -05:00
ouwou
bcfb2146cd mark guild as read (shift+esc) 2021-12-08 19:12:35 -05:00
ouwou
a1b662a325 make mark guild as read actually work properly 2021-12-07 02:51:29 -05:00
ouwou
14b5bf7d0d reorder menu items 2021-12-06 17:35:44 -05:00
ouwou
d288989386 mark guild as read 2021-12-06 03:04:22 -05:00
ouwou
d63941797f mark channels as unread on MESSAGE_CREATE 2021-12-05 04:07:30 -05:00
ouwou
1ea2811713 dont send acks for channels known to be read 2021-12-05 04:00:02 -05:00
ouwou
af56784797 basic unread indicators for channels 2021-12-05 03:57:26 -05:00
ouwou
2461406887 split channel CellRenderer into its own sources 2021-12-04 02:21:08 -05:00
ouwou
8e11dd97e9 dont make requests for inaccessible channels 2021-12-01 03:42:15 -05:00
ouwou
2690febf20 fix corrupted disk image sqlite error (fixes #51) 2021-11-29 21:51:15 -05:00
ouwou
af3d278825 rename find module (fixes #50) 2021-11-29 17:16:11 -05:00
ouwou
e02107feea actually retrieve roles for guilds
FetchRoles isnt needed anymore cuz full roles are fetched now
2021-11-28 22:42:55 -05:00
ouwou
192b043e7a fix distortion of non-1:1 emojis 2021-11-28 22:40:41 -05:00
ouwou
8c72d4c18d dont print identify message to console
mainly since i feel its only a matter of time before someone copy pastes it somewhere and itd be my fault
also typedef -> using
2021-11-25 02:57:11 -05:00
ouwou
0da913cd4a remove unnecessary copying left over from debugging 2021-11-25 02:32:39 -05:00
ouwou
fb3d69c5e7 bump build number in identify 2021-11-24 22:36:39 -05:00
ouwou
c40208ac66 update readme 2021-11-24 22:24:40 -05:00
ouwou
069c22e9cd add fetching private archived threads 2021-11-24 20:31:34 -05:00
ouwou
8f30bb33a3 fix build 2021-11-24 03:29:29 -05:00
ouwou
4326c5e29b remove SimpleIni as a dependency
use Glib::KeyFile instead which is basically the same file format
also read into and save from struct once, cuz its faster and less redundant
2021-11-24 03:15:22 -05:00
Dylam De La Torre
a51a54bc59 Restructure source and resource files (#46)
importantly, res is now res/res and css is now res/css
2021-11-23 04:21:56 +00:00
ouwou
d88079000a normalize include paths 2021-11-20 22:29:03 -05:00
ouwou
574cbc35d8 merge store 2021-11-20 18:48:15 -05:00
ouwou
fc76a15c46 fix sqlite error messages 2021-11-19 01:08:03 -05:00
Dylam De La Torre
9d21df8e1b Fix warnings shown by GCC (#47)
* fix all warnings shown by GCC
2021-11-16 19:38:14 +00:00
ouwou
1da2c57376 fix retrieving message references
and also add a reset statement i forgot (raii might be good here...)
2021-11-16 02:37:12 -05:00
ouwou
409af292af Merge branch 'master' into store 2021-11-14 02:27:23 -05:00
ouwou
108002248c presence of ThreadMember means user added to private thread 2021-11-13 03:09:17 -05:00
ouwou
43795f4c87 dont call StopDiscord in destructor
since the destructor is run during static destruction it can cause issues
it's redundant anyways as StopDiscord is slotted into Gtk::Application::signal_shutdown
2021-11-13 00:25:54 -05:00
ouwou
2257ff9798 fix double close of store throwing 2021-11-06 01:10:56 -04:00
ouwou
c40b8a4122 add files that should have been pushed with last commit
oops
2021-11-04 01:48:13 -04:00
ouwou
1f445742b4 preserve channel list expansion and active channel (#36)
also check getenv in platform
2021-11-04 01:39:56 -04:00
ouwou
d629846220 Merge branch 'master' of https://github.com/uowuo/abaddon 2021-11-02 21:01:58 -04:00
ouwou
b26b2d4be0 remove channel row css classes from readme 2021-11-02 21:01:54 -04:00
South
b65c09411a fix typo (#41) 2021-11-01 23:56:47 +00:00
ouwou
a51b813f70 some snowflake IsValid checks
mainly so i could debug something easier
2021-10-30 02:47:14 -04:00
ouwou
1078d94b73 fix big mistake that made it not run 2021-10-30 02:46:50 -04:00
ouwou
12c105623c templatize some stuff 2021-10-29 01:35:16 -04:00
ouwou
d950460e14 try to fix some more compilation errors/warnings 2021-10-28 04:19:05 -04:00
ouwou
2c2686946d try to fix weird ambiguous int stuff 2021-10-28 02:33:13 -04:00
ouwou
9175da2cf0 take care of some warnings 2021-10-28 02:19:13 -04:00
ouwou
14d025a342 dont use functions that dont actually exist 2021-10-28 02:08:29 -04:00
ouwou
c98b62caca fix syntax error 2021-10-28 00:42:04 -04:00
ouwou
d425997c26 rewrite store
this probably should have been broken up into smaller commits. oh well
2021-10-28 00:31:35 -04:00
ouwou
84b1b62e0e fix UB when all reactions on a msg are removed and one is added again 2021-10-27 03:01:53 -04:00
ouwou
b248e0ae9a set action versions 2021-10-18 23:56:36 -04:00
226 changed files with 5150 additions and 2970 deletions

View File

@@ -15,17 +15,17 @@ jobs:
submodules: true
- name: Fetch CMake
uses: lukka/get-cmake@latest
uses: lukka/get-cmake@v3.21.2
- name: Fetch dependencies
uses: lukka/run-vcpkg@main
uses: lukka/run-vcpkg@v7
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@main
uses: lukka/run-cmake@v3
with:
useVcpkgToolchainFile: true
vcpkgTriplet: x64-windows
@@ -41,9 +41,9 @@ 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 }}\css" "${{ runner.workspace }}\build\css"
xcopy /E /I "${{ github.workspace }}\res" "${{ runner.workspace }}\build\res"
xcopy /E /I "${{ github.workspace }}\fonts" "${{ runner.workspace }}\build\fonts"
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"
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 +66,7 @@ jobs:
submodules: true
- name: Fetch CMake
uses: lukka/get-cmake@latest
uses: lukka/get-cmake@v3.21.2
- name: Fetch dependencies
run: |
@@ -74,7 +74,7 @@ jobs:
brew install nlohmann-json
- name: Build
uses: lukka/run-cmake@main
uses: lukka/run-cmake@v3
with:
buildDirectory: ${{ runner.workspace }}/build
cmakeBuildType: ${{ matrix.buildtype }}
@@ -83,8 +83,8 @@ jobs:
run: |
mkdir "${{ runner.workspace }}/artifactdir"
cp "${{runner.workspace}}/build/abaddon" "${{ runner.workspace }}/artifactdir/abaddon"
cp -r "${{ github.workspace }}/css" "${{ runner.workspace }}/artifactdir/css"
cp -r "${{ github.workspace }}/res" "${{ runner.workspace }}/artifactdir/res"
cp -r "${{ github.workspace }}/res/css" "${{ runner.workspace }}/artifactdir/css"
cp -r "${{ github.workspace }}/res/res" "${{ runner.workspace }}/artifactdir/res"
- name: Upload build
uses: actions/upload-artifact@v2
@@ -104,7 +104,7 @@ jobs:
submodules: true
- name: Fetch CMake
uses: lukka/get-cmake@latest
uses: lukka/get-cmake@v3.21.2
- name: Fetch dependencies
run: |
@@ -123,7 +123,7 @@ jobs:
sudo apt-get install libcurl4-gnutls-dev
- name: Build
uses: lukka/run-cmake@main
uses: lukka/run-cmake@v3
env:
CC: gcc-9
CXX: g++-9
@@ -136,8 +136,8 @@ jobs:
run: |
mkdir "${{ runner.workspace }}/artifactdir"
cp "${{runner.workspace}}/build/abaddon" "${{ runner.workspace }}/artifactdir/abaddon"
cp -r "${{ github.workspace }}/css" "${{ runner.workspace }}/artifactdir/css"
cp -r "${{ github.workspace }}/res" "${{ runner.workspace }}/artifactdir/res"
cp -r "${{ github.workspace }}/res/css" "${{ runner.workspace }}/artifactdir/css"
cp -r "${{ github.workspace }}/res/res" "${{ runner.workspace }}/artifactdir/res"
- name: Upload build
uses: actions/upload-artifact@v2

9
.gitmodules vendored
View File

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

View File

@@ -18,17 +18,10 @@ 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(thirdparty/IXWebSocket)
add_subdirectory(subprojects/ixwebsocket)
include_directories(IXWEBSOCKET_INCLUDE_DIRS)
endif()
add_compile_definitions(SI_NO_CONVERSION) # only CSimpleIniA is used
find_package(simpleini QUIET)
if (NOT simpleini_FOUND)
message("simpleini was not found and will be included as a submodule")
include_directories(thirdparty/simpleini)
endif()
if(MINGW OR WIN32)
link_libraries(ws2_32)
endif()
@@ -41,28 +34,16 @@ if(WIN32)
link_libraries(${Fontconfig_LIBRARIES})
endif()
configure_file(${PROJECT_SOURCE_DIR}/config.h.in ${PROJECT_BINARY_DIR}/config.h)
configure_file(${PROJECT_SOURCE_DIR}/src/config.h.in ${PROJECT_BINARY_DIR}/config.h)
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"
file(GLOB_RECURSE ABADDON_SOURCES
"src/*.h"
"src/*.hpp"
"src/*.cpp"
)
add_executable(abaddon ${ABADDON_SOURCES})
target_include_directories(abaddon PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
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})

View File

@@ -4,7 +4,7 @@ Alternative Discord client made in C++ with GTK
<img src="/.readme/s3.png">
[😎Discord Server](https://discord.gg/wkCU3vuzG5)
<a href="https://discord.gg/wkCU3vuzG5"><img src="https://discord.com/api/guilds/858156817711890443/widget.png?style=shield"></a>
Current features:
* Not Electron
@@ -12,6 +12,7 @@ Current features:
* 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>
* Set status
* Unread and mention indicators
* Start new DMs and group DMs
* View user profiles (notes, mutual servers, mutual friends)
* Kick, ban, and unban members
@@ -25,14 +26,14 @@ Current features:
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 subtituted 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.
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.
### 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 simpleini: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 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
@@ -75,12 +76,10 @@ 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:
* Voice support
* Unread indicators
* User activities
* Nicknames
* More server management stuff
@@ -93,12 +92,6 @@ 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
@@ -184,18 +177,24 @@ 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 10) - how many images can be concurrently retrieved
* concurrent (int, default 20) - how many images can be concurrently retrieved
#### gui
* member_list_discriminator (true or false, default true) - show user discriminators in the member list
@@ -205,13 +204,15 @@ For example, memory_db would be set by adding `memory_db = true` under the line
* 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
* 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
* unreads (true or false, default true) - show unread indicators and mention badges
#### style
* linkcolor (string) - color to use for links in messages
* expandercolor (string) - color to use for the expander in the channel list
* nsfwchannelcolor (string) - color to use for NSFW channels in the channel list
* channelcolor (string) - color to use for SFW channels in the channel list
* mentionbadgecolor (string) - background color for mention badges
* mentionbadgetextcolor (string) - color to use for number displayed on mention badges
### Environment variables

View File

@@ -1,15 +0,0 @@
set(simpleini_LIBRARY_NAME simpleini)
find_path(simpleini_INCLUDE_DIR
NAMES SimpleIni.h
HINTS /usr/include
/usr/local/include
/opt/local/include
PATH_SUFFIXES ${simpleini_LIBRARY_NAME})
include(FindPackageHandleStandardArgs)
find_package_handle_standard_args(simpleini
REQUIRED_VARS
simpleini_INCLUDE_DIR)
mark_as_advanced(simpleini_INCLUDE_DIR)

File diff suppressed because it is too large Load Diff

View File

@@ -1,172 +0,0 @@
#pragma once
#include "../util.hpp"
#include "objects.hpp"
#include <unordered_map>
#include <unordered_set>
#include <mutex>
#include <filesystem>
#include <sqlite3.h>
#ifdef GetMessage // fuck you windows.h
#undef GetMessage
#endif
class Store {
public:
Store(bool mem_store = false);
~Store();
bool IsValid() const;
void SetUser(Snowflake id, const UserData &user);
void SetChannel(Snowflake id, const ChannelData &chan);
void SetGuild(Snowflake id, const GuildData &guild);
void SetRole(Snowflake id, const RoleData &role);
void SetMessage(Snowflake id, const Message &message);
void SetGuildMember(Snowflake guild_id, Snowflake user_id, const GuildMember &data);
void SetPermissionOverwrite(Snowflake channel_id, Snowflake id, const PermissionOverwrite &perm);
void SetEmoji(Snowflake id, const EmojiData &emoji);
void SetBan(Snowflake guild_id, Snowflake user_id, const BanData &ban);
// slap const on everything even tho its not *really* const
std::optional<ChannelData> GetChannel(Snowflake id) const;
std::optional<EmojiData> GetEmoji(Snowflake id) const;
std::optional<GuildData> GetGuild(Snowflake id) const;
std::optional<GuildMember> GetGuildMember(Snowflake guild_id, Snowflake user_id) const;
std::optional<Message> GetMessage(Snowflake id) const;
std::optional<PermissionOverwrite> GetPermissionOverwrite(Snowflake channel_id, Snowflake id) const;
std::optional<RoleData> GetRole(Snowflake id) const;
std::optional<UserData> GetUser(Snowflake id) const;
std::optional<BanData> GetBan(Snowflake guild_id, Snowflake user_id) const;
std::vector<BanData> GetBans(Snowflake guild_id) const;
std::vector<Message> GetLastMessages(Snowflake id, size_t num) const;
std::vector<Snowflake> GetChannelMessageIDs(Snowflake id) const;
std::vector<Message> GetPinnedMessages(Snowflake channel_id) const;
std::vector<ChannelData> GetActiveThreads(Snowflake channel_id) const; // public
void ClearGuild(Snowflake id);
void ClearChannel(Snowflake id);
void ClearBan(Snowflake guild_id, Snowflake user_id);
using users_type = std::unordered_map<Snowflake, UserData>;
using channels_type = std::unordered_map<Snowflake, ChannelData>;
using guilds_type = std::unordered_map<Snowflake, GuildData>;
using roles_type = std::unordered_map<Snowflake, RoleData>;
using messages_type = std::unordered_map<Snowflake, Message>;
using members_type = std::unordered_map<Snowflake, std::unordered_map<Snowflake, GuildMember>>; // [guild][user]
using permission_overwrites_type = std::unordered_map<Snowflake, std::unordered_map<Snowflake, PermissionOverwrite>>; // [channel][user/role]
using emojis_type = std::unordered_map<Snowflake, EmojiData>;
const std::unordered_set<Snowflake> &GetChannels() const;
const std::unordered_set<Snowflake> &GetGuilds() const;
void ClearAll();
void BeginTransaction();
void EndTransaction();
private:
Message GetMessageBound(sqlite3_stmt *stmt) const;
void SetMessageInteractionPair(Snowflake message_id, const MessageInteractionData &interaction);
std::unordered_set<Snowflake> m_channels;
std::unordered_set<Snowflake> m_guilds;
bool CreateTables();
bool CreateStatements();
void Cleanup();
template<typename T>
void Bind(sqlite3_stmt *stmt, int index, const std::optional<T> &opt) const;
template<typename T>
typename std::enable_if<std::is_enum<T>::value, void>::type
Bind(sqlite3_stmt *stmt, int index, T val) const;
void Bind(sqlite3_stmt *stmt, int index, int num) const;
void Bind(sqlite3_stmt *stmt, int index, uint64_t num) const;
void Bind(sqlite3_stmt *stmt, int index, const std::string &str) const;
void Bind(sqlite3_stmt *stmt, int index, bool val) const;
void Bind(sqlite3_stmt *stmt, int index, std::nullptr_t) const;
bool RunInsert(sqlite3_stmt *stmt);
bool FetchOne(sqlite3_stmt *stmt) const;
template<typename T>
void Get(sqlite3_stmt *stmt, int index, std::optional<T> &out) const;
template<typename T>
typename std::enable_if<std::is_enum<T>::value, void>::type
Get(sqlite3_stmt *stmt, int index, T &out) const;
void Get(sqlite3_stmt *stmt, int index, int &out) const;
void Get(sqlite3_stmt *stmt, int index, uint64_t &out) const;
void Get(sqlite3_stmt *stmt, int index, std::string &out) const;
void Get(sqlite3_stmt *stmt, int index, bool &out) const;
void Get(sqlite3_stmt *stmt, int index, Snowflake &out) const;
bool IsNull(sqlite3_stmt *stmt, int index) const;
void Reset(sqlite3_stmt *stmt) const;
std::filesystem::path m_db_path;
mutable sqlite3 *m_db;
mutable int m_db_err;
mutable sqlite3_stmt *m_set_user_stmt;
mutable sqlite3_stmt *m_get_user_stmt;
mutable sqlite3_stmt *m_set_perm_stmt;
mutable sqlite3_stmt *m_get_perm_stmt;
mutable sqlite3_stmt *m_set_msg_stmt;
mutable sqlite3_stmt *m_get_msg_stmt;
mutable sqlite3_stmt *m_set_role_stmt;
mutable sqlite3_stmt *m_get_role_stmt;
mutable sqlite3_stmt *m_set_emote_stmt;
mutable sqlite3_stmt *m_get_emote_stmt;
mutable sqlite3_stmt *m_set_member_stmt;
mutable sqlite3_stmt *m_get_member_stmt;
mutable sqlite3_stmt *m_set_guild_stmt;
mutable sqlite3_stmt *m_get_guild_stmt;
mutable sqlite3_stmt *m_set_chan_stmt;
mutable sqlite3_stmt *m_get_chan_stmt;
mutable sqlite3_stmt *m_set_ban_stmt;
mutable sqlite3_stmt *m_get_ban_stmt;
mutable sqlite3_stmt *m_clear_ban_stmt;
mutable sqlite3_stmt *m_get_bans_stmt;
mutable sqlite3_stmt *m_set_msg_interaction_stmt;
mutable sqlite3_stmt *m_get_last_msgs_stmt;
mutable sqlite3_stmt *m_get_msg_ids_stmt;
mutable sqlite3_stmt *m_get_pins_stmt;
mutable sqlite3_stmt *m_get_threads_stmt;
mutable sqlite3_stmt *m_clear_chan_stmt;
};
template<typename T>
inline void Store::Bind(sqlite3_stmt *stmt, int index, const std::optional<T> &opt) const {
if (opt.has_value())
Bind(stmt, index, *opt);
else
sqlite3_bind_null(stmt, index);
}
template<typename T>
inline typename std::enable_if<std::is_enum<T>::value, void>::type
Store::Bind(sqlite3_stmt *stmt, int index, T val) const {
Bind(stmt, index, static_cast<typename std::underlying_type<T>::type>(val));
}
template<typename T>
inline void Store::Get(sqlite3_stmt *stmt, int index, std::optional<T> &out) const {
if (sqlite3_column_type(stmt, index) == SQLITE_NULL)
out = std::nullopt;
else {
T v;
Get(stmt, index, v);
out = std::optional<T>(v);
}
}
template<typename T>
inline typename std::enable_if<std::is_enum<T>::value, void>::type
Store::Get(sqlite3_stmt *stmt, int index, T &out) const {
out = static_cast<T>(sqlite3_column_int(stmt, index));
}

View File

Before

Width:  |  Height:  |  Size: 7.9 KiB

After

Width:  |  Height:  |  Size: 7.9 KiB

View File

Before

Width:  |  Height:  |  Size: 7.0 KiB

After

Width:  |  Height:  |  Size: 7.0 KiB

View File

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 48 KiB

View File

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 3.4 KiB

View File

Before

Width:  |  Height:  |  Size: 6.5 KiB

After

Width:  |  Height:  |  Size: 6.5 KiB

View File

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

Before

Width:  |  Height:  |  Size: 6.6 KiB

After

Width:  |  Height:  |  Size: 6.6 KiB

View File

Before

Width:  |  Height:  |  Size: 6.9 KiB

After

Width:  |  Height:  |  Size: 6.9 KiB

View File

Before

Width:  |  Height:  |  Size: 8.2 KiB

After

Width:  |  Height:  |  Size: 8.2 KiB

View File

Before

Width:  |  Height:  |  Size: 8.8 KiB

After

Width:  |  Height:  |  Size: 8.8 KiB

View File

Before

Width:  |  Height:  |  Size: 7.0 KiB

After

Width:  |  Height:  |  Size: 7.0 KiB

View File

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

Before

Width:  |  Height:  |  Size: 6.5 KiB

After

Width:  |  Height:  |  Size: 6.5 KiB

View File

Before

Width:  |  Height:  |  Size: 4.3 KiB

After

Width:  |  Height:  |  Size: 4.3 KiB

View File

Before

Width:  |  Height:  |  Size: 7.1 KiB

After

Width:  |  Height:  |  Size: 7.1 KiB

View File

Before

Width:  |  Height:  |  Size: 7.3 KiB

After

Width:  |  Height:  |  Size: 7.3 KiB

View File

Before

Width:  |  Height:  |  Size: 7.3 KiB

After

Width:  |  Height:  |  Size: 7.3 KiB

View File

Before

Width:  |  Height:  |  Size: 6.6 KiB

After

Width:  |  Height:  |  Size: 6.6 KiB

View File

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 3.4 KiB

View File

Before

Width:  |  Height:  |  Size: 5.2 KiB

After

Width:  |  Height:  |  Size: 5.2 KiB

View File

Before

Width:  |  Height:  |  Size: 7.0 KiB

After

Width:  |  Height:  |  Size: 7.0 KiB

View File

Before

Width:  |  Height:  |  Size: 6.3 KiB

After

Width:  |  Height:  |  Size: 6.3 KiB

View File

Before

Width:  |  Height:  |  Size: 6.6 KiB

After

Width:  |  Height:  |  Size: 6.6 KiB

View File

Before

Width:  |  Height:  |  Size: 4.8 KiB

After

Width:  |  Height:  |  Size: 4.8 KiB

View File

Before

Width:  |  Height:  |  Size: 6.9 KiB

After

Width:  |  Height:  |  Size: 6.9 KiB

View File

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

Before

Width:  |  Height:  |  Size: 3.6 KiB

After

Width:  |  Height:  |  Size: 3.6 KiB

View File

Before

Width:  |  Height:  |  Size: 4.1 KiB

After

Width:  |  Height:  |  Size: 4.1 KiB

View File

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

View File

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 3.3 KiB

View File

@@ -1,111 +0,0 @@
#include "settings.hpp"
#include <filesystem>
#include <fstream>
SettingsManager::SettingsManager(std::string filename)
: m_filename(filename) {
if (!std::filesystem::exists(filename)) {
std::fstream fs;
fs.open(filename, std::ios::out);
fs.close();
}
auto rc = m_ini.LoadFile(filename.c_str());
m_ok = rc == SI_OK;
}
void SettingsManager::Reload() {
m_ok = m_ini.LoadFile(m_filename.c_str()) == SI_OK;
}
std::string SettingsManager::GetSettingString(const std::string &section, const std::string &key, std::string fallback) const {
return m_ini.GetValue(section.c_str(), key.c_str(), fallback.c_str());
}
int SettingsManager::GetSettingInt(const std::string &section, const std::string &key, int fallback) const {
return std::stoul(GetSettingString(section, key, std::to_string(fallback)));
}
bool SettingsManager::GetSettingBool(const std::string &section, const std::string &key, bool fallback) const {
return GetSettingString(section, key, fallback ? "true" : "false") != "false";
}
bool SettingsManager::IsValid() const {
return m_ok;
}
void SettingsManager::Close() {
m_ini.SaveFile(m_filename.c_str());
}
bool SettingsManager::GetUseMemoryDB() const {
return GetSettingBool("discord", "memory_db", false);
}
std::string SettingsManager::GetUserAgent() const {
return GetSettingString("http", "user_agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.87 Safari/537.36");
}
std::string SettingsManager::GetDiscordToken() const {
return GetSettingString("discord", "token");
}
bool SettingsManager::GetShowMemberListDiscriminators() const {
return GetSettingBool("gui", "member_list_discriminator", true);
}
bool SettingsManager::GetShowStockEmojis() const {
#ifdef _WIN32
return GetSettingBool("gui", "stock_emojis", false);
#else
return GetSettingBool("gui", "stock_emojis", true);
#endif
}
bool SettingsManager::GetShowCustomEmojis() const {
return GetSettingBool("gui", "custom_emojis", true);
}
std::string SettingsManager::GetLinkColor() const {
return GetSettingString("style", "linkcolor", "rgba(40, 200, 180, 255)");
}
std::string SettingsManager::GetChannelsExpanderColor() const {
return GetSettingString("style", "expandercolor", "rgba(255, 83, 112, 255)");
}
std::string SettingsManager::GetNSFWChannelColor() const {
return GetSettingString("style", "nsfwchannelcolor", "#ed6666");
}
int SettingsManager::GetCacheHTTPConcurrency() const {
return GetSettingInt("http", "concurrent", 20);
}
bool SettingsManager::GetPrefetch() const {
return GetSettingBool("discord", "prefetch", false);
}
std::string SettingsManager::GetMainCSS() const {
return GetSettingString("gui", "css", "main.css");
}
bool SettingsManager::GetShowAnimations() const {
return GetSettingBool("gui", "animations", true);
}
bool SettingsManager::GetShowOwnerCrown() const {
return GetSettingBool("gui", "owner_crown", true);
}
std::string SettingsManager::GetGatewayURL() const {
return GetSettingString("discord", "gateway", "wss://gateway.discord.gg/?v=9&encoding=json&compress=zlib-stream");
}
std::string SettingsManager::GetAPIBaseURL() const {
return GetSettingString("discord", "api_base", "https://discord.com/api/v9");
}
bool SettingsManager::GetAnimatedGuildHoverOnly() const {
return GetSettingBool("gui", "animated_guild_hover_only", true);
}

View File

@@ -1,61 +0,0 @@
#pragma once
#include <string>
#include <type_traits>
#include <SimpleIni.h>
class SettingsManager {
public:
SettingsManager(std::string filename);
void Reload();
void Close();
bool GetUseMemoryDB() const;
std::string GetUserAgent() const;
std::string GetDiscordToken() const;
bool GetShowMemberListDiscriminators() const;
bool GetShowStockEmojis() const;
bool GetShowCustomEmojis() const;
int GetCacheHTTPConcurrency() const;
bool GetPrefetch() const;
std::string GetMainCSS() const;
bool GetShowAnimations() const;
bool GetShowOwnerCrown() const;
std::string GetGatewayURL() const;
std::string GetAPIBaseURL() const;
bool GetAnimatedGuildHoverOnly() const;
// i would like to use Gtk::StyleProperty for this, but it will not work on windows
// #1 it's missing from the project files for the version used by vcpkg
// #2 it's still broken and doesn't function even when added to the solution
// #3 it's a massive pain in the ass to try and bump the version to a functioning version
// because they switch build systems to nmake/meson (took months to get merged in vcpkg)
// #4 c++ build systems sucks
// three options are: use gtk4 with updated vcpkg, try and port it myself, or use msys2 instead of vcpkg
// im leaning towards msys
std::string GetLinkColor() const;
std::string GetChannelsExpanderColor() const;
std::string GetNSFWChannelColor() const;
bool IsValid() const;
template<typename T>
void SetSetting(std::string section, std::string key, T value) {
m_ini.SetValue(section.c_str(), key.c_str(), std::to_string(value).c_str());
m_ini.SaveFile(m_filename.c_str());
}
void SetSetting(std::string section, std::string key, std::string value) {
m_ini.SetValue(section.c_str(), key.c_str(), value.c_str());
m_ini.SaveFile(m_filename.c_str());
}
private:
std::string GetSettingString(const std::string &section, const std::string &key, std::string fallback = "") const;
int GetSettingInt(const std::string &section, const std::string &key, int fallback) const;
bool GetSettingBool(const std::string &section, const std::string &key, bool fallback) const;
private:
bool m_ok;
std::string m_filename;
CSimpleIniA m_ini;
};

View File

@@ -23,12 +23,12 @@
Abaddon::Abaddon()
: m_settings(Platform::FindConfigFile())
, m_emojis(GetResPath("/emojis.bin"))
, m_discord(m_settings.GetUseMemoryDB()) { // stupid but easy
, m_discord(GetSettings().UseMemoryDB) // stupid but easy
, m_emojis(GetResPath("/emojis.bin")) {
LoadFromSettings();
// todo: set user agent for non-client(?)
std::string ua = m_settings.GetUserAgent();
std::string ua = GetSettings().UserAgent;
m_discord.SetUserAgent(ua);
m_discord.signal_gateway_ready().connect(sigc::mem_fun(*this, &Abaddon::DiscordOnReady));
@@ -43,7 +43,7 @@ Abaddon::Abaddon()
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 (m_settings.GetPrefetch())
if (GetSettings().Prefetch)
m_discord.signal_message_create().connect([this](const Message &message) {
if (message.Author.HasAvatar())
m_img_mgr.Prefetch(message.Author.GetAvatarURL());
@@ -54,11 +54,6 @@ Abaddon::Abaddon()
});
}
Abaddon::~Abaddon() {
m_settings.Close();
m_discord.Stop();
}
Abaddon &Abaddon::Get() {
static Abaddon instance;
return instance;
@@ -83,9 +78,30 @@ int Abaddon::StartGTK() {
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);
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);
dlg.set_position(Gtk::WIN_POS_CENTER);
dlg.run();
}
if (!m_emojis.Load()) {
Gtk::MessageDialog dlg(*m_main_window, "The emoji file couldn't be loaded!", false, Gtk::MESSAGE_ERROR, Gtk::BUTTONS_OK, true);
dlg.set_position(Gtk::WIN_POS_CENTER);
dlg.run();
}
if (!m_discord.IsStoreValid()) {
Gtk::MessageDialog dlg(*m_main_window, "The Discord cache could not be created!", false, Gtk::MESSAGE_ERROR, Gtk::BUTTONS_OK, true);
dlg.set_position(Gtk::WIN_POS_CENTER);
dlg.run();
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();
@@ -113,35 +129,19 @@ int Abaddon::StartGTK() {
ActionReloadCSS();
m_gtk_app->signal_shutdown().connect([&]() {
StopDiscord();
});
if (!m_settings.IsValid()) {
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();
}
if (!m_emojis.Load()) {
Gtk::MessageDialog dlg(*m_main_window, "The emoji file couldn't be loaded!", false, Gtk::MESSAGE_ERROR, Gtk::BUTTONS_OK, true);
dlg.set_position(Gtk::WIN_POS_CENTER);
dlg.run();
}
if (!m_discord.IsStoreValid()) {
Gtk::MessageDialog dlg(*m_main_window, "The Discord cache could not be created!", false, Gtk::MESSAGE_ERROR, Gtk::BUTTONS_OK, true);
dlg.set_position(Gtk::WIN_POS_CENTER);
dlg.run();
return 1;
}
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 = m_settings.GetDiscordToken();
std::string token = GetSettings().DiscordToken;
if (token.size()) {
m_discord_token = token;
m_discord.UpdateToken(m_discord_token);
@@ -154,6 +154,7 @@ void Abaddon::StartDiscord() {
void Abaddon::StopDiscord() {
m_discord.Stop();
SaveState();
}
bool Abaddon::IsDiscordActive() const {
@@ -176,6 +177,7 @@ const DiscordClient &Abaddon::GetDiscordClient() const {
void Abaddon::DiscordOnReady() {
m_main_window->UpdateComponents();
LoadState();
}
void Abaddon::DiscordOnMessageCreate(const Message &message) {
@@ -247,8 +249,8 @@ void Abaddon::DiscordOnThreadUpdate(const ThreadUpdateData &data) {
}
}
const SettingsManager &Abaddon::GetSettings() const {
return m_settings;
SettingsManager::Settings &Abaddon::GetSettings() {
return m_settings.GetSettings();
}
Glib::RefPtr<Gtk::CssProvider> Abaddon::GetStyleProvider() {
@@ -268,7 +270,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;
@@ -365,6 +367,44 @@ void Abaddon::SetupUserMenu() {
m_user_menu->show_all();
}
void Abaddon::SaveState() {
if (!GetSettings().SaveState) return;
AbaddonApplicationState state;
state.ActiveChannel = m_main_window->GetChatActiveChannel();
state.Expansion = m_main_window->GetChannelList()->GetExpansionState();
const auto path = GetStateCachePath();
if (!util::IsFolder(path)) {
std::error_code ec;
std::filesystem::create_directories(path, ec);
}
auto *fp = std::fopen(GetStateCachePath("/state.json").c_str(), "wb");
if (fp == nullptr) return;
const auto s = nlohmann::json(state).dump(4);
std::fwrite(s.c_str(), 1, s.size(), fp);
std::fclose(fp);
}
void Abaddon::LoadState() {
if (!GetSettings().SaveState) {
// call with empty data to purge the temporary table
m_main_window->GetChannelList()->UseExpansionState({});
return;
}
const auto data = ReadWholeFile(GetStateCachePath("/state.json"));
if (data.empty()) return;
try {
AbaddonApplicationState state = nlohmann::json::parse(data.begin(), data.end());
m_main_window->GetChannelList()->UseExpansionState(state.Expansion);
ActionChannelOpened(state.ActiveChannel);
} catch (const std::exception &e) {
printf("failed to load application state: %s\n", e.what());
}
}
void Abaddon::ManageHeapWindow(Gtk::Window *window) {
window->signal_hide().connect([this, window]() {
delete window;
@@ -422,6 +462,11 @@ std::string Abaddon::GetResPath() {
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;
}
@@ -430,6 +475,10 @@ 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();
@@ -447,7 +496,7 @@ void Abaddon::ActionSetToken() {
m_discord_token = dlg.GetToken();
m_discord.UpdateToken(m_discord_token);
m_main_window->UpdateComponents();
m_settings.SetSetting("discord", "token", m_discord_token);
GetSettings().DiscordToken = m_discord_token;
}
}
@@ -461,12 +510,15 @@ void Abaddon::ActionJoinGuildDialog() {
}
void Abaddon::ActionChannelOpened(Snowflake id) {
if (id == m_main_window->GetChatActiveChannel()) return;
if (!id.IsValid() || id == m_main_window->GetChatActiveChannel()) return;
m_main_window->GetChatWindow()->SetTopic("");
const auto channel = m_discord.GetChannel(id);
if (!channel.has_value()) return;
const bool can_access = channel->IsDM() || m_discord.HasChannelPermission(m_discord.GetUserData().ID, id, Permission::VIEW_CHANNEL);
if (channel->Type == ChannelType::GUILD_TEXT || channel->Type == ChannelType::GUILD_NEWS)
m_main_window->set_title(std::string(APP_TITLE) + " - #" + *channel->Name);
else {
@@ -482,23 +534,28 @@ 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_main_window->UpdateChatWindowContents();
m_channels_requested.insert(id);
});
// dont fire requests we know will fail
if (can_access) {
m_discord.FetchMessagesInChannel(id, [this, id](const std::vector<Message> &msgs) {
m_main_window->UpdateChatWindowContents();
m_channels_requested.insert(id);
});
}
} else {
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()) {
m_discord.SendLazyLoad(id);
if (can_access) {
if (channel->IsThread()) {
m_discord.SendThreadLazyLoad(id);
if (channel->ThreadMetadata->IsArchived)
m_main_window->GetChatWindow()->SetTopic("This thread is archived. Sending a message will unarchive it");
} else if (channel->Type != ChannelType::DM && channel->Type != ChannelType::GROUP_DM && channel->GuildID.has_value()) {
m_discord.SendLazyLoad(id);
if (m_discord.IsVerificationRequired(*channel->GuildID))
ShowGuildVerificationGateDialog(*channel->GuildID);
if (m_discord.IsVerificationRequired(*channel->GuildID))
ShowGuildVerificationGateDialog(*channel->GuildID);
}
}
}
@@ -510,16 +567,9 @@ void Abaddon::ActionChatLoadHistory(Snowflake id) {
return;
Snowflake before_id = m_main_window->GetChatOldestListedMessage();
auto knownset = m_discord.GetMessageIDsForChannel(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);
auto msgs = m_discord.GetMessagesBefore(id, before_id);
if (distance >= 50) {
std::vector<Message> msgs;
for (auto it = knownvec.begin() + distance - 50; it != knownvec.begin() + distance; it++)
msgs.push_back(*m_discord.GetMessage(*it));
if (msgs.size() >= 50) {
m_main_window->UpdateChatPrependHistory(msgs);
return;
}
@@ -661,7 +711,7 @@ bool Abaddon::ShowConfirm(const Glib::ustring &prompt, Gtk::Window *window) {
void Abaddon::ActionReloadCSS() {
try {
Gtk::StyleContext::remove_provider_for_screen(Gdk::Screen::get_default(), m_css_provider);
m_css_provider->load_from_path(GetCSSPath("/" + m_settings.GetMainCSS()));
m_css_provider->load_from_path(GetCSSPath("/" + GetSettings().MainCSS));
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);

View File

@@ -14,7 +14,6 @@
class Abaddon {
private:
Abaddon();
~Abaddon();
Abaddon(const Abaddon &) = delete;
Abaddon &operator=(const Abaddon &) = delete;
Abaddon(Abaddon &&) = delete;
@@ -24,6 +23,8 @@ public:
static Abaddon &Get();
int StartGTK();
void OnShutdown();
void StartDiscord();
void StopDiscord();
@@ -74,7 +75,7 @@ public:
void DiscordOnDisconnect(bool is_reconnecting, GatewayCloseCode close_code);
void DiscordOnThreadUpdate(const ThreadUpdateData &data);
const SettingsManager &GetSettings() const;
SettingsManager::Settings &GetSettings();
Glib::RefPtr<Gtk::CssProvider> GetStyleProvider();
@@ -84,13 +85,17 @@ public:
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();
Snowflake m_shown_user_menu_id;
Snowflake m_shown_user_menu_guild_id;

View File

@@ -61,7 +61,6 @@ void CellRendererPixbufAnimation::render_vfunc(const Cairo::RefPtr<Cairo::Contex
Gtk::CellRendererState flags) {
Gtk::Requisition minimum, natural;
get_preferred_size(widget, minimum, natural);
auto alloc = widget.get_allocation();
int xpad, ypad;
get_padding(xpad, ypad);
int pix_x = cell_area.get_x() + xpad;

View File

@@ -1,29 +1,32 @@
#include "abaddon.hpp"
#include "channels.hpp"
#include "imgmanager.hpp"
#include "statusindicator.hpp"
#include "util.hpp"
#include <algorithm>
#include <map>
#include <unordered_map>
#include "../abaddon.hpp"
#include "../imgmanager.hpp"
#include "../util.hpp"
#include "statusindicator.hpp"
ChannelList::ChannelList()
: Glib::ObjectBase(typeid(ChannelList))
, Gtk::ScrolledWindow()
, m_model(Gtk::TreeStore::create(m_columns))
, m_menu_guild_copy_id("_Copy ID", true)
, m_menu_guild_settings("View _Settings", true)
, m_menu_guild_leave("_Leave", true)
, m_menu_guild_mark_as_read("Mark as _Read", true)
, m_menu_category_copy_id("_Copy ID", true)
, m_menu_channel_copy_id("_Copy ID", true)
, m_menu_dm_close("") // changes depending on if group or not
, m_menu_channel_mark_as_read("Mark as _Read", true)
, m_menu_dm_copy_id("_Copy ID", true)
, m_menu_dm_close("") // changes depending on if group or not
, m_menu_thread_copy_id("_Copy ID", true)
, m_menu_thread_leave("_Leave", true)
, m_menu_thread_archive("_Archive", true)
, m_menu_thread_unarchive("_Unarchive", true) {
, m_menu_thread_unarchive("_Unarchive", true)
, m_menu_thread_mark_as_read("Mark as _Read", true) {
get_style_context()->add_class("channel-list");
// todo: move to method
const auto cb = [this](const Gtk::TreeModel::Path &path, Gtk::TreeViewColumn *column) {
auto row = *m_model->get_iter(path);
const auto type = row[m_columns.m_type];
@@ -40,7 +43,9 @@ ChannelList::ChannelList()
}
if (type == RenderType::TextChannel || type == RenderType::DM || type == RenderType::Thread) {
m_signal_action_channel_item_select.emit(static_cast<Snowflake>(row[m_columns.m_id]));
const auto id = static_cast<Snowflake>(row[m_columns.m_id]);
m_signal_action_channel_item_select.emit(id);
Abaddon::Get().GetDiscordClient().MarkChannelAsRead(id, [](...) {});
}
};
m_view.signal_row_activated().connect(cb, false);
@@ -77,6 +82,7 @@ ChannelList::ChannelList()
column->add_attribute(renderer->property_icon(), m_columns.m_icon);
column->add_attribute(renderer->property_icon_animation(), m_columns.m_icon_anim);
column->add_attribute(renderer->property_name(), m_columns.m_name);
column->add_attribute(renderer->property_id(), m_columns.m_id);
column->add_attribute(renderer->property_expanded(), m_columns.m_expanded);
column->add_attribute(renderer->property_nsfw(), m_columns.m_nsfw);
m_view.append_column(*column);
@@ -90,20 +96,55 @@ ChannelList::ChannelList()
m_menu_guild_leave.signal_activate().connect([this] {
m_signal_action_guild_leave.emit(static_cast<Snowflake>((*m_model->get_iter(m_path_for_menu))[m_columns.m_id]));
});
m_menu_guild.append(m_menu_guild_copy_id);
m_menu_guild_mark_as_read.signal_activate().connect([this] {
Abaddon::Get().GetDiscordClient().MarkGuildAsRead(static_cast<Snowflake>((*m_model->get_iter(m_path_for_menu))[m_columns.m_id]), [](...) {});
});
m_menu_guild_toggle_mute.signal_activate().connect([this] {
const auto id = static_cast<Snowflake>((*m_model->get_iter(m_path_for_menu))[m_columns.m_id]);
auto &discord = Abaddon::Get().GetDiscordClient();
if (discord.IsGuildMuted(id))
discord.UnmuteGuild(id, NOOP_CALLBACK);
else
discord.MuteGuild(id, NOOP_CALLBACK);
});
m_menu_guild.append(m_menu_guild_mark_as_read);
m_menu_guild.append(m_menu_guild_settings);
m_menu_guild.append(m_menu_guild_leave);
m_menu_guild.append(m_menu_guild_toggle_mute);
m_menu_guild.append(m_menu_guild_copy_id);
m_menu_guild.show_all();
m_menu_category_copy_id.signal_activate().connect([this] {
Gtk::Clipboard::get()->set_text(std::to_string((*m_model->get_iter(m_path_for_menu))[m_columns.m_id]));
});
m_menu_category_toggle_mute.signal_activate().connect([this] {
const auto id = static_cast<Snowflake>((*m_model->get_iter(m_path_for_menu))[m_columns.m_id]);
auto &discord = Abaddon::Get().GetDiscordClient();
if (discord.IsChannelMuted(id))
discord.UnmuteChannel(id, NOOP_CALLBACK);
else
discord.MuteChannel(id, NOOP_CALLBACK);
});
m_menu_category.append(m_menu_category_toggle_mute);
m_menu_category.append(m_menu_category_copy_id);
m_menu_category.show_all();
m_menu_channel_copy_id.signal_activate().connect([this] {
Gtk::Clipboard::get()->set_text(std::to_string((*m_model->get_iter(m_path_for_menu))[m_columns.m_id]));
});
m_menu_channel_mark_as_read.signal_activate().connect([this] {
Abaddon::Get().GetDiscordClient().MarkChannelAsRead(static_cast<Snowflake>((*m_model->get_iter(m_path_for_menu))[m_columns.m_id]), [](...) {});
});
m_menu_channel_toggle_mute.signal_activate().connect([this] {
const auto id = static_cast<Snowflake>((*m_model->get_iter(m_path_for_menu))[m_columns.m_id]);
auto &discord = Abaddon::Get().GetDiscordClient();
if (discord.IsChannelMuted(id))
discord.UnmuteChannel(id, NOOP_CALLBACK);
else
discord.MuteChannel(id, NOOP_CALLBACK);
});
m_menu_channel.append(m_menu_channel_mark_as_read);
m_menu_channel.append(m_menu_channel_toggle_mute);
m_menu_channel.append(m_menu_channel_copy_id);
m_menu_channel.show_all();
@@ -121,8 +162,17 @@ ChannelList::ChannelList()
else if (Abaddon::Get().ShowConfirm("Are you sure you want to leave this group DM?"))
Abaddon::Get().GetDiscordClient().CloseDM(id);
});
m_menu_dm.append(m_menu_dm_copy_id);
m_menu_dm_toggle_mute.signal_activate().connect([this] {
const auto id = static_cast<Snowflake>((*m_model->get_iter(m_path_for_menu))[m_columns.m_id]);
auto &discord = Abaddon::Get().GetDiscordClient();
if (discord.IsChannelMuted(id))
discord.UnmuteChannel(id, NOOP_CALLBACK);
else
discord.MuteChannel(id, NOOP_CALLBACK);
});
m_menu_dm.append(m_menu_dm_toggle_mute);
m_menu_dm.append(m_menu_dm_close);
m_menu_dm.append(m_menu_dm_copy_id);
m_menu_dm.show_all();
m_menu_thread_copy_id.signal_activate().connect([this] {
@@ -138,12 +188,29 @@ ChannelList::ChannelList()
m_menu_thread_unarchive.signal_activate().connect([this] {
Abaddon::Get().GetDiscordClient().UnArchiveThread(static_cast<Snowflake>((*m_model->get_iter(m_path_for_menu))[m_columns.m_id]), [](...) {});
});
m_menu_thread.append(m_menu_thread_copy_id);
m_menu_thread_mark_as_read.signal_activate().connect([this] {
Abaddon::Get().GetDiscordClient().MarkChannelAsRead(static_cast<Snowflake>((*m_model->get_iter(m_path_for_menu))[m_columns.m_id]), NOOP_CALLBACK);
});
m_menu_thread_toggle_mute.signal_activate().connect([this] {
const auto id = static_cast<Snowflake>((*m_model->get_iter(m_path_for_menu))[m_columns.m_id]);
auto &discord = Abaddon::Get().GetDiscordClient();
if (discord.IsChannelMuted(id))
discord.UnmuteThread(id, NOOP_CALLBACK);
else
discord.MuteThread(id, NOOP_CALLBACK);
});
m_menu_thread.append(m_menu_thread_mark_as_read);
m_menu_thread.append(m_menu_thread_toggle_mute);
m_menu_thread.append(m_menu_thread_leave);
m_menu_thread.append(m_menu_thread_archive);
m_menu_thread.append(m_menu_thread_unarchive);
m_menu_thread.append(m_menu_thread_copy_id);
m_menu_thread.show_all();
m_menu_guild.signal_popped_up().connect(sigc::mem_fun(*this, &ChannelList::OnGuildSubmenuPopup));
m_menu_category.signal_popped_up().connect(sigc::mem_fun(*this, &ChannelList::OnCategorySubmenuPopup));
m_menu_channel.signal_popped_up().connect(sigc::mem_fun(*this, &ChannelList::OnChannelSubmenuPopup));
m_menu_dm.signal_popped_up().connect(sigc::mem_fun(*this, &ChannelList::OnDMSubmenuPopup));
m_menu_thread.signal_popped_up().connect(sigc::mem_fun(*this, &ChannelList::OnThreadSubmenuPopup));
auto &discord = Abaddon::Get().GetDiscordClient();
@@ -159,6 +226,19 @@ ChannelList::ChannelList()
discord.signal_added_to_thread().connect(sigc::mem_fun(*this, &ChannelList::OnThreadJoined));
discord.signal_removed_from_thread().connect(sigc::mem_fun(*this, &ChannelList::OnThreadRemoved));
discord.signal_guild_update().connect(sigc::mem_fun(*this, &ChannelList::UpdateGuild));
discord.signal_message_ack().connect(sigc::mem_fun(*this, &ChannelList::OnMessageAck));
discord.signal_channel_muted().connect(sigc::mem_fun(*this, &ChannelList::OnChannelMute));
discord.signal_channel_unmuted().connect(sigc::mem_fun(*this, &ChannelList::OnChannelUnmute));
discord.signal_guild_muted().connect(sigc::mem_fun(*this, &ChannelList::OnGuildMute));
discord.signal_guild_unmuted().connect(sigc::mem_fun(*this, &ChannelList::OnGuildUnmute));
}
void ChannelList::UsePanedHack(Gtk::Paned &paned) {
paned.property_position().signal_changed().connect(sigc::mem_fun(*this, &ChannelList::OnPanedPositionChanged));
}
void ChannelList::OnPanedPositionChanged() {
m_view.queue_draw();
}
void ChannelList::UpdateListing() {
@@ -167,7 +247,6 @@ void ChannelList::UpdateListing() {
m_model->clear();
auto &discord = Abaddon::Get().GetDiscordClient();
auto &img = Abaddon::Get().GetImageManager();
const auto guild_ids = discord.GetUserSortedGuilds();
int sortnum = 0;
@@ -185,10 +264,7 @@ void ChannelList::UpdateListing() {
}
void ChannelList::UpdateNewGuild(const GuildData &guild) {
auto &img = Abaddon::Get().GetImageManager();
auto iter = AddGuild(guild);
AddGuild(guild);
// update sort order
int sortnum = 0;
for (const auto guild_id : Abaddon::Get().GetDiscordClient().GetUserSortedGuilds()) {
@@ -235,7 +311,6 @@ void ChannelList::UpdateChannel(Snowflake id) {
}
void ChannelList::UpdateCreateChannel(const ChannelData &channel) {
;
if (channel.Type == ChannelType::GUILD_CATEGORY) return (void)UpdateCreateChannelCategory(channel);
if (channel.Type == ChannelType::DM || channel.Type == ChannelType::GROUP_DM) return UpdateCreateDMChannel(channel);
if (channel.Type != ChannelType::GUILD_TEXT && channel.Type != ChannelType::GUILD_NEWS) return;
@@ -267,11 +342,9 @@ void ChannelList::UpdateGuild(Snowflake id) {
const auto guild = Abaddon::Get().GetDiscordClient().GetGuild(id);
if (!iter || !guild.has_value()) return;
static const bool show_animations = Abaddon::Get().GetSettings().GetShowAnimations();
(*iter)[m_columns.m_name] = "<b>" + Glib::Markup::escape_text(guild->Name) + "</b>";
(*iter)[m_columns.m_icon] = img.GetPlaceholder(GuildIconSize);
if (show_animations && guild->HasAnimatedIcon()) {
if (Abaddon::Get().GetSettings().ShowAnimations && guild->HasAnimatedIcon()) {
const auto cb = [this, id](const Glib::RefPtr<Gdk::PixbufAnimation> &pb) {
auto iter = GetIteratorForGuildFromID(id);
if (iter) (*iter)[m_columns.m_icon_anim] = pb;
@@ -353,9 +426,35 @@ void ChannelList::DeleteThreadRow(Snowflake id) {
m_model->erase(iter);
}
void ChannelList::OnChannelMute(Snowflake id) {
if (auto iter = GetIteratorForChannelFromID(id))
m_model->row_changed(m_model->get_path(iter), iter);
}
void ChannelList::OnChannelUnmute(Snowflake id) {
if (auto iter = GetIteratorForChannelFromID(id))
m_model->row_changed(m_model->get_path(iter), iter);
}
void ChannelList::OnGuildMute(Snowflake id) {
if (auto iter = GetIteratorForGuildFromID(id))
m_model->row_changed(m_model->get_path(iter), iter);
}
void ChannelList::OnGuildUnmute(Snowflake id) {
if (auto iter = GetIteratorForGuildFromID(id))
m_model->row_changed(m_model->get_path(iter), iter);
}
// create a temporary channel row for non-joined threads
// and delete them when the active channel switches off of them if still not joined
void ChannelList::SetActiveChannel(Snowflake id) {
// mark channel as read when switching off
if (m_active_channel.IsValid())
Abaddon::Get().GetDiscordClient().MarkChannelAsRead(m_active_channel, [](...) {});
m_active_channel = id;
if (m_temporary_thread_row) {
const auto thread_id = static_cast<Snowflake>((*m_temporary_thread_row)[m_columns.m_id]);
const auto thread = Abaddon::Get().GetDiscordClient().GetChannel(thread_id);
@@ -380,6 +479,58 @@ void ChannelList::SetActiveChannel(Snowflake id) {
}
}
void ChannelList::UseExpansionState(const ExpansionStateRoot &root) {
auto recurse = [this](auto &self, const ExpansionStateRoot &root) -> void {
// and these are only channels
for (const auto &[id, state] : root.Children) {
if (const auto iter = m_tmp_channel_map.find(id); iter != m_tmp_channel_map.end()) {
if (state.IsExpanded)
m_view.expand_row(m_model->get_path(iter->second), false);
else
m_view.collapse_row(m_model->get_path(iter->second));
}
self(self, state.Children);
}
};
// top level is guild
for (const auto &[id, state] : root.Children) {
if (const auto iter = GetIteratorForGuildFromID(id)) {
if (state.IsExpanded)
m_view.expand_row(m_model->get_path(iter), false);
else
m_view.collapse_row(m_model->get_path(iter));
}
recurse(recurse, state.Children);
}
m_tmp_channel_map.clear();
}
ExpansionStateRoot ChannelList::GetExpansionState() const {
ExpansionStateRoot r;
auto recurse = [this](auto &self, const Gtk::TreeRow &row) -> ExpansionState {
ExpansionState r;
r.IsExpanded = row[m_columns.m_expanded];
for (const auto &child : row.children())
r.Children.Children[static_cast<Snowflake>(child[m_columns.m_id])] = self(self, child);
return r;
};
for (const auto &child : m_model->children()) {
const auto id = static_cast<Snowflake>(child[m_columns.m_id]);
if (static_cast<uint64_t>(id) == 0ULL) continue; // dont save DM header
r.Children[id] = recurse(recurse, child);
}
return r;
}
Gtk::TreeModel::iterator ChannelList::AddGuild(const GuildData &guild) {
auto &discord = Abaddon::Get().GetDiscordClient();
auto &img = Abaddon::Get().GetImageManager();
@@ -390,9 +541,7 @@ Gtk::TreeModel::iterator ChannelList::AddGuild(const GuildData &guild) {
guild_row[m_columns.m_name] = "<b>" + Glib::Markup::escape_text(guild.Name) + "</b>";
guild_row[m_columns.m_icon] = img.GetPlaceholder(GuildIconSize);
static const bool show_animations = Abaddon::Get().GetSettings().GetShowAnimations();
if (show_animations && guild.HasAnimatedIcon()) {
if (Abaddon::Get().GetSettings().ShowAnimations && guild.HasAnimatedIcon()) {
const auto cb = [this, id = guild.ID](const Glib::RefPtr<Gdk::PixbufAnimation> &pb) {
auto iter = GetIteratorForGuildFromID(id);
if (iter) (*iter)[m_columns.m_icon_anim] = pb;
@@ -438,7 +587,7 @@ Gtk::TreeModel::iterator ChannelList::AddGuild(const GuildData &guild) {
if (it == threads.end()) return;
for (const auto &thread : it->second)
CreateThreadRow(row.children(), thread);
m_tmp_channel_map[thread.ID] = CreateThreadRow(row.children(), thread);
};
for (const auto &channel : orphan_channels) {
@@ -449,6 +598,7 @@ Gtk::TreeModel::iterator ChannelList::AddGuild(const GuildData &guild) {
channel_row[m_columns.m_sort] = *channel.Position + OrphanChannelSortOffset;
channel_row[m_columns.m_nsfw] = channel.NSFW();
add_threads(channel, channel_row);
m_tmp_channel_map[channel.ID] = channel_row;
}
for (const auto &[category_id, channels] : categories) {
@@ -460,6 +610,7 @@ Gtk::TreeModel::iterator ChannelList::AddGuild(const GuildData &guild) {
cat_row[m_columns.m_name] = Glib::Markup::escape_text(*category->Name);
cat_row[m_columns.m_sort] = *category->Position;
cat_row[m_columns.m_expanded] = true;
m_tmp_channel_map[category_id] = cat_row;
// m_view.expand_row wont work because it might not have channels
for (const auto &channel : channels) {
@@ -470,6 +621,7 @@ Gtk::TreeModel::iterator ChannelList::AddGuild(const GuildData &guild) {
channel_row[m_columns.m_sort] = *channel.Position;
channel_row[m_columns.m_nsfw] = channel.NSFW();
add_threads(channel, channel_row);
m_tmp_channel_map[channel.ID] = channel_row;
}
}
@@ -511,7 +663,7 @@ void ChannelList::UpdateChannelCategory(const ChannelData &channel) {
}
Gtk::TreeModel::iterator ChannelList::GetIteratorForGuildFromID(Snowflake id) {
for (const auto child : m_model->children()) {
for (const auto &child : m_model->children()) {
if (child[m_columns.m_id] == id)
return child;
}
@@ -520,14 +672,14 @@ Gtk::TreeModel::iterator ChannelList::GetIteratorForGuildFromID(Snowflake id) {
Gtk::TreeModel::iterator ChannelList::GetIteratorForChannelFromID(Snowflake id) {
std::queue<Gtk::TreeModel::iterator> queue;
for (const auto child : m_model->children())
for (const auto child2 : child.children())
for (const auto &child : m_model->children())
for (const auto &child2 : child.children())
queue.push(child2);
while (!queue.empty()) {
auto item = queue.front();
if ((*item)[m_columns.m_id] == id) return item;
for (const auto child : item->children())
for (const auto &child : item->children())
queue.push(child);
queue.pop();
}
@@ -616,7 +768,7 @@ void ChannelList::UpdateCreateDMChannel(const ChannelData &dm) {
std::optional<UserData> top_recipient;
const auto recipients = dm.GetDMRecipients();
if (recipients.size() > 0)
if (!recipients.empty())
top_recipient = recipients[0];
auto iter = m_model->append(header_row->children());
@@ -640,13 +792,30 @@ void ChannelList::UpdateCreateDMChannel(const ChannelData &dm) {
}
}
void ChannelList::OnMessageAck(const MessageAckData &data) {
// trick renderer into redrawing
m_model->row_changed(Gtk::TreeModel::Path("0"), m_model->get_iter("0")); // 0 is always path for dm header
auto iter = GetIteratorForChannelFromID(data.ChannelID);
if (iter) m_model->row_changed(m_model->get_path(iter), iter);
auto channel = Abaddon::Get().GetDiscordClient().GetChannel(data.ChannelID);
if (channel.has_value() && channel->GuildID.has_value()) {
iter = GetIteratorForGuildFromID(*channel->GuildID);
if (iter) m_model->row_changed(m_model->get_path(iter), iter);
}
}
void ChannelList::OnMessageCreate(const Message &msg) {
auto iter = GetIteratorForChannelFromID(msg.ChannelID);
if (iter) m_model->row_changed(m_model->get_path(iter), iter); // redraw
const auto channel = Abaddon::Get().GetDiscordClient().GetChannel(msg.ChannelID);
if (!channel.has_value()) return;
if (channel->Type != ChannelType::DM && channel->Type != ChannelType::GROUP_DM) return;
auto iter = GetIteratorForChannelFromID(msg.ChannelID);
if (iter)
(*iter)[m_columns.m_sort] = -msg.ID;
if (channel->Type == ChannelType::DM || channel->Type == ChannelType::GROUP_DM) {
if (iter)
(*iter)[m_columns.m_sort] = -msg.ID;
}
if (channel->GuildID.has_value())
if ((iter = GetIteratorForGuildFromID(*channel->GuildID)))
m_model->row_changed(m_model->get_path(iter), iter);
}
bool ChannelList::OnButtonPressEvent(GdkEventButton *ev) {
@@ -712,6 +881,46 @@ void ChannelList::MoveRow(const Gtk::TreeModel::iterator &iter, const Gtk::TreeM
m_model->erase(iter);
}
void ChannelList::OnGuildSubmenuPopup(const Gdk::Rectangle *flipped_rect, const Gdk::Rectangle *final_rect, bool flipped_x, bool flipped_y) {
const auto iter = m_model->get_iter(m_path_for_menu);
if (!iter) return;
const auto id = static_cast<Snowflake>((*iter)[m_columns.m_id]);
if (Abaddon::Get().GetDiscordClient().IsGuildMuted(id))
m_menu_guild_toggle_mute.set_label("Unmute");
else
m_menu_guild_toggle_mute.set_label("Mute");
}
void ChannelList::OnCategorySubmenuPopup(const Gdk::Rectangle *flipped_rect, const Gdk::Rectangle *final_rect, bool flipped_x, bool flipped_y) {
const auto iter = m_model->get_iter(m_path_for_menu);
if (!iter) return;
const auto id = static_cast<Snowflake>((*iter)[m_columns.m_id]);
if (Abaddon::Get().GetDiscordClient().IsChannelMuted(id))
m_menu_category_toggle_mute.set_label("Unmute");
else
m_menu_category_toggle_mute.set_label("Mute");
}
void ChannelList::OnChannelSubmenuPopup(const Gdk::Rectangle *flipped_rect, const Gdk::Rectangle *final_rect, bool flipped_x, bool flipped_y) {
const auto iter = m_model->get_iter(m_path_for_menu);
if (!iter) return;
const auto id = static_cast<Snowflake>((*iter)[m_columns.m_id]);
if (Abaddon::Get().GetDiscordClient().IsChannelMuted(id))
m_menu_channel_toggle_mute.set_label("Unmute");
else
m_menu_channel_toggle_mute.set_label("Mute");
}
void ChannelList::OnDMSubmenuPopup(const Gdk::Rectangle *flipped_rect, const Gdk::Rectangle *final_rect, bool flipped_x, bool flipped_y) {
auto iter = m_model->get_iter(m_path_for_menu);
if (!iter) return;
const auto id = static_cast<Snowflake>((*iter)[m_columns.m_id]);
if (Abaddon::Get().GetDiscordClient().IsChannelMuted(id))
m_menu_dm_toggle_mute.set_label("Unmute");
else
m_menu_dm_toggle_mute.set_label("Mute");
}
void ChannelList::OnThreadSubmenuPopup(const Gdk::Rectangle *flipped_rect, const Gdk::Rectangle *final_rect, bool flipped_x, bool flipped_y) {
m_menu_thread_archive.set_visible(false);
m_menu_thread_unarchive.set_visible(false);
@@ -719,7 +928,14 @@ void ChannelList::OnThreadSubmenuPopup(const Gdk::Rectangle *flipped_rect, const
auto &discord = Abaddon::Get().GetDiscordClient();
auto iter = m_model->get_iter(m_path_for_menu);
if (!iter) return;
auto channel = discord.GetChannel(static_cast<Snowflake>((*iter)[m_columns.m_id]));
const auto id = static_cast<Snowflake>((*iter)[m_columns.m_id]);
if (discord.IsChannelMuted(id))
m_menu_thread_toggle_mute.set_label("Unmute");
else
m_menu_thread_toggle_mute.set_label("Mute");
auto channel = discord.GetChannel(id);
if (!channel.has_value() || !channel->ThreadMetadata.has_value()) return;
if (!discord.HasGuildPermission(discord.GetUserData().ID, *channel->GuildID, Permission::MANAGE_THREADS)) return;
@@ -749,457 +965,3 @@ ChannelList::ModelColumns::ModelColumns() {
add(m_nsfw);
add(m_expanded);
}
CellRendererChannels::CellRendererChannels()
: Glib::ObjectBase(typeid(CellRendererChannels))
, Gtk::CellRenderer()
, m_property_type(*this, "render-type")
, m_property_name(*this, "name")
, m_property_pixbuf(*this, "pixbuf")
, m_property_pixbuf_animation(*this, "pixbuf-animation")
, m_property_expanded(*this, "expanded")
, m_property_nsfw(*this, "nsfw") {
property_mode() = Gtk::CELL_RENDERER_MODE_ACTIVATABLE;
property_xpad() = 2;
property_ypad() = 2;
m_property_name.get_proxy().signal_changed().connect([this] {
m_renderer_text.property_markup() = m_property_name;
});
}
CellRendererChannels::~CellRendererChannels() {
}
Glib::PropertyProxy<RenderType> CellRendererChannels::property_type() {
return m_property_type.get_proxy();
}
Glib::PropertyProxy<Glib::ustring> CellRendererChannels::property_name() {
return m_property_name.get_proxy();
}
Glib::PropertyProxy<Glib::RefPtr<Gdk::Pixbuf>> CellRendererChannels::property_icon() {
return m_property_pixbuf.get_proxy();
}
Glib::PropertyProxy<Glib::RefPtr<Gdk::PixbufAnimation>> CellRendererChannels::property_icon_animation() {
return m_property_pixbuf_animation.get_proxy();
}
Glib::PropertyProxy<bool> CellRendererChannels::property_expanded() {
return m_property_expanded.get_proxy();
}
Glib::PropertyProxy<bool> CellRendererChannels::property_nsfw() {
return m_property_nsfw.get_proxy();
}
void CellRendererChannels::get_preferred_width_vfunc(Gtk::Widget &widget, int &minimum_width, int &natural_width) const {
switch (m_property_type.get_value()) {
case RenderType::Guild:
return get_preferred_width_vfunc_guild(widget, minimum_width, natural_width);
case RenderType::Category:
return get_preferred_width_vfunc_category(widget, minimum_width, natural_width);
case RenderType::TextChannel:
return get_preferred_width_vfunc_channel(widget, minimum_width, natural_width);
case RenderType::Thread:
return get_preferred_width_vfunc_thread(widget, minimum_width, natural_width);
case RenderType::DMHeader:
return get_preferred_width_vfunc_dmheader(widget, minimum_width, natural_width);
case RenderType::DM:
return get_preferred_width_vfunc_dm(widget, minimum_width, natural_width);
}
}
void CellRendererChannels::get_preferred_width_for_height_vfunc(Gtk::Widget &widget, int height, int &minimum_width, int &natural_width) const {
switch (m_property_type.get_value()) {
case RenderType::Guild:
return get_preferred_width_for_height_vfunc_guild(widget, height, minimum_width, natural_width);
case RenderType::Category:
return get_preferred_width_for_height_vfunc_category(widget, height, minimum_width, natural_width);
case RenderType::TextChannel:
return get_preferred_width_for_height_vfunc_channel(widget, height, minimum_width, natural_width);
case RenderType::Thread:
return get_preferred_width_for_height_vfunc_thread(widget, height, minimum_width, natural_width);
case RenderType::DMHeader:
return get_preferred_width_for_height_vfunc_dmheader(widget, height, minimum_width, natural_width);
case RenderType::DM:
return get_preferred_width_for_height_vfunc_dm(widget, height, minimum_width, natural_width);
}
}
void CellRendererChannels::get_preferred_height_vfunc(Gtk::Widget &widget, int &minimum_height, int &natural_height) const {
switch (m_property_type.get_value()) {
case RenderType::Guild:
return get_preferred_height_vfunc_guild(widget, minimum_height, natural_height);
case RenderType::Category:
return get_preferred_height_vfunc_category(widget, minimum_height, natural_height);
case RenderType::TextChannel:
return get_preferred_height_vfunc_channel(widget, minimum_height, natural_height);
case RenderType::Thread:
return get_preferred_height_vfunc_thread(widget, minimum_height, natural_height);
case RenderType::DMHeader:
return get_preferred_height_vfunc_dmheader(widget, minimum_height, natural_height);
case RenderType::DM:
return get_preferred_height_vfunc_dm(widget, minimum_height, natural_height);
}
}
void CellRendererChannels::get_preferred_height_for_width_vfunc(Gtk::Widget &widget, int width, int &minimum_height, int &natural_height) const {
switch (m_property_type.get_value()) {
case RenderType::Guild:
return get_preferred_height_for_width_vfunc_guild(widget, width, minimum_height, natural_height);
case RenderType::Category:
return get_preferred_height_for_width_vfunc_category(widget, width, minimum_height, natural_height);
case RenderType::TextChannel:
return get_preferred_height_for_width_vfunc_channel(widget, width, minimum_height, natural_height);
case RenderType::Thread:
return get_preferred_height_for_width_vfunc_thread(widget, width, minimum_height, natural_height);
case RenderType::DMHeader:
return get_preferred_height_for_width_vfunc_dmheader(widget, width, minimum_height, natural_height);
case RenderType::DM:
return get_preferred_height_for_width_vfunc_dm(widget, width, minimum_height, natural_height);
}
}
void CellRendererChannels::render_vfunc(const Cairo::RefPtr<Cairo::Context> &cr, Gtk::Widget &widget, const Gdk::Rectangle &background_area, const Gdk::Rectangle &cell_area, Gtk::CellRendererState flags) {
switch (m_property_type.get_value()) {
case RenderType::Guild:
return render_vfunc_guild(cr, widget, background_area, cell_area, flags);
case RenderType::Category:
return render_vfunc_category(cr, widget, background_area, cell_area, flags);
case RenderType::TextChannel:
return render_vfunc_channel(cr, widget, background_area, cell_area, flags);
case RenderType::Thread:
return render_vfunc_thread(cr, widget, background_area, cell_area, flags);
case RenderType::DMHeader:
return render_vfunc_dmheader(cr, widget, background_area, cell_area, flags);
case RenderType::DM:
return render_vfunc_dm(cr, widget, background_area, cell_area, flags);
}
}
// guild functions
void CellRendererChannels::get_preferred_width_vfunc_guild(Gtk::Widget &widget, int &minimum_width, int &natural_width) const {
int pixbuf_width = 0;
if (auto pixbuf = m_property_pixbuf_animation.get_value())
pixbuf_width = pixbuf->get_width();
else if (auto pixbuf = m_property_pixbuf.get_value())
pixbuf_width = pixbuf->get_width();
int text_min, text_nat;
m_renderer_text.get_preferred_width(widget, text_min, text_nat);
int xpad, ypad;
get_padding(xpad, ypad);
minimum_width = std::max(text_min, pixbuf_width) + xpad * 2;
natural_width = std::max(text_nat, pixbuf_width) + xpad * 2;
}
void CellRendererChannels::get_preferred_width_for_height_vfunc_guild(Gtk::Widget &widget, int height, int &minimum_width, int &natural_width) const {
get_preferred_width_vfunc_guild(widget, minimum_width, natural_width);
}
void CellRendererChannels::get_preferred_height_vfunc_guild(Gtk::Widget &widget, int &minimum_height, int &natural_height) const {
int pixbuf_height = 0;
if (auto pixbuf = m_property_pixbuf_animation.get_value())
pixbuf_height = pixbuf->get_height();
else if (auto pixbuf = m_property_pixbuf.get_value())
pixbuf_height = pixbuf->get_height();
int text_min, text_nat;
m_renderer_text.get_preferred_height(widget, text_min, text_nat);
int xpad, ypad;
get_padding(xpad, ypad);
minimum_height = std::max(text_min, pixbuf_height) + ypad * 2;
natural_height = std::max(text_nat, pixbuf_height) + ypad * 2;
}
void CellRendererChannels::get_preferred_height_for_width_vfunc_guild(Gtk::Widget &widget, int width, int &minimum_height, int &natural_height) const {
get_preferred_height_vfunc_guild(widget, minimum_height, natural_height);
}
void CellRendererChannels::render_vfunc_guild(const Cairo::RefPtr<Cairo::Context> &cr, Gtk::Widget &widget, const Gdk::Rectangle &background_area, const Gdk::Rectangle &cell_area, Gtk::CellRendererState flags) {
Gtk::Requisition text_minimum, text_natural;
m_renderer_text.get_preferred_size(widget, text_minimum, text_natural);
Gtk::Requisition minimum, natural;
get_preferred_size(widget, minimum, natural);
int pixbuf_w, pixbuf_h = 0;
if (auto pixbuf = m_property_pixbuf_animation.get_value()) {
pixbuf_w = pixbuf->get_width();
pixbuf_h = pixbuf->get_height();
} else if (auto pixbuf = m_property_pixbuf.get_value()) {
pixbuf_w = pixbuf->get_width();
pixbuf_h = pixbuf->get_height();
}
const double icon_w = pixbuf_w;
const double icon_h = pixbuf_h;
const double icon_x = background_area.get_x();
const double icon_y = background_area.get_y() + background_area.get_height() / 2.0 - icon_h / 2.0;
const double text_x = icon_x + icon_w + 5.0;
const double text_y = background_area.get_y() + background_area.get_height() / 2.0 - text_natural.height / 2.0;
const double text_w = text_natural.width;
const double text_h = text_natural.height;
Gdk::Rectangle text_cell_area(text_x, text_y, text_w, text_h);
m_renderer_text.render(cr, widget, background_area, text_cell_area, flags);
const static bool hover_only = Abaddon::Get().GetSettings().GetAnimatedGuildHoverOnly();
const bool is_hovered = flags & Gtk::CELL_RENDERER_PRELIT;
auto anim = m_property_pixbuf_animation.get_value();
// kinda gross
if (anim) {
auto map_iter = m_pixbuf_anim_iters.find(anim);
if (map_iter == m_pixbuf_anim_iters.end())
m_pixbuf_anim_iters[anim] = anim->get_iter(nullptr);
auto pb_iter = m_pixbuf_anim_iters.at(anim);
const auto cb = [this, &widget, anim, icon_x, icon_y, icon_w, icon_h] {
if (m_pixbuf_anim_iters.at(anim)->advance())
widget.queue_draw_area(icon_x, icon_y, icon_w, icon_h);
};
if ((hover_only && is_hovered) || !hover_only)
Glib::signal_timeout().connect_once(sigc::track_obj(cb, widget), pb_iter->get_delay_time());
if (hover_only && !is_hovered)
m_pixbuf_anim_iters[anim] = anim->get_iter(nullptr);
Gdk::Cairo::set_source_pixbuf(cr, pb_iter->get_pixbuf(), icon_x, icon_y);
cr->rectangle(icon_x, icon_y, icon_w, icon_h);
cr->fill();
} else if (auto pixbuf = m_property_pixbuf.get_value()) {
Gdk::Cairo::set_source_pixbuf(cr, pixbuf, icon_x, icon_y);
cr->rectangle(icon_x, icon_y, icon_w, icon_h);
cr->fill();
}
}
// category
void CellRendererChannels::get_preferred_width_vfunc_category(Gtk::Widget &widget, int &minimum_width, int &natural_width) const {
m_renderer_text.get_preferred_width(widget, minimum_width, natural_width);
}
void CellRendererChannels::get_preferred_width_for_height_vfunc_category(Gtk::Widget &widget, int height, int &minimum_width, int &natural_width) const {
m_renderer_text.get_preferred_width_for_height(widget, height, minimum_width, natural_width);
}
void CellRendererChannels::get_preferred_height_vfunc_category(Gtk::Widget &widget, int &minimum_height, int &natural_height) const {
m_renderer_text.get_preferred_height(widget, minimum_height, natural_height);
}
void CellRendererChannels::get_preferred_height_for_width_vfunc_category(Gtk::Widget &widget, int width, int &minimum_height, int &natural_height) const {
m_renderer_text.get_preferred_height_for_width(widget, width, minimum_height, natural_height);
}
void CellRendererChannels::render_vfunc_category(const Cairo::RefPtr<Cairo::Context> &cr, Gtk::Widget &widget, const Gdk::Rectangle &background_area, const Gdk::Rectangle &cell_area, Gtk::CellRendererState flags) {
int available_xpad = background_area.get_width();
int available_ypad = background_area.get_height();
// todo: figure out how Gtk::Arrow is rendered because i like it better :^)
constexpr static int len = 5;
int x1, y1, x2, y2, x3, y3;
if (property_expanded()) {
x1 = background_area.get_x() + 7;
y1 = background_area.get_y() + background_area.get_height() / 2 - len;
x2 = background_area.get_x() + 7 + len;
y2 = background_area.get_y() + background_area.get_height() / 2 + len;
x3 = background_area.get_x() + 7 + len * 2;
y3 = background_area.get_y() + background_area.get_height() / 2 - len;
} else {
x1 = background_area.get_x() + 7;
y1 = background_area.get_y() + background_area.get_height() / 2 - len;
x2 = background_area.get_x() + 7 + len * 2;
y2 = background_area.get_y() + background_area.get_height() / 2;
x3 = background_area.get_x() + 7;
y3 = background_area.get_y() + background_area.get_height() / 2 + len;
}
cr->move_to(x1, y1);
cr->line_to(x2, y2);
cr->line_to(x3, y3);
static const auto expander_color = Gdk::RGBA(Abaddon::Get().GetSettings().GetChannelsExpanderColor());
cr->set_source_rgb(expander_color.get_red(), expander_color.get_green(), expander_color.get_blue());
cr->stroke();
Gtk::Requisition text_minimum, text_natural;
m_renderer_text.get_preferred_size(widget, text_minimum, text_natural);
const int text_x = background_area.get_x() + 22;
const int text_y = background_area.get_y() + background_area.get_height() / 2 - text_natural.height / 2;
const int text_w = text_natural.width;
const int text_h = text_natural.height;
Gdk::Rectangle text_cell_area(text_x, text_y, text_w, text_h);
m_renderer_text.render(cr, widget, background_area, text_cell_area, flags);
}
// text channel
void CellRendererChannels::get_preferred_width_vfunc_channel(Gtk::Widget &widget, int &minimum_width, int &natural_width) const {
m_renderer_text.get_preferred_width(widget, minimum_width, natural_width);
}
void CellRendererChannels::get_preferred_width_for_height_vfunc_channel(Gtk::Widget &widget, int height, int &minimum_width, int &natural_width) const {
m_renderer_text.get_preferred_width_for_height(widget, height, minimum_width, natural_width);
}
void CellRendererChannels::get_preferred_height_vfunc_channel(Gtk::Widget &widget, int &minimum_height, int &natural_height) const {
m_renderer_text.get_preferred_height(widget, minimum_height, natural_height);
}
void CellRendererChannels::get_preferred_height_for_width_vfunc_channel(Gtk::Widget &widget, int width, int &minimum_height, int &natural_height) const {
m_renderer_text.get_preferred_height_for_width(widget, width, minimum_height, natural_height);
}
void CellRendererChannels::render_vfunc_channel(const Cairo::RefPtr<Cairo::Context> &cr, Gtk::Widget &widget, const Gdk::Rectangle &background_area, const Gdk::Rectangle &cell_area, Gtk::CellRendererState flags) {
Gtk::Requisition minimum_size, natural_size;
m_renderer_text.get_preferred_size(widget, minimum_size, natural_size);
const int text_x = background_area.get_x() + 21;
const int text_y = background_area.get_y() + background_area.get_height() / 2 - natural_size.height / 2;
const int text_w = natural_size.width;
const int text_h = natural_size.height;
Gdk::Rectangle text_cell_area(text_x, text_y, text_w, text_h);
const static auto nsfw_color = Gdk::RGBA(Abaddon::Get().GetSettings().GetNSFWChannelColor());
if (m_property_nsfw.get_value())
m_renderer_text.property_foreground_rgba() = nsfw_color;
m_renderer_text.render(cr, widget, background_area, text_cell_area, flags);
// setting property_foreground_rgba() sets this to true which makes non-nsfw cells use the property too which is bad
// so unset it
m_renderer_text.property_foreground_set() = false;
}
// thread
void CellRendererChannels::get_preferred_width_vfunc_thread(Gtk::Widget &widget, int &minimum_width, int &natural_width) const {
m_renderer_text.get_preferred_width(widget, minimum_width, natural_width);
}
void CellRendererChannels::get_preferred_width_for_height_vfunc_thread(Gtk::Widget &widget, int height, int &minimum_width, int &natural_width) const {
get_preferred_width_vfunc_thread(widget, minimum_width, natural_width);
}
void CellRendererChannels::get_preferred_height_vfunc_thread(Gtk::Widget &widget, int &minimum_height, int &natural_height) const {
m_renderer_text.get_preferred_height(widget, minimum_height, natural_height);
}
void CellRendererChannels::get_preferred_height_for_width_vfunc_thread(Gtk::Widget &widget, int width, int &minimum_height, int &natural_height) const {
get_preferred_height_vfunc_thread(widget, minimum_height, natural_height);
}
void CellRendererChannels::render_vfunc_thread(const Cairo::RefPtr<Cairo::Context> &cr, Gtk::Widget &widget, const Gdk::Rectangle &background_area, const Gdk::Rectangle &cell_area, Gtk::CellRendererState flags) {
Gtk::Requisition minimum_size, natural_size;
m_renderer_text.get_preferred_size(widget, minimum_size, natural_size);
const int text_x = background_area.get_x() + 26;
const int text_y = background_area.get_y() + background_area.get_height() / 2 - natural_size.height / 2;
const int text_w = natural_size.width;
const int text_h = natural_size.height;
Gdk::Rectangle text_cell_area(text_x, text_y, text_w, text_h);
m_renderer_text.render(cr, widget, background_area, text_cell_area, flags);
}
// dm header
void CellRendererChannels::get_preferred_width_vfunc_dmheader(Gtk::Widget &widget, int &minimum_width, int &natural_width) const {
m_renderer_text.get_preferred_width(widget, minimum_width, natural_width);
}
void CellRendererChannels::get_preferred_width_for_height_vfunc_dmheader(Gtk::Widget &widget, int height, int &minimum_width, int &natural_width) const {
m_renderer_text.get_preferred_width_for_height(widget, height, minimum_width, natural_width);
}
void CellRendererChannels::get_preferred_height_vfunc_dmheader(Gtk::Widget &widget, int &minimum_height, int &natural_height) const {
m_renderer_text.get_preferred_height(widget, minimum_height, natural_height);
}
void CellRendererChannels::get_preferred_height_for_width_vfunc_dmheader(Gtk::Widget &widget, int width, int &minimum_height, int &natural_height) const {
m_renderer_text.get_preferred_height_for_width(widget, width, minimum_height, natural_height);
}
void CellRendererChannels::render_vfunc_dmheader(const Cairo::RefPtr<Cairo::Context> &cr, Gtk::Widget &widget, const Gdk::Rectangle &background_area, const Gdk::Rectangle &cell_area, Gtk::CellRendererState flags) {
// gdk::rectangle more like gdk::stupid
Gdk::Rectangle text_cell_area(
cell_area.get_x() + 9, cell_area.get_y(), // maybe theres a better way to align this ?
cell_area.get_width(), cell_area.get_height());
m_renderer_text.render(cr, widget, background_area, text_cell_area, flags);
}
// dm (basically the same thing as guild)
void CellRendererChannels::get_preferred_width_vfunc_dm(Gtk::Widget &widget, int &minimum_width, int &natural_width) const {
int pixbuf_width = 0;
if (auto pixbuf = m_property_pixbuf.get_value())
pixbuf_width = pixbuf->get_width();
int text_min, text_nat;
m_renderer_text.get_preferred_width(widget, text_min, text_nat);
int xpad, ypad;
get_padding(xpad, ypad);
minimum_width = std::max(text_min, pixbuf_width) + xpad * 2;
natural_width = std::max(text_nat, pixbuf_width) + xpad * 2;
}
void CellRendererChannels::get_preferred_width_for_height_vfunc_dm(Gtk::Widget &widget, int height, int &minimum_width, int &natural_width) const {
get_preferred_width_vfunc_guild(widget, minimum_width, natural_width);
}
void CellRendererChannels::get_preferred_height_vfunc_dm(Gtk::Widget &widget, int &minimum_height, int &natural_height) const {
int pixbuf_height = 0;
if (auto pixbuf = m_property_pixbuf.get_value())
pixbuf_height = pixbuf->get_height();
int text_min, text_nat;
m_renderer_text.get_preferred_height(widget, text_min, text_nat);
int xpad, ypad;
get_padding(xpad, ypad);
minimum_height = std::max(text_min, pixbuf_height) + ypad * 2;
natural_height = std::max(text_nat, pixbuf_height) + ypad * 2;
}
void CellRendererChannels::get_preferred_height_for_width_vfunc_dm(Gtk::Widget &widget, int width, int &minimum_height, int &natural_height) const {
get_preferred_height_vfunc_guild(widget, minimum_height, natural_height);
}
void CellRendererChannels::render_vfunc_dm(const Cairo::RefPtr<Cairo::Context> &cr, Gtk::Widget &widget, const Gdk::Rectangle &background_area, const Gdk::Rectangle &cell_area, Gtk::CellRendererState flags) {
Gtk::Requisition text_minimum, text_natural;
m_renderer_text.get_preferred_size(widget, text_minimum, text_natural);
Gtk::Requisition minimum, natural;
get_preferred_size(widget, minimum, natural);
auto pixbuf = m_property_pixbuf.get_value();
const double icon_w = pixbuf->get_width();
const double icon_h = pixbuf->get_height();
const double icon_x = background_area.get_x() + 2;
const double icon_y = background_area.get_y() + background_area.get_height() / 2.0 - icon_h / 2.0;
const double text_x = icon_x + icon_w + 5.0;
const double text_y = background_area.get_y() + background_area.get_height() / 2.0 - text_natural.height / 2.0;
const double text_w = text_natural.width;
const double text_h = text_natural.height;
Gdk::Rectangle text_cell_area(text_x, text_y, text_w, text_h);
m_renderer_text.render(cr, widget, background_area, text_cell_area, flags);
Gdk::Cairo::set_source_pixbuf(cr, m_property_pixbuf.get_value(), icon_x, icon_y);
cr->rectangle(icon_x, icon_y, icon_w, icon_h);
cr->fill();
}

164
src/components/channels.hpp Normal file
View File

@@ -0,0 +1,164 @@
#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"
#include "state.hpp"
#include "channelscellrenderer.hpp"
constexpr static int GuildIconSize = 24;
constexpr static int DMIconSize = 20;
constexpr static int OrphanChannelSortOffset = -100; // forces orphan channels to the top of the list
class ChannelList : public Gtk::ScrolledWindow {
public:
ChannelList();
void UpdateListing();
void SetActiveChannel(Snowflake id);
// channel list should be populated when this is called
void UseExpansionState(const ExpansionStateRoot &state);
ExpansionStateRoot GetExpansionState() const;
void UsePanedHack(Gtk::Paned &paned);
protected:
void OnPanedPositionChanged();
void UpdateNewGuild(const GuildData &guild);
void UpdateRemoveGuild(Snowflake id);
void UpdateRemoveChannel(Snowflake id);
void UpdateChannel(Snowflake id);
void UpdateCreateChannel(const ChannelData &channel);
void UpdateGuild(Snowflake id);
void DeleteThreadRow(Snowflake id);
void OnChannelMute(Snowflake id);
void OnChannelUnmute(Snowflake id);
void OnGuildMute(Snowflake id);
void OnGuildUnmute(Snowflake id);
void OnThreadJoined(Snowflake id);
void OnThreadRemoved(Snowflake id);
void OnThreadDelete(const ThreadDeleteData &data);
void OnThreadUpdate(const ThreadUpdateData &data);
void OnThreadListSync(const ThreadListSyncData &data);
Gtk::TreeView m_view;
class ModelColumns : public Gtk::TreeModel::ColumnRecord {
public:
ModelColumns();
Gtk::TreeModelColumn<RenderType> m_type;
Gtk::TreeModelColumn<uint64_t> m_id;
Gtk::TreeModelColumn<Glib::ustring> m_name;
Gtk::TreeModelColumn<Glib::RefPtr<Gdk::Pixbuf>> m_icon;
Gtk::TreeModelColumn<Glib::RefPtr<Gdk::PixbufAnimation>> m_icon_anim;
Gtk::TreeModelColumn<int64_t> m_sort;
Gtk::TreeModelColumn<bool> m_nsfw;
// Gtk::CellRenderer's property_is_expanded only works how i want it to if it has children
// because otherwise it doesnt count as an "expander" (property_is_expander)
// so this solution will have to do which i hate but the alternative is adding invisible children
// to all categories without children and having a filter model but that sounds worse
// of course its a lot better than the absolute travesty i had before
Gtk::TreeModelColumn<bool> m_expanded;
};
ModelColumns m_columns;
Glib::RefPtr<Gtk::TreeStore> m_model;
Gtk::TreeModel::iterator AddGuild(const GuildData &guild);
Gtk::TreeModel::iterator UpdateCreateChannelCategory(const ChannelData &channel);
Gtk::TreeModel::iterator CreateThreadRow(const Gtk::TreeNodeChildren &children, const ChannelData &channel);
void UpdateChannelCategory(const ChannelData &channel);
// separation necessary because a channel and guild can share the same id
Gtk::TreeModel::iterator GetIteratorForGuildFromID(Snowflake id);
Gtk::TreeModel::iterator GetIteratorForChannelFromID(Snowflake id);
bool IsTextChannel(ChannelType type);
void OnRowCollapsed(const Gtk::TreeModel::iterator &iter, const Gtk::TreeModel::Path &path);
void OnRowExpanded(const Gtk::TreeModel::iterator &iter, const Gtk::TreeModel::Path &path);
bool SelectionFunc(const Glib::RefPtr<Gtk::TreeModel> &model, const Gtk::TreeModel::Path &path, bool is_currently_selected);
bool OnButtonPressEvent(GdkEventButton *ev);
void MoveRow(const Gtk::TreeModel::iterator &iter, const Gtk::TreeModel::iterator &new_parent);
Gtk::TreeModel::Path m_last_selected;
Gtk::TreeModel::Path m_dm_header;
void AddPrivateChannels();
void UpdateCreateDMChannel(const ChannelData &channel);
void OnMessageAck(const MessageAckData &data);
void OnMessageCreate(const Message &msg);
Gtk::TreeModel::Path m_path_for_menu;
// cant be recovered through selection
Gtk::TreeModel::iterator m_temporary_thread_row;
Gtk::Menu m_menu_guild;
Gtk::MenuItem m_menu_guild_copy_id;
Gtk::MenuItem m_menu_guild_settings;
Gtk::MenuItem m_menu_guild_leave;
Gtk::MenuItem m_menu_guild_mark_as_read;
Gtk::MenuItem m_menu_guild_toggle_mute;
Gtk::Menu m_menu_category;
Gtk::MenuItem m_menu_category_copy_id;
Gtk::MenuItem m_menu_category_toggle_mute;
Gtk::Menu m_menu_channel;
Gtk::MenuItem m_menu_channel_copy_id;
Gtk::MenuItem m_menu_channel_mark_as_read;
Gtk::MenuItem m_menu_channel_toggle_mute;
Gtk::Menu m_menu_dm;
Gtk::MenuItem m_menu_dm_copy_id;
Gtk::MenuItem m_menu_dm_close;
Gtk::MenuItem m_menu_dm_toggle_mute;
Gtk::Menu m_menu_thread;
Gtk::MenuItem m_menu_thread_copy_id;
Gtk::MenuItem m_menu_thread_leave;
Gtk::MenuItem m_menu_thread_archive;
Gtk::MenuItem m_menu_thread_unarchive;
Gtk::MenuItem m_menu_thread_mark_as_read;
Gtk::MenuItem m_menu_thread_toggle_mute;
void OnGuildSubmenuPopup(const Gdk::Rectangle *flipped_rect, const Gdk::Rectangle *final_rect, bool flipped_x, bool flipped_y);
void OnCategorySubmenuPopup(const Gdk::Rectangle *flipped_rect, const Gdk::Rectangle *final_rect, bool flipped_x, bool flipped_y);
void OnChannelSubmenuPopup(const Gdk::Rectangle *flipped_rect, const Gdk::Rectangle *final_rect, bool flipped_x, bool flipped_y);
void OnDMSubmenuPopup(const Gdk::Rectangle *flipped_rect, const Gdk::Rectangle *final_rect, bool flipped_x, bool flipped_y);
void OnThreadSubmenuPopup(const Gdk::Rectangle *flipped_rect, const Gdk::Rectangle *final_rect, bool flipped_x, bool flipped_y);
bool m_updating_listing = false;
Snowflake m_active_channel;
// (GetIteratorForChannelFromID is rather slow)
// only temporary since i dont want to worry about maintaining this map
std::unordered_map<Snowflake, Gtk::TreeModel::iterator> m_tmp_channel_map;
public:
typedef sigc::signal<void, Snowflake> type_signal_action_channel_item_select;
typedef sigc::signal<void, Snowflake> type_signal_action_guild_leave;
typedef sigc::signal<void, Snowflake> type_signal_action_guild_settings;
type_signal_action_channel_item_select signal_action_channel_item_select();
type_signal_action_guild_leave signal_action_guild_leave();
type_signal_action_guild_settings signal_action_guild_settings();
protected:
type_signal_action_channel_item_select m_signal_action_channel_item_select;
type_signal_action_guild_leave m_signal_action_guild_leave;
type_signal_action_guild_settings m_signal_action_guild_settings;
};

View File

@@ -0,0 +1,662 @@
#include "abaddon.hpp"
#include "channelscellrenderer.hpp"
#include <gtkmm.h>
constexpr static int MentionsRightPad = 7;
#ifndef M_PI
constexpr static double M_PI = 3.14159265358979;
#endif
constexpr static double M_PI_H = M_PI / 2.0;
constexpr static double M_PI_3_2 = M_PI * 3.0 / 2.0;
CellRendererChannels::CellRendererChannels()
: Glib::ObjectBase(typeid(CellRendererChannels))
, Gtk::CellRenderer()
, m_property_type(*this, "render-type")
, m_property_id(*this, "id")
, m_property_name(*this, "name")
, m_property_pixbuf(*this, "pixbuf")
, m_property_pixbuf_animation(*this, "pixbuf-animation")
, m_property_expanded(*this, "expanded")
, m_property_nsfw(*this, "nsfw") {
property_mode() = Gtk::CELL_RENDERER_MODE_ACTIVATABLE;
property_xpad() = 2;
property_ypad() = 2;
m_property_name.get_proxy().signal_changed().connect([this] {
m_renderer_text.property_markup() = m_property_name;
});
}
CellRendererChannels::~CellRendererChannels() {
}
Glib::PropertyProxy<RenderType> CellRendererChannels::property_type() {
return m_property_type.get_proxy();
}
Glib::PropertyProxy<uint64_t> CellRendererChannels::property_id() {
return m_property_id.get_proxy();
}
Glib::PropertyProxy<Glib::ustring> CellRendererChannels::property_name() {
return m_property_name.get_proxy();
}
Glib::PropertyProxy<Glib::RefPtr<Gdk::Pixbuf>> CellRendererChannels::property_icon() {
return m_property_pixbuf.get_proxy();
}
Glib::PropertyProxy<Glib::RefPtr<Gdk::PixbufAnimation>> CellRendererChannels::property_icon_animation() {
return m_property_pixbuf_animation.get_proxy();
}
Glib::PropertyProxy<bool> CellRendererChannels::property_expanded() {
return m_property_expanded.get_proxy();
}
Glib::PropertyProxy<bool> CellRendererChannels::property_nsfw() {
return m_property_nsfw.get_proxy();
}
void CellRendererChannels::get_preferred_width_vfunc(Gtk::Widget &widget, int &minimum_width, int &natural_width) const {
switch (m_property_type.get_value()) {
case RenderType::Guild:
return get_preferred_width_vfunc_guild(widget, minimum_width, natural_width);
case RenderType::Category:
return get_preferred_width_vfunc_category(widget, minimum_width, natural_width);
case RenderType::TextChannel:
return get_preferred_width_vfunc_channel(widget, minimum_width, natural_width);
case RenderType::Thread:
return get_preferred_width_vfunc_thread(widget, minimum_width, natural_width);
case RenderType::DMHeader:
return get_preferred_width_vfunc_dmheader(widget, minimum_width, natural_width);
case RenderType::DM:
return get_preferred_width_vfunc_dm(widget, minimum_width, natural_width);
}
}
void CellRendererChannels::get_preferred_width_for_height_vfunc(Gtk::Widget &widget, int height, int &minimum_width, int &natural_width) const {
switch (m_property_type.get_value()) {
case RenderType::Guild:
return get_preferred_width_for_height_vfunc_guild(widget, height, minimum_width, natural_width);
case RenderType::Category:
return get_preferred_width_for_height_vfunc_category(widget, height, minimum_width, natural_width);
case RenderType::TextChannel:
return get_preferred_width_for_height_vfunc_channel(widget, height, minimum_width, natural_width);
case RenderType::Thread:
return get_preferred_width_for_height_vfunc_thread(widget, height, minimum_width, natural_width);
case RenderType::DMHeader:
return get_preferred_width_for_height_vfunc_dmheader(widget, height, minimum_width, natural_width);
case RenderType::DM:
return get_preferred_width_for_height_vfunc_dm(widget, height, minimum_width, natural_width);
}
}
void CellRendererChannels::get_preferred_height_vfunc(Gtk::Widget &widget, int &minimum_height, int &natural_height) const {
switch (m_property_type.get_value()) {
case RenderType::Guild:
return get_preferred_height_vfunc_guild(widget, minimum_height, natural_height);
case RenderType::Category:
return get_preferred_height_vfunc_category(widget, minimum_height, natural_height);
case RenderType::TextChannel:
return get_preferred_height_vfunc_channel(widget, minimum_height, natural_height);
case RenderType::Thread:
return get_preferred_height_vfunc_thread(widget, minimum_height, natural_height);
case RenderType::DMHeader:
return get_preferred_height_vfunc_dmheader(widget, minimum_height, natural_height);
case RenderType::DM:
return get_preferred_height_vfunc_dm(widget, minimum_height, natural_height);
}
}
void CellRendererChannels::get_preferred_height_for_width_vfunc(Gtk::Widget &widget, int width, int &minimum_height, int &natural_height) const {
switch (m_property_type.get_value()) {
case RenderType::Guild:
return get_preferred_height_for_width_vfunc_guild(widget, width, minimum_height, natural_height);
case RenderType::Category:
return get_preferred_height_for_width_vfunc_category(widget, width, minimum_height, natural_height);
case RenderType::TextChannel:
return get_preferred_height_for_width_vfunc_channel(widget, width, minimum_height, natural_height);
case RenderType::Thread:
return get_preferred_height_for_width_vfunc_thread(widget, width, minimum_height, natural_height);
case RenderType::DMHeader:
return get_preferred_height_for_width_vfunc_dmheader(widget, width, minimum_height, natural_height);
case RenderType::DM:
return get_preferred_height_for_width_vfunc_dm(widget, width, minimum_height, natural_height);
}
}
void CellRendererChannels::render_vfunc(const Cairo::RefPtr<Cairo::Context> &cr, Gtk::Widget &widget, const Gdk::Rectangle &background_area, const Gdk::Rectangle &cell_area, Gtk::CellRendererState flags) {
switch (m_property_type.get_value()) {
case RenderType::Guild:
return render_vfunc_guild(cr, widget, background_area, cell_area, flags);
case RenderType::Category:
return render_vfunc_category(cr, widget, background_area, cell_area, flags);
case RenderType::TextChannel:
return render_vfunc_channel(cr, widget, background_area, cell_area, flags);
case RenderType::Thread:
return render_vfunc_thread(cr, widget, background_area, cell_area, flags);
case RenderType::DMHeader:
return render_vfunc_dmheader(cr, widget, background_area, cell_area, flags);
case RenderType::DM:
return render_vfunc_dm(cr, widget, background_area, cell_area, flags);
}
}
// guild functions
void CellRendererChannels::get_preferred_width_vfunc_guild(Gtk::Widget &widget, int &minimum_width, int &natural_width) const {
int pixbuf_width = 0;
if (auto pixbuf = m_property_pixbuf_animation.get_value())
pixbuf_width = pixbuf->get_width();
else if (auto pixbuf = m_property_pixbuf.get_value())
pixbuf_width = pixbuf->get_width();
int text_min, text_nat;
m_renderer_text.get_preferred_width(widget, text_min, text_nat);
int xpad, ypad;
get_padding(xpad, ypad);
minimum_width = std::max(text_min, pixbuf_width) + xpad * 2;
natural_width = std::max(text_nat, pixbuf_width) + xpad * 2;
}
void CellRendererChannels::get_preferred_width_for_height_vfunc_guild(Gtk::Widget &widget, int height, int &minimum_width, int &natural_width) const {
get_preferred_width_vfunc_guild(widget, minimum_width, natural_width);
}
void CellRendererChannels::get_preferred_height_vfunc_guild(Gtk::Widget &widget, int &minimum_height, int &natural_height) const {
int pixbuf_height = 0;
if (auto pixbuf = m_property_pixbuf_animation.get_value())
pixbuf_height = pixbuf->get_height();
else if (auto pixbuf = m_property_pixbuf.get_value())
pixbuf_height = pixbuf->get_height();
int text_min, text_nat;
m_renderer_text.get_preferred_height(widget, text_min, text_nat);
int xpad, ypad;
get_padding(xpad, ypad);
minimum_height = std::max(text_min, pixbuf_height) + ypad * 2;
natural_height = std::max(text_nat, pixbuf_height) + ypad * 2;
}
void CellRendererChannels::get_preferred_height_for_width_vfunc_guild(Gtk::Widget &widget, int width, int &minimum_height, int &natural_height) const {
get_preferred_height_vfunc_guild(widget, minimum_height, natural_height);
}
void CellRendererChannels::render_vfunc_guild(const Cairo::RefPtr<Cairo::Context> &cr, Gtk::Widget &widget, const Gdk::Rectangle &background_area, const Gdk::Rectangle &cell_area, Gtk::CellRendererState flags) {
Gtk::Requisition text_minimum, text_natural;
m_renderer_text.get_preferred_size(widget, text_minimum, text_natural);
Gtk::Requisition minimum, natural;
get_preferred_size(widget, minimum, natural);
int pixbuf_w, pixbuf_h = 0;
if (auto pixbuf = m_property_pixbuf_animation.get_value()) {
pixbuf_w = pixbuf->get_width();
pixbuf_h = pixbuf->get_height();
} else if (auto pixbuf = m_property_pixbuf.get_value()) {
pixbuf_w = pixbuf->get_width();
pixbuf_h = pixbuf->get_height();
}
const double icon_w = pixbuf_w;
const double icon_h = pixbuf_h;
const double icon_x = background_area.get_x() + 3;
const double icon_y = background_area.get_y() + background_area.get_height() / 2.0 - icon_h / 2.0;
const double text_x = icon_x + icon_w + 5.0;
const double text_y = background_area.get_y() + background_area.get_height() / 2.0 - text_natural.height / 2.0;
const double text_w = text_natural.width;
const double text_h = text_natural.height;
Gdk::Rectangle text_cell_area(text_x, text_y, text_w, text_h);
static const auto color = Gdk::RGBA(Abaddon::Get().GetSettings().ChannelColor);
m_renderer_text.property_foreground_rgba() = color;
m_renderer_text.render(cr, widget, background_area, text_cell_area, flags);
const bool hover_only = Abaddon::Get().GetSettings().AnimatedGuildHoverOnly;
const bool is_hovered = flags & Gtk::CELL_RENDERER_PRELIT;
auto anim = m_property_pixbuf_animation.get_value();
// kinda gross
if (anim) {
auto map_iter = m_pixbuf_anim_iters.find(anim);
if (map_iter == m_pixbuf_anim_iters.end())
m_pixbuf_anim_iters[anim] = anim->get_iter(nullptr);
auto pb_iter = m_pixbuf_anim_iters.at(anim);
const auto cb = [this, &widget, anim, icon_x, icon_y, icon_w, icon_h] {
if (m_pixbuf_anim_iters.at(anim)->advance())
widget.queue_draw_area(icon_x, icon_y, icon_w, icon_h);
};
if ((hover_only && is_hovered) || !hover_only)
Glib::signal_timeout().connect_once(sigc::track_obj(cb, widget), pb_iter->get_delay_time());
if (hover_only && !is_hovered)
m_pixbuf_anim_iters[anim] = anim->get_iter(nullptr);
Gdk::Cairo::set_source_pixbuf(cr, pb_iter->get_pixbuf(), icon_x, icon_y);
cr->rectangle(icon_x, icon_y, icon_w, icon_h);
cr->fill();
} else if (auto pixbuf = m_property_pixbuf.get_value()) {
Gdk::Cairo::set_source_pixbuf(cr, pixbuf, icon_x, icon_y);
cr->rectangle(icon_x, icon_y, icon_w, icon_h);
cr->fill();
}
// unread
if (!Abaddon::Get().GetSettings().Unreads) return;
const auto id = m_property_id.get_value();
auto &discord = Abaddon::Get().GetDiscordClient();
int total_mentions;
const auto has_unread = discord.GetUnreadStateForGuild(id, total_mentions);
if (has_unread && !discord.IsGuildMuted(id)) {
cr->set_source_rgb(1.0, 1.0, 1.0);
const auto x = background_area.get_x();
const auto y = background_area.get_y();
const auto w = background_area.get_width();
const auto h = background_area.get_height();
cr->rectangle(x, y + h / 2 - 24 / 2, 3, 24);
cr->fill();
}
if (total_mentions < 1) return;
auto *paned = static_cast<Gtk::Paned *>(widget.get_ancestor(Gtk::Paned::get_type()));
if (paned != nullptr) {
const auto edge = std::min(paned->get_position(), background_area.get_width());
unread_render_mentions(cr, widget, total_mentions, edge, background_area);
}
}
// category
void CellRendererChannels::get_preferred_width_vfunc_category(Gtk::Widget &widget, int &minimum_width, int &natural_width) const {
m_renderer_text.get_preferred_width(widget, minimum_width, natural_width);
}
void CellRendererChannels::get_preferred_width_for_height_vfunc_category(Gtk::Widget &widget, int height, int &minimum_width, int &natural_width) const {
m_renderer_text.get_preferred_width_for_height(widget, height, minimum_width, natural_width);
}
void CellRendererChannels::get_preferred_height_vfunc_category(Gtk::Widget &widget, int &minimum_height, int &natural_height) const {
m_renderer_text.get_preferred_height(widget, minimum_height, natural_height);
}
void CellRendererChannels::get_preferred_height_for_width_vfunc_category(Gtk::Widget &widget, int width, int &minimum_height, int &natural_height) const {
m_renderer_text.get_preferred_height_for_width(widget, width, minimum_height, natural_height);
}
void CellRendererChannels::render_vfunc_category(const Cairo::RefPtr<Cairo::Context> &cr, Gtk::Widget &widget, const Gdk::Rectangle &background_area, const Gdk::Rectangle &cell_area, Gtk::CellRendererState flags) {
// todo: figure out how Gtk::Arrow is rendered because i like it better :^)
constexpr static int len = 5;
int x1, y1, x2, y2, x3, y3;
if (property_expanded()) {
x1 = background_area.get_x() + 7;
y1 = background_area.get_y() + background_area.get_height() / 2 - len;
x2 = background_area.get_x() + 7 + len;
y2 = background_area.get_y() + background_area.get_height() / 2 + len;
x3 = background_area.get_x() + 7 + len * 2;
y3 = background_area.get_y() + background_area.get_height() / 2 - len;
} else {
x1 = background_area.get_x() + 7;
y1 = background_area.get_y() + background_area.get_height() / 2 - len;
x2 = background_area.get_x() + 7 + len * 2;
y2 = background_area.get_y() + background_area.get_height() / 2;
x3 = background_area.get_x() + 7;
y3 = background_area.get_y() + background_area.get_height() / 2 + len;
}
cr->move_to(x1, y1);
cr->line_to(x2, y2);
cr->line_to(x3, y3);
const auto expander_color = Gdk::RGBA(Abaddon::Get().GetSettings().ChannelsExpanderColor);
cr->set_source_rgb(expander_color.get_red(), expander_color.get_green(), expander_color.get_blue());
cr->stroke();
Gtk::Requisition text_minimum, text_natural;
m_renderer_text.get_preferred_size(widget, text_minimum, text_natural);
const int text_x = background_area.get_x() + 22;
const int text_y = background_area.get_y() + background_area.get_height() / 2 - text_natural.height / 2;
const int text_w = text_natural.width;
const int text_h = text_natural.height;
Gdk::Rectangle text_cell_area(text_x, text_y, text_w, text_h);
static const auto color = Gdk::RGBA(Abaddon::Get().GetSettings().ChannelColor);
if (Abaddon::Get().GetDiscordClient().IsChannelMuted(m_property_id.get_value())) {
auto muted = color;
muted.set_red(muted.get_red() * 0.5);
muted.set_green(muted.get_green() * 0.5);
muted.set_blue(muted.get_blue() * 0.5);
m_renderer_text.property_foreground_rgba() = muted;
} else {
m_renderer_text.property_foreground_rgba() = color;
}
m_renderer_text.render(cr, widget, background_area, text_cell_area, flags);
m_renderer_text.property_foreground_set() = false;
}
// text channel
void CellRendererChannels::get_preferred_width_vfunc_channel(Gtk::Widget &widget, int &minimum_width, int &natural_width) const {
m_renderer_text.get_preferred_width(widget, minimum_width, natural_width);
}
void CellRendererChannels::get_preferred_width_for_height_vfunc_channel(Gtk::Widget &widget, int height, int &minimum_width, int &natural_width) const {
m_renderer_text.get_preferred_width_for_height(widget, height, minimum_width, natural_width);
}
void CellRendererChannels::get_preferred_height_vfunc_channel(Gtk::Widget &widget, int &minimum_height, int &natural_height) const {
m_renderer_text.get_preferred_height(widget, minimum_height, natural_height);
}
void CellRendererChannels::get_preferred_height_for_width_vfunc_channel(Gtk::Widget &widget, int width, int &minimum_height, int &natural_height) const {
m_renderer_text.get_preferred_height_for_width(widget, width, minimum_height, natural_height);
}
void CellRendererChannels::render_vfunc_channel(const Cairo::RefPtr<Cairo::Context> &cr, Gtk::Widget &widget, const Gdk::Rectangle &background_area, const Gdk::Rectangle &cell_area, Gtk::CellRendererState flags) {
Gtk::Requisition minimum_size, natural_size;
m_renderer_text.get_preferred_size(widget, minimum_size, natural_size);
const int text_x = background_area.get_x() + 21;
const int text_y = background_area.get_y() + background_area.get_height() / 2 - natural_size.height / 2;
const int text_w = natural_size.width;
const int text_h = natural_size.height;
Gdk::Rectangle text_cell_area(text_x, text_y, text_w, text_h);
auto &discord = Abaddon::Get().GetDiscordClient();
const auto id = m_property_id.get_value();
const bool is_muted = discord.IsChannelMuted(id);
static const auto sfw_unmuted = Gdk::RGBA(Abaddon::Get().GetSettings().ChannelColor);
m_renderer_text.property_sensitive() = false;
static const auto nsfw_color = Gdk::RGBA(Abaddon::Get().GetSettings().NSFWChannelColor);
if (m_property_nsfw.get_value())
m_renderer_text.property_foreground_rgba() = nsfw_color;
else
m_renderer_text.property_foreground_rgba() = sfw_unmuted;
if (is_muted) {
auto col = m_renderer_text.property_foreground_rgba().get_value();
col.set_red(col.get_red() * 0.5);
col.set_green(col.get_green() * 0.5);
col.set_blue(col.get_blue() * 0.5);
m_renderer_text.property_foreground_rgba() = col;
}
m_renderer_text.render(cr, widget, background_area, text_cell_area, flags);
// unset foreground to default so properties dont bleed
m_renderer_text.property_foreground_set() = false;
// unread
if (!Abaddon::Get().GetSettings().Unreads) return;
const auto unread_state = discord.GetUnreadStateForChannel(id);
if (unread_state < 0) return;
if (!is_muted) {
cr->set_source_rgb(1.0, 1.0, 1.0);
const auto x = background_area.get_x();
const auto y = background_area.get_y();
const auto w = background_area.get_width();
const auto h = background_area.get_height();
cr->rectangle(x, y, 3, h);
cr->fill();
}
if (unread_state < 1) return;
auto *paned = static_cast<Gtk::Paned *>(widget.get_ancestor(Gtk::Paned::get_type()));
if (paned != nullptr) {
const auto edge = std::min(paned->get_position(), cell_area.get_width());
unread_render_mentions(cr, widget, unread_state, edge, cell_area);
}
}
// thread
void CellRendererChannels::get_preferred_width_vfunc_thread(Gtk::Widget &widget, int &minimum_width, int &natural_width) const {
m_renderer_text.get_preferred_width(widget, minimum_width, natural_width);
}
void CellRendererChannels::get_preferred_width_for_height_vfunc_thread(Gtk::Widget &widget, int height, int &minimum_width, int &natural_width) const {
get_preferred_width_vfunc_thread(widget, minimum_width, natural_width);
}
void CellRendererChannels::get_preferred_height_vfunc_thread(Gtk::Widget &widget, int &minimum_height, int &natural_height) const {
m_renderer_text.get_preferred_height(widget, minimum_height, natural_height);
}
void CellRendererChannels::get_preferred_height_for_width_vfunc_thread(Gtk::Widget &widget, int width, int &minimum_height, int &natural_height) const {
get_preferred_height_vfunc_thread(widget, minimum_height, natural_height);
}
void CellRendererChannels::render_vfunc_thread(const Cairo::RefPtr<Cairo::Context> &cr, Gtk::Widget &widget, const Gdk::Rectangle &background_area, const Gdk::Rectangle &cell_area, Gtk::CellRendererState flags) {
Gtk::Requisition minimum_size, natural_size;
m_renderer_text.get_preferred_size(widget, minimum_size, natural_size);
const int text_x = background_area.get_x() + 26;
const int text_y = background_area.get_y() + background_area.get_height() / 2 - natural_size.height / 2;
const int text_w = natural_size.width;
const int text_h = natural_size.height;
Gdk::Rectangle text_cell_area(text_x, text_y, text_w, text_h);
auto &discord = Abaddon::Get().GetDiscordClient();
const auto id = m_property_id.get_value();
const bool is_muted = discord.IsChannelMuted(id);
static const auto color = Gdk::RGBA(Abaddon::Get().GetSettings().ChannelColor);
if (Abaddon::Get().GetDiscordClient().IsChannelMuted(m_property_id.get_value())) {
auto muted = color;
muted.set_red(muted.get_red() * 0.5);
muted.set_green(muted.get_green() * 0.5);
muted.set_blue(muted.get_blue() * 0.5);
m_renderer_text.property_foreground_rgba() = muted;
} else {
m_renderer_text.property_foreground_rgba() = color;
}
m_renderer_text.render(cr, widget, background_area, text_cell_area, flags);
m_renderer_text.property_foreground_set() = false;
// unread
if (!Abaddon::Get().GetSettings().Unreads) return;
const auto unread_state = discord.GetUnreadStateForChannel(id);
if (unread_state < 0) return;
if (!is_muted) {
cr->set_source_rgb(1.0, 1.0, 1.0);
const auto x = background_area.get_x();
const auto y = background_area.get_y();
const auto w = background_area.get_width();
const auto h = background_area.get_height();
cr->rectangle(x, y, 3, h);
cr->fill();
}
if (unread_state < 1) return;
auto *paned = static_cast<Gtk::Paned *>(widget.get_ancestor(Gtk::Paned::get_type()));
if (paned != nullptr) {
const auto edge = std::min(paned->get_position(), cell_area.get_width());
unread_render_mentions(cr, widget, unread_state, edge, cell_area);
}
}
// dm header
void CellRendererChannels::get_preferred_width_vfunc_dmheader(Gtk::Widget &widget, int &minimum_width, int &natural_width) const {
m_renderer_text.get_preferred_width(widget, minimum_width, natural_width);
}
void CellRendererChannels::get_preferred_width_for_height_vfunc_dmheader(Gtk::Widget &widget, int height, int &minimum_width, int &natural_width) const {
m_renderer_text.get_preferred_width_for_height(widget, height, minimum_width, natural_width);
}
void CellRendererChannels::get_preferred_height_vfunc_dmheader(Gtk::Widget &widget, int &minimum_height, int &natural_height) const {
m_renderer_text.get_preferred_height(widget, minimum_height, natural_height);
}
void CellRendererChannels::get_preferred_height_for_width_vfunc_dmheader(Gtk::Widget &widget, int width, int &minimum_height, int &natural_height) const {
m_renderer_text.get_preferred_height_for_width(widget, width, minimum_height, natural_height);
}
void CellRendererChannels::render_vfunc_dmheader(const Cairo::RefPtr<Cairo::Context> &cr, Gtk::Widget &widget, const Gdk::Rectangle &background_area, const Gdk::Rectangle &cell_area, Gtk::CellRendererState flags) {
// gdk::rectangle more like gdk::stupid
Gdk::Rectangle text_cell_area(
cell_area.get_x() + 9, cell_area.get_y(), // maybe theres a better way to align this ?
cell_area.get_width(), cell_area.get_height());
m_renderer_text.render(cr, widget, background_area, text_cell_area, flags);
if (!Abaddon::Get().GetSettings().Unreads) return;
auto *paned = static_cast<Gtk::Paned *>(widget.get_ancestor(Gtk::Paned::get_type()));
if (paned != nullptr) {
const auto edge = std::min(paned->get_position(), background_area.get_width());
if (const auto unread = Abaddon::Get().GetDiscordClient().GetUnreadDMsCount(); unread > 0)
unread_render_mentions(cr, widget, unread, edge, background_area);
}
}
// dm (basically the same thing as guild)
void CellRendererChannels::get_preferred_width_vfunc_dm(Gtk::Widget &widget, int &minimum_width, int &natural_width) const {
int pixbuf_width = 0;
if (auto pixbuf = m_property_pixbuf.get_value())
pixbuf_width = pixbuf->get_width();
int text_min, text_nat;
m_renderer_text.get_preferred_width(widget, text_min, text_nat);
int xpad, ypad;
get_padding(xpad, ypad);
minimum_width = std::max(text_min, pixbuf_width) + xpad * 2;
natural_width = std::max(text_nat, pixbuf_width) + xpad * 2;
}
void CellRendererChannels::get_preferred_width_for_height_vfunc_dm(Gtk::Widget &widget, int height, int &minimum_width, int &natural_width) const {
get_preferred_width_vfunc_guild(widget, minimum_width, natural_width);
}
void CellRendererChannels::get_preferred_height_vfunc_dm(Gtk::Widget &widget, int &minimum_height, int &natural_height) const {
int pixbuf_height = 0;
if (auto pixbuf = m_property_pixbuf.get_value())
pixbuf_height = pixbuf->get_height();
int text_min, text_nat;
m_renderer_text.get_preferred_height(widget, text_min, text_nat);
int xpad, ypad;
get_padding(xpad, ypad);
minimum_height = std::max(text_min, pixbuf_height) + ypad * 2;
natural_height = std::max(text_nat, pixbuf_height) + ypad * 2;
}
void CellRendererChannels::get_preferred_height_for_width_vfunc_dm(Gtk::Widget &widget, int width, int &minimum_height, int &natural_height) const {
get_preferred_height_vfunc_guild(widget, minimum_height, natural_height);
}
void CellRendererChannels::render_vfunc_dm(const Cairo::RefPtr<Cairo::Context> &cr, Gtk::Widget &widget, const Gdk::Rectangle &background_area, const Gdk::Rectangle &cell_area, Gtk::CellRendererState flags) {
Gtk::Requisition text_minimum, text_natural;
m_renderer_text.get_preferred_size(widget, text_minimum, text_natural);
Gtk::Requisition minimum, natural;
get_preferred_size(widget, minimum, natural);
auto pixbuf = m_property_pixbuf.get_value();
const double icon_w = pixbuf->get_width();
const double icon_h = pixbuf->get_height();
const double icon_x = background_area.get_x() + 3;
const double icon_y = background_area.get_y() + background_area.get_height() / 2.0 - icon_h / 2.0;
const double text_x = icon_x + icon_w + 6.0;
const double text_y = background_area.get_y() + background_area.get_height() / 2.0 - text_natural.height / 2.0;
const double text_w = text_natural.width;
const double text_h = text_natural.height;
Gdk::Rectangle text_cell_area(text_x, text_y, text_w, text_h);
auto &discord = Abaddon::Get().GetDiscordClient();
const auto id = m_property_id.get_value();
const bool is_muted = discord.IsChannelMuted(id);
static const auto color = Gdk::RGBA(Abaddon::Get().GetSettings().ChannelColor);
if (Abaddon::Get().GetDiscordClient().IsChannelMuted(m_property_id.get_value())) {
auto muted = color;
muted.set_red(muted.get_red() * 0.5);
muted.set_green(muted.get_green() * 0.5);
muted.set_blue(muted.get_blue() * 0.5);
m_renderer_text.property_foreground_rgba() = muted;
} else {
m_renderer_text.property_foreground_rgba() = color;
}
m_renderer_text.render(cr, widget, background_area, text_cell_area, flags);
m_renderer_text.property_foreground_set() = false;
Gdk::Cairo::set_source_pixbuf(cr, m_property_pixbuf.get_value(), icon_x, icon_y);
cr->rectangle(icon_x, icon_y, icon_w, icon_h);
cr->fill();
// unread
if (!Abaddon::Get().GetSettings().Unreads) return;
const auto unread_state = discord.GetUnreadStateForChannel(id);
if (unread_state < 0) return;
if (!is_muted) {
cr->set_source_rgb(1.0, 1.0, 1.0);
const auto x = background_area.get_x();
const auto y = background_area.get_y();
const auto w = background_area.get_width();
const auto h = background_area.get_height();
cr->rectangle(x, y, 3, h);
cr->fill();
}
}
void CellRendererChannels::cairo_path_rounded_rect(const Cairo::RefPtr<Cairo::Context> &cr, double x, double y, double w, double h, double r) {
const double degrees = M_PI / 180.0;
cr->begin_new_sub_path();
cr->arc(x + w - r, y + r, r, -M_PI_H, 0);
cr->arc(x + w - r, y + h - r, r, 0, M_PI_H);
cr->arc(x + r, y + h - r, r, M_PI_H, M_PI);
cr->arc(x + r, y + r, r, M_PI, M_PI_3_2);
cr->close_path();
}
void CellRendererChannels::unread_render_mentions(const Cairo::RefPtr<Cairo::Context> &cr, Gtk::Widget &widget, int mentions, int edge, const Gdk::Rectangle &cell_area) {
Pango::FontDescription font;
font.set_family("sans 14");
//font.set_weight(Pango::WEIGHT_BOLD);
auto layout = widget.create_pango_layout(std::to_string(mentions));
layout->set_font_description(font);
layout->set_alignment(Pango::ALIGN_RIGHT);
int width, height;
layout->get_pixel_size(width, height);
{
static const auto bg = Gdk::RGBA(Abaddon::Get().GetSettings().MentionBadgeColor);
static const auto text = Gdk::RGBA(Abaddon::Get().GetSettings().MentionBadgeTextColor);
const auto x = cell_area.get_x() + edge - width - MentionsRightPad;
const auto y = cell_area.get_y() + cell_area.get_height() / 2.0 - height / 2.0 - 1;
cairo_path_rounded_rect(cr, x - 4, y + 2, width + 8, height, 5);
cr->set_source_rgb(bg.get_red(), bg.get_green(), bg.get_blue());
cr->fill();
cr->set_source_rgb(text.get_red(), text.get_green(), text.get_blue());
cr->move_to(x, y);
layout->show_in_cairo_context(cr);
}
}

View File

@@ -1,16 +1,9 @@
#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"
constexpr static int GuildIconSize = 24;
constexpr static int DMIconSize = 20;
constexpr static int OrphanChannelSortOffset = -100; // forces orphan channels to the top of the list
#include <gtkmm/cellrenderertext.h>
#include <gdkmm/pixbufanimation.h>
#include <glibmm/property.h>
#include <map>
#include "discord/snowflake.hpp"
enum class RenderType : uint8_t {
Guild,
@@ -28,6 +21,7 @@ public:
virtual ~CellRendererChannels();
Glib::PropertyProxy<RenderType> property_type();
Glib::PropertyProxy<uint64_t> property_id();
Glib::PropertyProxy<Glib::ustring> property_name();
Glib::PropertyProxy<Glib::RefPtr<Gdk::Pixbuf>> property_icon();
Glib::PropertyProxy<Glib::RefPtr<Gdk::PixbufAnimation>> property_icon_animation();
@@ -111,11 +105,15 @@ protected:
const Gdk::Rectangle &cell_area,
Gtk::CellRendererState flags);
static void cairo_path_rounded_rect(const Cairo::RefPtr<Cairo::Context> &cr, double x, double y, double w, double h, double r);
void unread_render_mentions(const Cairo::RefPtr<Cairo::Context> &cr, Gtk::Widget &widget, int mentions, int edge, const Gdk::Rectangle &cell_area);
private:
Gtk::CellRendererText m_renderer_text;
Glib::Property<RenderType> m_property_type; // all
Glib::Property<Glib::ustring> m_property_name; // all
Glib::Property<RenderType> m_property_type; // all
Glib::Property<Glib::ustring> m_property_name; // all
Glib::Property<uint64_t> m_property_id;
Glib::Property<Glib::RefPtr<Gdk::Pixbuf>> m_property_pixbuf; // guild, dm
Glib::Property<Glib::RefPtr<Gdk::PixbufAnimation>> m_property_pixbuf_animation; // guild
Glib::Property<bool> m_property_expanded; // category
@@ -126,120 +124,3 @@ private:
// an animation or two wont be the end of the world though
std::map<Glib::RefPtr<Gdk::PixbufAnimation>, Glib::RefPtr<Gdk::PixbufAnimationIter>> m_pixbuf_anim_iters;
};
class ChannelList : public Gtk::ScrolledWindow {
public:
ChannelList();
void UpdateListing();
void SetActiveChannel(Snowflake id);
protected:
void UpdateNewGuild(const GuildData &guild);
void UpdateRemoveGuild(Snowflake id);
void UpdateRemoveChannel(Snowflake id);
void UpdateChannel(Snowflake id);
void UpdateCreateChannel(const ChannelData &channel);
void UpdateGuild(Snowflake id);
void DeleteThreadRow(Snowflake id);
void OnThreadJoined(Snowflake id);
void OnThreadRemoved(Snowflake id);
void OnThreadDelete(const ThreadDeleteData &data);
void OnThreadUpdate(const ThreadUpdateData &data);
void OnThreadListSync(const ThreadListSyncData &data);
Gtk::TreeView m_view;
class ModelColumns : public Gtk::TreeModel::ColumnRecord {
public:
ModelColumns();
Gtk::TreeModelColumn<RenderType> m_type;
Gtk::TreeModelColumn<uint64_t> m_id;
Gtk::TreeModelColumn<Glib::ustring> m_name;
Gtk::TreeModelColumn<Glib::RefPtr<Gdk::Pixbuf>> m_icon;
Gtk::TreeModelColumn<Glib::RefPtr<Gdk::PixbufAnimation>> m_icon_anim;
Gtk::TreeModelColumn<int64_t> m_sort;
Gtk::TreeModelColumn<bool> m_nsfw;
// Gtk::CellRenderer's property_is_expanded only works how i want it to if it has children
// because otherwise it doesnt count as an "expander" (property_is_expander)
// so this solution will have to do which i hate but the alternative is adding invisible children
// to all categories without children and having a filter model but that sounds worse
// of course its a lot better than the absolute travesty i had before
Gtk::TreeModelColumn<bool> m_expanded;
};
ModelColumns m_columns;
Glib::RefPtr<Gtk::TreeStore> m_model;
Gtk::TreeModel::iterator AddGuild(const GuildData &guild);
Gtk::TreeModel::iterator UpdateCreateChannelCategory(const ChannelData &channel);
Gtk::TreeModel::iterator CreateThreadRow(const Gtk::TreeNodeChildren &children, const ChannelData &channel);
void UpdateChannelCategory(const ChannelData &channel);
// separation necessary because a channel and guild can share the same id
Gtk::TreeModel::iterator GetIteratorForGuildFromID(Snowflake id);
Gtk::TreeModel::iterator GetIteratorForChannelFromID(Snowflake id);
bool IsTextChannel(ChannelType type);
void OnRowCollapsed(const Gtk::TreeModel::iterator &iter, const Gtk::TreeModel::Path &path);
void OnRowExpanded(const Gtk::TreeModel::iterator &iter, const Gtk::TreeModel::Path &path);
bool SelectionFunc(const Glib::RefPtr<Gtk::TreeModel> &model, const Gtk::TreeModel::Path &path, bool is_currently_selected);
bool OnButtonPressEvent(GdkEventButton *ev);
void MoveRow(const Gtk::TreeModel::iterator &iter, const Gtk::TreeModel::iterator &new_parent);
Gtk::TreeModel::Path m_last_selected;
Gtk::TreeModel::Path m_dm_header;
void AddPrivateChannels();
void UpdateCreateDMChannel(const ChannelData &channel);
void OnMessageCreate(const Message &msg);
Gtk::TreeModel::Path m_path_for_menu;
// cant be recovered through selection
Gtk::TreeModel::iterator m_temporary_thread_row;
Gtk::Menu m_menu_guild;
Gtk::MenuItem m_menu_guild_copy_id;
Gtk::MenuItem m_menu_guild_settings;
Gtk::MenuItem m_menu_guild_leave;
Gtk::Menu m_menu_category;
Gtk::MenuItem m_menu_category_copy_id;
Gtk::Menu m_menu_channel;
Gtk::MenuItem m_menu_channel_copy_id;
Gtk::Menu m_menu_dm;
Gtk::MenuItem m_menu_dm_copy_id;
Gtk::MenuItem m_menu_dm_close;
Gtk::Menu m_menu_thread;
Gtk::MenuItem m_menu_thread_copy_id;
Gtk::MenuItem m_menu_thread_leave;
Gtk::MenuItem m_menu_thread_archive;
Gtk::MenuItem m_menu_thread_unarchive;
void OnThreadSubmenuPopup(const Gdk::Rectangle *flipped_rect, const Gdk::Rectangle *final_rect, bool flipped_x, bool flipped_y);
bool m_updating_listing = false;
public:
typedef sigc::signal<void, Snowflake> type_signal_action_channel_item_select;
typedef sigc::signal<void, Snowflake> type_signal_action_guild_leave;
typedef sigc::signal<void, Snowflake> type_signal_action_guild_settings;
type_signal_action_channel_item_select signal_action_channel_item_select();
type_signal_action_guild_leave signal_action_guild_leave();
type_signal_action_guild_settings signal_action_guild_settings();
protected:
type_signal_action_channel_item_select m_signal_action_channel_item_select;
type_signal_action_guild_leave m_signal_action_guild_leave;
type_signal_action_guild_settings m_signal_action_guild_settings;
};

View File

@@ -1,7 +1,7 @@
#include <filesystem>
#include "chatinputindicator.hpp"
#include "../abaddon.hpp"
#include "../util.hpp"
#include "abaddon.hpp"
#include "util.hpp"
constexpr static const int MaxUsersInIndicator = 4;
@@ -112,7 +112,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 (int i = 0; i < typers.size() - 1; i++)
for (size_t i = 0; i < typers.size() - 1; i++)
str += typers[i].Username + ", ";
SetTypingString(str + "and " + typers[typers.size() - 1].Username + " are typing...");
} else { // size() > MaxUsersInIndicator

View File

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

View File

@@ -1,6 +1,6 @@
#include "chatmessage.hpp"
#include "../abaddon.hpp"
#include "../util.hpp"
#include "abaddon.hpp"
#include "util.hpp"
#include "lazyimage.hpp"
#include <unordered_map>
@@ -116,8 +116,10 @@ void ChatMessageItemContainer::UpdateContent() {
}
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) {
@@ -229,11 +231,13 @@ 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)
@@ -306,8 +310,6 @@ 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);
@@ -355,7 +357,7 @@ Gtk::Widget *ChatMessageItemContainer::CreateEmbedComponent(const EmbedData &emb
}
return false;
});
static auto color = Abaddon::Get().GetSettings().GetLinkColor();
static auto color = Abaddon::Get().GetSettings().LinkColor;
title_label->override_color(Gdk::RGBA(color));
title_label->set_markup("<b>" + Glib::Markup::escape_text(*embed.Title) + "</b>");
}
@@ -653,6 +655,14 @@ 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);
@@ -664,16 +674,16 @@ Gtk::Widget *ChatMessageItemContainer::CreateReplyComponent(const Message &data)
} else {
lbl->set_markup(user.GetEscapedBoldString<false>());
}
} else if (data.ReferencedMessage.has_value()) {
if (data.ReferencedMessage.value().get() == nullptr) {
} else if (referenced_message.has_value()) {
if (referenced_message.value() == nullptr) {
lbl->set_markup("<i>deleted message</i>");
} else {
const auto &referenced = *data.ReferencedMessage.value().get();
const auto &referenced = *referenced_message.value();
Glib::ustring text;
if (referenced.Content == "") {
if (referenced.Attachments.size() > 0) {
if (referenced.Content.empty()) {
if (!referenced.Attachments.empty()) {
text = "<i>attachment</i>";
} else if (referenced.Embeds.size() > 0) {
} else if (!referenced.Embeds.empty()) {
text = "<i>embed</i>";
}
} else {
@@ -788,7 +798,6 @@ 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);
@@ -796,7 +805,7 @@ void ChatMessageItemContainer::HandleCustomEmojis(Gtk::TextView &tv) {
auto end_it = buf->get_iter_at_offset(chars_end);
startpos = mend;
if (is_animated && show_animations) {
if (is_animated && Abaddon::Get().GetSettings().ShowAnimations) {
const auto mark_start = buf->create_mark(start_it, false);
end_it.backward_char();
const auto mark_end = buf->create_mark(end_it, false);
@@ -825,7 +834,9 @@ void ChatMessageItemContainer::HandleCustomEmojis(Gtk::TextView &tv) {
buf->delete_mark(mark_start);
buf->delete_mark(mark_end);
auto it = buf->erase(start_it, end_it);
buf->insert_pixbuf(it, pixbuf->scale_simple(EmojiSize, EmojiSize, Gdk::INTERP_BILINEAR));
int width, height;
GetImageDimensions(pixbuf->get_width(), pixbuf->get_height(), width, height, EmojiSize, EmojiSize);
buf->insert_pixbuf(it, pixbuf->scale_simple(width, height, Gdk::INTERP_BILINEAR));
};
img.LoadFromURL(EmojiData::URLFromID(match.fetch(2)), sigc::track_obj(cb, tv));
}
@@ -835,11 +846,8 @@ void ChatMessageItemContainer::HandleCustomEmojis(Gtk::TextView &tv) {
}
void ChatMessageItemContainer::HandleEmojis(Gtk::TextView &tv) {
static const bool stock_emojis = Abaddon::Get().GetSettings().GetShowStockEmojis();
static const bool custom_emojis = Abaddon::Get().GetSettings().GetShowCustomEmojis();
if (stock_emojis) HandleStockEmojis(tv);
if (custom_emojis) HandleCustomEmojis(tv);
if (Abaddon::Get().GetSettings().ShowStockEmojis) HandleStockEmojis(tv);
if (Abaddon::Get().GetSettings().ShowCustomEmojis) HandleCustomEmojis(tv);
}
void ChatMessageItemContainer::CleanupEmojis(Glib::RefPtr<Gtk::TextBuffer> buf) {
@@ -959,9 +967,6 @@ 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)) {
@@ -970,7 +975,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(link_color);
tag->property_foreground_rgba() = Gdk::RGBA(Abaddon::Get().GetSettings().LinkColor);
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);
@@ -1128,7 +1133,7 @@ ChatMessageHeader::ChatMessageHeader(const Message &data)
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()) {
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);

View File

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

View File

@@ -1,6 +1,6 @@
#include "chatwindow.hpp"
#include "chatmessage.hpp"
#include "../abaddon.hpp"
#include "abaddon.hpp"
#include "chatinputindicator.hpp"
#include "ratelimitindicator.hpp"
#include "chatinput.hpp"

View File

@@ -2,7 +2,7 @@
#include <gtkmm.h>
#include <string>
#include <set>
#include "../discord/discord.hpp"
#include "discord/discord.hpp"
#include "completer.hpp"
class ChatMessageHeader;

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