153 Commits

Author SHA1 Message Date
ouwou
af85d5c895 Merge branch 'threads' 2021-09-13 01:44:43 -04:00
ouwou
b012340830 fix join guild
even though this probably isnt the best idea in a third party client...
2021-09-13 01:44:20 -04:00
ouwou
002004cb5f add notice for archived threads at top of chatwindow 2021-09-03 04:04:51 -04:00
ouwou
f3769ca301 remove archived threads from channel list on sync
this probably doesnt do anything but i added it while trying to fix something and it might work sometime i dont know
2021-09-03 03:49:25 -04:00
ouwou
07e30b9acd send channel lazy load when loading thread if not previously sent 2021-09-01 22:22:08 -04:00
ouwou
66747ec753 permissions check on archive/unarchive (oops) 2021-08-28 02:43:02 -04:00
ouwou
8ed2cd65b6 archive/unarchive from channel list 2021-08-28 02:28:41 -04:00
ouwou
3977159415 threads have pins 2021-08-27 20:29:00 -04:00
ouwou
3333e2ce8c make ClearChannel actually remove the channel from the db
so that the threads window does not show deleted threads in active
2021-08-24 23:04:11 -04:00
ouwou
ab948c29b7 fix moving rows 2021-08-24 01:51:49 -04:00
ouwou
af0cbcf2c1 dont try to open channels if they arent in store 2021-08-24 01:18:37 -04:00
ouwou
95b93a0698 dont fetch temp pending msgs from store 2021-08-24 00:30:46 -04:00
ouwou
04ebd069b4 ad-hoc MoveRow for channel list
so threads no longer disappear when a channel is moved
2021-08-23 19:15:38 -04:00
ouwou
fcb094463b fix RateLimitIndicator picking up updates from other channels 2021-08-23 02:48:56 -04:00
ouwou
b0d609d386 allow viewing all threads from within a thread 2021-08-22 01:36:49 -04:00
ouwou
69e03bbfb7 public/private filter in threads window 2021-08-22 01:21:06 -04:00
ouwou
962cda957d merge master 2021-08-21 03:53:53 -04:00
ouwou
5c63fe9487 hide pin/unpin if no perms, allow viewing pins in dm 2021-08-21 03:48:24 -04:00
ouwou
8059e524f1 fix build 2021-08-20 01:42:59 -04:00
ouwou
2a45d7173a proper member list in threads 2021-08-20 01:13:51 -04:00
ouwou
1fa3e5beac make channel in thread created message clickable 2021-08-17 23:29:48 -04:00
ouwou
2a2f96f9b6 update channel name in THREAD_UPDATE 2021-08-16 02:58:18 -04:00
ouwou
d2f6bd08fc handle archiving via THREAD_UPDATE (sorta) 2021-08-16 02:47:08 -04:00
ouwou
66c7ecf2ab emit thread delete if message req 404's 2021-08-15 02:02:56 -04:00
ouwou
e0db238cf0 removed deleted threads on sync 2021-08-15 01:36:57 -04:00
ouwou
613bb2b7c6 handle thread create via thread_member_update 2021-08-15 00:25:00 -04:00
ouwou
7ffded5b13 rest of view threads window 2021-08-11 03:32:09 -04:00
ouwou
e01110c739 add thread stuff to audit log 2021-08-05 23:50:15 -04:00
ouwou
856674506c better join/leave thread logic 2021-08-05 04:02:47 -04:00
ouwou
41a63df1b1 add temporary row for non-joined threads 2021-08-05 03:32:53 -04:00
ouwou
40897ece3c basic window to view threads 2021-08-04 21:30:32 -04:00
ouwou
a19d214272 basic THREAD_MEMBERS_UPDATE handling for updating channel list 2021-08-02 02:00:03 -04:00
ouwou
06f85c3a2d THREAD_LIST_SYNC 2021-08-02 00:53:08 -04:00
ouwou
6c77e89bbe leave thread via menu 2021-08-02 00:52:37 -04:00
ouwou
22376ba54a handle THREAD_DELETE 2021-07-30 01:32:43 -04:00
ouwou
355ef009c8 Merge branch 'master' into threads 2021-07-30 01:30:54 -04:00
ouwou
b659e3bc91 update identify message 2021-07-30 01:30:00 -04:00
ouwou
014e176e0a handle THREAD_CREATE
also fix compilation
also change channel_create signal to emit ChannelData
2021-07-28 03:34:36 -04:00
ouwou
9d2d13a389 very rudimentary thread support 2021-07-27 22:54:17 -04:00
ouwou
1936f9ab8a handle THREAD_CREATED message type 2021-07-26 02:47:20 -04:00
ouwou
ede2f53ba5 Merge branch 'channels-list' 2021-07-26 02:10:56 -04:00
ouwou
df243a40b5 let channel list manage itself instead of window 2021-07-26 00:56:14 -04:00
ouwou
30391836d0 add missing channel sign 2021-07-26 00:03:36 -04:00
ouwou
ea9dd29721 Merge branch 'resources' (closes #29) 2021-07-25 23:46:44 -04:00
ouwou
5bf48fa6c0 add animated guild icons to channel list 2021-07-23 00:35:33 -04:00
ouwou
96b8b19dd3 only make text channel mentions bold and clickable 2021-07-21 03:33:52 -04:00
ouwou
fdee6c22cf channel list: hide expanders and search 2021-07-21 03:23:45 -04:00
ouwou
4988db95bc select new channel when active channel changes
also automatically fix expander indicator when indirectly expanded
2021-07-21 02:30:46 -04:00
ouwou
a5e5954ae7 fix calculating hoisted color role
also slight optimization + make Snowflake::Invalid a real Snowflake
2021-07-21 01:29:17 -04:00
ouwou
a4d0cd9612 color nsfw channels 2021-07-20 17:55:03 -04:00
ouwou
d20a822fdb tweak text channel rendering 2021-07-19 03:07:27 -04:00
ouwou
0250229e81 less than ideal solution for category expander color 2021-07-19 01:42:55 -04:00
ouwou
8a4283edd1 add menu items 2021-07-18 03:33:16 -04:00
ouwou
e74e6f2342 fix build 2021-07-16 23:46:03 -04:00
ouwou
a30f7346f7 restore expansion cuz gtk behaves annoyingly 2021-07-14 03:14:30 -04:00
ouwou
8c3752ef9f add private channels to list 2021-07-13 20:09:01 -04:00
ouwou
f60e2cd6bd Merge branch 'master' into channels-list 2021-07-12 19:06:00 -04:00
ouwou
ccf7c414be fix ci hopefully 2021-07-12 17:36:06 -04:00
ouwou
f5e78c899a more reliable menu sensitivty/actions 2021-07-11 01:27:47 -04:00
ouwou
41bd2334fa restrict auto-reconnect to abnormal close code 2021-07-10 03:43:26 -04:00
ouwou
b2b55eb06e disconnect action should interrupt reconnect 2021-07-10 03:42:14 -04:00
ouwou
ca3eacbd79 dont use unordered collections (reduce memory a bit) 2021-07-10 03:11:59 -04:00
ouwou
fa87adb4a3 improve handling of socket close (#31) 2021-07-10 02:23:34 -04:00
ouwou
8321cd29d6 pick resources up from ~/.local/share/abaddon instead 2021-07-08 03:32:29 -04:00
ouwou
ecf8fb6a5f fix up channel row selection to work how i want it to 2021-07-06 02:38:27 -04:00
ouwou
9ec52e3473 make ChannelList directly subclass a container 2021-07-05 17:35:53 -04:00
ouwou
3565ec885e try to fix some weird behavior 2021-07-05 17:09:19 -04:00
ouwou
ab2c7bed88 tweak the arrow 2021-07-05 17:04:59 -04:00
ouwou
7b127439ea Merge branch 'master' into channels-list 2021-07-05 16:42:31 -04:00
ouwou
9f66990af2 remove reload settings menu item cuz it doesnt do anything 2021-07-05 01:58:32 -04:00
ouwou
b01a4406fa some more minor tweaking 2021-07-05 01:57:55 -04:00
ouwou
09872cf426 expand categories by default and preserve expansion 2021-07-05 00:10:05 -04:00
ouwou
87d5faf30b some refactorage 2021-07-04 02:36:12 -04:00
ouwou
c154a63967 update guild 2021-07-04 02:21:32 -04:00
ouwou
4102db1eb9 better channel update handling 2021-07-04 02:11:38 -04:00
ouwou
d0d5c655fc handle channel create 2021-07-04 01:39:56 -04:00
ouwou
716627f47d handle update channel 2021-07-03 22:09:53 -04:00
ouwou
f1504eca15 handle channel remove 2021-07-03 21:11:51 -04:00
ouwou
67c944f219 channel list: handle guild create/delete 2021-07-03 19:47:13 -04:00
ouwou
66e2311bf0 initial working better channel list 2021-07-03 19:06:49 -04:00
ouwou
90076cf689 add cmake config file for resource dir 2021-07-02 21:41:05 -04:00
ouwou
0fcd14c089 search priority of css+res to cwd, ~/.config/abaddon, then /usr/share/abaddon 2021-07-01 02:51:00 -04:00
ouwou
cbc65bf766 re-add suppport for static (a)png stickers 2021-07-01 02:03:41 -04:00
ouwou
4b089606ea add ABADDON_CONFIG environment variable 2021-06-30 21:45:26 -04:00
ouwou
f7ac0f2a1e allow config to go in ~/.config/abaddon 2021-06-30 19:21:29 -04:00
ouwou
698ec52d5c try loading resources from share, fallback to cwd 2021-06-30 18:15:03 -04:00
ouwou
220aa6d13a Merge branch 'master' of https://github.com/uowuo/abaddon 2021-06-26 03:18:59 -04:00
ouwou
9388c8926c add an invite link to the readme woowee 2021-06-26 07:18:50 +00:00
ouwou
4d0eb756d2 get rid of something bad 2021-06-26 03:18:29 -04:00
ouwou
eab0e100c4 be more consistent with callbacks 2021-06-25 22:54:34 -04:00
ouwou
989ec06838 change how client determines if verification is needed (#28) 2021-06-24 14:31:50 -04:00
ouwou
6e75c4a95d add pin menu item to messages 2021-06-23 02:24:24 -04:00
ouwou
41d60e5e90 update pins window on pin/unpin 2021-06-23 02:09:07 -04:00
ouwou
7db2675087 fetch pins from store if already requested 2021-06-20 20:32:16 -04:00
ouwou
78f3164063 add unpin to menu, refactor a bit 2021-06-17 01:08:09 -04:00
ouwou
51fdcddaea move chat message menu into the chatlist 2021-06-16 03:53:30 -04:00
ouwou
d527669d39 pins: separate msgs, sort and store 2021-06-15 01:01:29 -04:00
ouwou
e166c83d33 basic pins window 2021-06-11 01:24:23 -04:00
ouwou
efc97aa2b0 reduce db access + refactor 2021-06-10 15:27:32 -04:00
ouwou
c73b08e341 update emojis 2021-06-09 19:01:11 -04:00
ouwou
5de40a4e61 add minsizerel configuration for windows 2021-06-04 22:00:41 -04:00
ouwou
8e58da2daf squelch warning 2021-06-04 03:15:56 -04:00
ouwou
43ea62d444 merge 2021-06-04 02:39:35 -04:00
ouwou
28b33d1b22 fix build(?) 2021-06-04 02:18:43 -04:00
ouwou
be44508316 slight reformat + fix stuff 2021-06-04 01:28:56 -04:00
ouwou
a4aab9bc16 fix potential crash in curl 2021-06-04 00:57:41 -04:00
ouwou
6dca3d9d17 idk 2021-06-04 00:12:35 -04:00
ouwou
7768401a03 bump vcpkg and hope it fixes whatever tf is happening 2021-06-03 22:39:21 -04:00
ouwou
5866836d5d allow overriding of gateway + api url 2021-06-03 19:23:33 -04:00
ouwou
c3b2bbd647 trim whitespace from token input 2021-06-03 17:31:24 -04:00
ouwou
c0c402797f update manage permissions stuff 2021-06-03 17:03:17 -04:00
ouwou
14d0968c92 basic support of per-guild avatars 2021-06-03 03:59:25 -04:00
ouwou
fe9642f1f1 minor css tweaking to fix text rendering 2021-06-03 03:40:44 -04:00
ouwou
79c00c68e5 update twemoji to 13.1 2021-05-28 22:59:55 -04:00
ouwou
c43b39fe70 add some workarounds/fixes for css stuff
adds a separate css file registered with a lower priority to allow for application-wide css while allowing the main css file to override more general selectors
2021-05-28 04:04:06 -04:00
ouwou
a4868e16e9 add custom-built gtk-compatible twemoji TTF 2021-05-26 04:29:01 -04:00
ouwou
91ed1359fc copy fonts folder 2021-05-26 03:35:16 -04:00
ouwou
484e21e693 display user bio in profile window 2021-05-26 02:50:22 -04:00
ouwou
193c2b7912 update .gitignore 2021-05-24 22:39:10 -04:00
ouwou
249d47c5f7 initial font stuff 2021-05-24 22:13:37 -04:00
ouwou
5d8209cf10 add tiny workaround for some compiler issues 2021-05-24 18:29:45 -04:00
ouwou
c75a91d15f add more granular control over emojis 2021-05-24 02:03:42 -04:00
ouwou
b2655260fa pull out chat list into a separate component 2021-05-24 01:42:04 -04:00
ouwou
a1c7d14efa some memory usage optimizations pending proper rewrite 2021-05-21 22:41:42 -04:00
ouwou
acbe03157c add certified moderator user flag 2021-05-19 23:27:22 -04:00
ouwou
72053457a0 optimize curl worker thread a good amount 2021-05-19 18:42:43 -04:00
ouwou
6a15f91a14 set main window to invalid channel when looking at friends 2021-05-14 23:43:37 -04:00
ouwou
a283ab14a2 friends: respect show animations setting 2021-05-14 23:23:01 -04:00
ouwou
1c5f912019 Merge branch 'master' into friends 2021-05-14 03:18:05 -04:00
ouwou
223a185124 clean up some unnecessary stuff 2021-05-14 03:16:52 -04:00
ouwou
f53b9742cb handle stage-related audit log action types 2021-05-14 03:08:27 -04:00
ouwou
cd97c55465 fix potential crash when displaying audit log 2021-05-14 02:44:07 -04:00
ouwou
95eb664641 friends: friends list is now in main content stack 2021-05-14 00:27:58 -04:00
ouwou
858fd8ce62 friends: accept incoming 2021-05-11 04:06:23 -04:00
ouwou
09cb0b07bb fix setting status to invisible 2021-05-10 18:26:39 -04:00
ouwou
06ba3acc93 friends: send friend requests 2021-05-10 02:13:12 -04:00
ouwou
81ae2b3a83 friends: disable friends menu item when disconnected 2021-05-10 00:07:23 -04:00
ouwou
e6a20e5984 add ability to remove relationships 2021-05-09 02:55:35 -04:00
ouwou
ae3b256746 friends: handle RELATIONSHIP_ADD events 2021-05-09 02:12:15 -04:00
ouwou
bf26e49f2c Merge branch 'master' into friends 2021-05-09 01:22:37 -04:00
ouwou
925405826a handle RELATIONSHIP_ADD 2021-05-09 01:20:48 -04:00
ouwou
a781cc76a0 friends: handle RELATIONSHIP_REMOVE events 2021-05-08 23:49:27 -04:00
ouwou
abd2b9c71e Merge branch 'master' into friends 2021-05-08 23:37:52 -04:00
ouwou
469053a144 handle RELATIONSHIP_REMOVE 2021-05-08 23:35:15 -04:00
ouwou
dd7852014f dont allow sending empty messages (fixes #27)
also add a nullptr check just in case
2021-05-07 04:09:07 -04:00
ouwou
78d3c5b679 friends: bold name & fix missing method 2021-05-07 03:55:14 -04:00
ouwou
e44484cc26 friends: handle presence updates 2021-05-07 03:04:05 -04:00
ouwou
19a2aed020 Merge branch 'master' into friends 2021-05-07 02:50:09 -04:00
ouwou
712e78b0c8 send user data over signal_presence_update 2021-05-07 02:49:59 -04:00
ouwou
d679b1af76 friends: accept menu item for incoming 2021-05-07 02:32:30 -04:00
ouwou
e2736e5806 friends: update labels 2021-05-07 02:26:17 -04:00
ouwou
762f501f2c friends: avatars 2021-05-07 02:17:25 -04:00
ouwou
86ab14e5ec start friends list 2021-05-07 01:57:08 -04:00
124 changed files with 7754 additions and 2052 deletions

View File

@@ -8,7 +8,7 @@ jobs:
runs-on: windows-latest
strategy:
matrix:
buildtype: [Debug, RelWithDebInfo]
buildtype: [Debug, RelWithDebInfo, MinSizeRel]
steps:
- uses: actions/checkout@v1
with:
@@ -43,6 +43,7 @@ jobs:
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"
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"

2
.gitignore vendored
View File

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

View File

@@ -2,6 +2,8 @@ cmake_minimum_required(VERSION 3.16)
project(abaddon)
set(ABADDON_RESOURCE_DIR "/usr/share/abaddon" CACHE PATH "Fallback directory for resources on Linux")
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${CMAKE_SOURCE_DIR}/cmake/")
@@ -30,8 +32,13 @@ endif()
if(WIN32)
add_compile_definitions(_CRT_SECURE_NO_WARNINGS)
add_compile_definitions(NOMINMAX)
find_package(Fontconfig REQUIRED)
link_libraries(${Fontconfig_LIBRARIES})
endif()
configure_file(${PROJECT_SOURCE_DIR}/config.h.in ${PROJECT_BINARY_DIR}/config.h)
file(GLOB ABADDON_SOURCES
"*.h"
"*.hpp"
@@ -51,6 +58,7 @@ file(GLOB ABADDON_SOURCES
)
add_executable(abaddon ${ABADDON_SOURCES})
target_include_directories(abaddon PUBLIC ${PROJECT_BINARY_DIR})
target_include_directories(abaddon PUBLIC ${GTKMM_INCLUDE_DIRS})
target_include_directories(abaddon PUBLIC ${ZLIB_INCLUDE_DIRS})
target_include_directories(abaddon PUBLIC ${SQLite3_INCLUDE_DIRS})

View File

@@ -4,6 +4,8 @@ Alternative Discord client made in C++ with GTK
<img src="/.readme/s3.png">
[😎Discord Server](https://discord.gg/wkCU3vuzG5)
Current features:
* Not Electron
* Handles most types of chat messages including embeds, images, and replies
@@ -52,7 +54,11 @@ Or, do steps 1 and 2, and open CMakeLists.txt in Visual Studio if `vcpkg integra
- MacOS: [here](https://nightly.link/uowuo/abaddon/workflows/ci/master/build-macos-RelWithDebInfo.zip) unsigned, unpackaged, requires gtkmm3 (e.g. from homebrew)
- Linux: [here](https://nightly.link/uowuo/abaddon/workflows/ci/master/build-linux-MinSizeRel.zip) unpackaged (for now), requires gtkmm3. built on Ubuntu 18.04 + gcc9
⚠️ Make sure you start from the directory where `css` and `res` are or else stuff will be broken
⚠️ If you use Windows, make sure to start from the directory containing `css` and `res`
If you don't use Windows, `css` and `res` can be loaded from `/usr/share/abaddon`
`abaddon.ini` will also be automatically used if located at `~/.config/abaddon/abaddon.ini` and there is no `abaddon.ini` in the working directory
#### Dependencies:
* [gtkmm](https://www.gtkmm.org/en/)
@@ -184,10 +190,21 @@ For example, memory_db would be set by adding `memory_db = true` under the line
#### gui
* member_list_discriminator (true or false, default true) - show user discriminators in the member list
* emojis (true or false, default true) - resolve unicode and custom emojis to images. this needs to be false to allow GTK to render emojis by itself
* stock_emojis (true or false, default true) - allow abaddon to substitute unicode emojis with images from emojis.bin, must be false to allow GTK to render emojis itself
* custom_emojis (true or false, default true) - download and use custom Discord emojis
* css (string) - path to the main CSS file
* animations (true or false, default true) - use animated images where available (e.g. server icons, emojis, avatars). false means static images will be used
* animated_guild_hover_only (true or false, default true) - only animate guild icons when the guild is being hovered over
* owner_crown (true or false, default true) - show a crown next to the owner
* 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
#### misc
#### 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
### Environment variables
* ABADDON_NO_FC (Windows only) - don't use custom font config
* ABADDON_CONFIG - change path of configuration file to use. relative to cwd or can be absolute

View File

@@ -2,6 +2,7 @@
#include <memory>
#include <string>
#include <algorithm>
#include "platform.hpp"
#include "discord/discord.hpp"
#include "dialogs/token.hpp"
#include "dialogs/editmessage.hpp"
@@ -13,14 +14,16 @@
#include "abaddon.hpp"
#include "windows/guildsettingswindow.hpp"
#include "windows/profilewindow.hpp"
#include "windows/pinnedwindow.hpp"
#include "windows/threadswindow.hpp"
#ifdef _WIN32
#pragma comment(lib, "crypt32.lib")
#endif
Abaddon::Abaddon()
: m_settings("abaddon.ini")
, m_emojis("res/emojis.bin")
: m_settings(Platform::FindConfigFile())
, m_emojis(GetResPath("/emojis.bin"))
, m_discord(m_settings.GetUseMemoryDB()) { // stupid but easy
LoadFromSettings();
@@ -33,15 +36,11 @@ Abaddon::Abaddon()
m_discord.signal_message_delete().connect(sigc::mem_fun(*this, &Abaddon::DiscordOnMessageDelete));
m_discord.signal_message_update().connect(sigc::mem_fun(*this, &Abaddon::DiscordOnMessageUpdate));
m_discord.signal_guild_member_list_update().connect(sigc::mem_fun(*this, &Abaddon::DiscordOnGuildMemberListUpdate));
m_discord.signal_guild_create().connect(sigc::mem_fun(*this, &Abaddon::DiscordOnGuildCreate));
m_discord.signal_guild_delete().connect(sigc::mem_fun(*this, &Abaddon::DiscordOnGuildDelete));
m_discord.signal_channel_delete().connect(sigc::mem_fun(*this, &Abaddon::DiscordOnChannelDelete));
m_discord.signal_channel_update().connect(sigc::mem_fun(*this, &Abaddon::DiscordOnChannelUpdate));
m_discord.signal_channel_create().connect(sigc::mem_fun(*this, &Abaddon::DiscordOnChannelCreate));
m_discord.signal_guild_update().connect(sigc::mem_fun(*this, &Abaddon::DiscordOnGuildUpdate));
m_discord.signal_thread_member_list_update().connect(sigc::mem_fun(*this, &Abaddon::DiscordOnThreadMemberListUpdate));
m_discord.signal_reaction_add().connect(sigc::mem_fun(*this, &Abaddon::DiscordOnReactionAdd));
m_discord.signal_reaction_remove().connect(sigc::mem_fun(*this, &Abaddon::DiscordOnReactionRemove));
m_discord.signal_guild_join_request_create().connect(sigc::mem_fun(*this, &Abaddon::DiscordOnGuildJoinRequestCreate));
m_discord.signal_thread_update().connect(sigc::mem_fun(*this, &Abaddon::DiscordOnThreadUpdate));
m_discord.signal_message_sent().connect(sigc::mem_fun(*this, &Abaddon::DiscordOnMessageSent));
m_discord.signal_disconnected().connect(sigc::mem_fun(*this, &Abaddon::DiscordOnDisconnect));
if (m_settings.GetPrefetch())
@@ -68,7 +67,6 @@ Abaddon &Abaddon::Get() {
int Abaddon::StartGTK() {
m_gtk_app = Gtk::Application::create("com.github.uowuo.abaddon");
// tmp css stuff
m_css_provider = Gtk::CssProvider::create();
m_css_provider->signal_parsing_error().connect([this](const Glib::RefPtr<const Gtk::CssSection> &section, const Glib::Error &error) {
Gtk::MessageDialog dlg(*m_main_window, "css failed parsing (" + error.what() + ")", false, Gtk::MESSAGE_ERROR, Gtk::BUTTONS_OK, true);
@@ -76,6 +74,13 @@ int Abaddon::StartGTK() {
dlg.run();
});
m_css_low_provider = Gtk::CssProvider::create();
m_css_low_provider->signal_parsing_error().connect([this](const Glib::RefPtr<const Gtk::CssSection> &section, const Glib::Error &error) {
Gtk::MessageDialog dlg(*m_main_window, "low-priority css failed parsing (" + error.what() + ")", false, Gtk::MESSAGE_ERROR, Gtk::BUTTONS_OK, true);
dlg.set_position(Gtk::WIN_POS_CENTER);
dlg.run();
});
m_main_window = std::make_unique<MainWindow>();
m_main_window->set_title(APP_TITLE);
m_main_window->UpdateComponents();
@@ -90,16 +95,14 @@ int Abaddon::StartGTK() {
m_main_window->signal_action_reload_css().connect(sigc::mem_fun(*this, &Abaddon::ActionReloadCSS));
m_main_window->signal_action_join_guild().connect(sigc::mem_fun(*this, &Abaddon::ActionJoinGuildDialog));
m_main_window->signal_action_set_status().connect(sigc::mem_fun(*this, &Abaddon::ActionSetStatus));
m_main_window->signal_action_reload_settings().connect(sigc::mem_fun(*this, &Abaddon::ActionReloadSettings));
m_main_window->signal_action_add_recipient().connect(sigc::mem_fun(*this, &Abaddon::ActionAddRecipient));
m_main_window->signal_action_show_user_menu().connect(sigc::mem_fun(*this, &Abaddon::ShowUserMenu));
m_main_window->signal_action_view_pins().connect(sigc::mem_fun(*this, &Abaddon::ActionViewPins));
m_main_window->signal_action_view_threads().connect(sigc::mem_fun(*this, &Abaddon::ActionViewThreads));
m_main_window->GetChannelList()->signal_action_channel_item_select().connect(sigc::mem_fun(*this, &Abaddon::ActionChannelOpened));
m_main_window->GetChannelList()->signal_action_guild_leave().connect(sigc::mem_fun(*this, &Abaddon::ActionLeaveGuild));
m_main_window->GetChannelList()->signal_action_guild_settings().connect(sigc::mem_fun(*this, &Abaddon::ActionGuildSettings));
m_main_window->GetChatWindow()->signal_action_message_delete().connect(sigc::mem_fun(*this, &Abaddon::ActionChatDeleteMessage));
m_main_window->GetChatWindow()->signal_action_message_edit().connect(sigc::mem_fun(*this, &Abaddon::ActionChatEditMessage));
m_main_window->GetChatWindow()->signal_action_chat_submit().connect(sigc::mem_fun(*this, &Abaddon::ActionChatInputSubmit));
m_main_window->GetChatWindow()->signal_action_chat_load_history().connect(sigc::mem_fun(*this, &Abaddon::ActionChatLoadHistory));
@@ -176,7 +179,7 @@ void Abaddon::DiscordOnReady() {
}
void Abaddon::DiscordOnMessageCreate(const Message &message) {
m_main_window->UpdateChatNewMessage(message.ID); // todo ill fix you later :^)
m_main_window->UpdateChatNewMessage(message);
}
void Abaddon::DiscordOnMessageDelete(Snowflake id, Snowflake channel_id) {
@@ -191,28 +194,8 @@ void Abaddon::DiscordOnGuildMemberListUpdate(Snowflake guild_id) {
m_main_window->UpdateMembers();
}
void Abaddon::DiscordOnGuildCreate(const GuildData &guild) {
m_main_window->UpdateChannelsNewGuild(guild.ID);
}
void Abaddon::DiscordOnGuildDelete(Snowflake guild_id) {
m_main_window->UpdateChannelsRemoveGuild(guild_id);
}
void Abaddon::DiscordOnChannelDelete(Snowflake channel_id) {
m_main_window->UpdateChannelsRemoveChannel(channel_id);
}
void Abaddon::DiscordOnChannelUpdate(Snowflake channel_id) {
m_main_window->UpdateChannelsUpdateChannel(channel_id);
}
void Abaddon::DiscordOnChannelCreate(Snowflake channel_id) {
m_main_window->UpdateChannelsCreateChannel(channel_id);
}
void Abaddon::DiscordOnGuildUpdate(Snowflake guild_id) {
m_main_window->UpdateChannelsUpdateGuild(guild_id);
void Abaddon::DiscordOnThreadMemberListUpdate(const ThreadMemberListUpdateData &data) {
m_main_window->UpdateMembers();
}
void Abaddon::DiscordOnReactionAdd(Snowflake message_id, const Glib::ustring &param) {
@@ -231,7 +214,7 @@ void Abaddon::DiscordOnGuildJoinRequestCreate(const GuildJoinRequestCreateData &
}
void Abaddon::DiscordOnMessageSent(const Message &data) {
m_main_window->UpdateChatNewMessage(data.ID);
m_main_window->UpdateChatNewMessage(data);
}
void Abaddon::DiscordOnDisconnect(bool is_reconnecting, GatewayCloseCode close_code) {
@@ -255,6 +238,15 @@ void Abaddon::DiscordOnDisconnect(bool is_reconnecting, GatewayCloseCode close_c
}
}
void Abaddon::DiscordOnThreadUpdate(const ThreadUpdateData &data) {
if (data.Thread.ID == m_main_window->GetChatActiveChannel()) {
if (data.Thread.ThreadMetadata->IsArchived)
m_main_window->GetChatWindow()->SetTopic("This thread is archived. Sending a message will unarchive it");
else
m_main_window->GetChatWindow()->SetTopic("");
}
}
const SettingsManager &Abaddon::GetSettings() const {
return m_settings;
}
@@ -320,8 +312,8 @@ void Abaddon::ShowUserMenu(const GdkEvent *event, Snowflake id, Snowflake guild_
void Abaddon::ShowGuildVerificationGateDialog(Snowflake guild_id) {
VerificationGateDialog dlg(*m_main_window, guild_id);
if (dlg.run() == Gtk::RESPONSE_OK) {
const auto cb = [this](bool success) {
if (!success) {
const auto cb = [this](DiscordError code) {
if (code != DiscordError::NONE) {
Gtk::MessageDialog dlg(*m_main_window, "Failed to accept the verification gate.", false, Gtk::MESSAGE_ERROR, Gtk::BUTTONS_OK, true);
dlg.set_position(Gtk::WIN_POS_CENTER);
dlg.run();
@@ -405,8 +397,8 @@ void Abaddon::on_user_menu_open_dm() {
if (existing.has_value())
ActionChannelOpened(*existing);
else
m_discord.CreateDM(m_shown_user_menu_id, [this](bool success, Snowflake channel_id) {
if (success) {
m_discord.CreateDM(m_shown_user_menu_id, [this](DiscordError code, Snowflake channel_id) {
if (code == DiscordError::NONE) {
// give the gateway a little window to send CHANNEL_CREATE
auto cb = [this, channel_id] {
ActionChannelOpened(channel_id);
@@ -420,6 +412,24 @@ void Abaddon::on_user_menu_remove_recipient() {
m_discord.RemoveGroupDMRecipient(m_main_window->GetChatActiveChannel(), m_shown_user_menu_id);
}
std::string Abaddon::GetCSSPath() {
const static auto path = Platform::FindResourceFolder() + "/css";
return path;
}
std::string Abaddon::GetResPath() {
const static auto path = Platform::FindResourceFolder() + "/res";
return path;
}
std::string Abaddon::GetCSSPath(const std::string &path) {
return GetCSSPath() + path;
}
std::string Abaddon::GetResPath(const std::string &path) {
return GetResPath() + path;
}
void Abaddon::ActionConnect() {
if (!m_discord.IsStarted())
StartDiscord();
@@ -427,8 +437,7 @@ void Abaddon::ActionConnect() {
}
void Abaddon::ActionDisconnect() {
if (m_discord.IsStarted())
StopDiscord();
StopDiscord();
}
void Abaddon::ActionSetToken() {
@@ -454,7 +463,10 @@ void Abaddon::ActionJoinGuildDialog() {
void Abaddon::ActionChannelOpened(Snowflake id) {
if (id == m_main_window->GetChatActiveChannel()) return;
m_main_window->GetChatWindow()->SetTopic("");
const auto channel = m_discord.GetChannel(id);
if (!channel.has_value()) return;
if (channel->Type == ChannelType::GUILD_TEXT || channel->Type == ChannelType::GUILD_NEWS)
m_main_window->set_title(std::string(APP_TITLE) + " - #" + *channel->Name);
else {
@@ -470,7 +482,7 @@ void Abaddon::ActionChannelOpened(Snowflake id) {
}
m_main_window->UpdateChatActiveChannel(id);
if (m_channels_requested.find(id) == m_channels_requested.end()) {
m_discord.FetchMessagesInChannel(id, [this, id](const std::vector<Snowflake> &msgs) {
m_discord.FetchMessagesInChannel(id, [this, id](const std::vector<Message> &msgs) {
m_main_window->UpdateChatWindowContents();
m_channels_requested.insert(id);
});
@@ -478,11 +490,14 @@ void Abaddon::ActionChannelOpened(Snowflake id) {
m_main_window->UpdateChatWindowContents();
}
if (channel->Type != ChannelType::DM && channel->Type != ChannelType::GROUP_DM) {
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);
const auto request = m_discord.GetGuildApplication(*channel->GuildID);
if (request.has_value() && request->ApplicationStatus == GuildApplicationStatus::STARTED)
if (m_discord.IsVerificationRequired(*channel->GuildID))
ShowGuildVerificationGateDialog(*channel->GuildID);
}
}
@@ -495,20 +510,23 @@ void Abaddon::ActionChatLoadHistory(Snowflake id) {
return;
Snowflake before_id = m_main_window->GetChatOldestListedMessage();
auto knownset = m_discord.GetMessagesForChannel(id);
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);
if (distance >= 50) {
m_main_window->UpdateChatPrependHistory(std::vector<Snowflake>(knownvec.begin() + distance - 50, knownvec.begin() + distance));
std::vector<Message> msgs;
for (auto it = knownvec.begin() + distance - 50; it != knownvec.begin() + distance; it++)
msgs.push_back(*m_discord.GetMessage(*it));
m_main_window->UpdateChatPrependHistory(msgs);
return;
}
m_channels_history_loading.insert(id);
m_discord.FetchMessagesInChannelBefore(id, before_id, [this, id](const std::vector<Snowflake> &msgs) {
m_discord.FetchMessagesInChannelBefore(id, before_id, [this, id](const std::vector<Message> &msgs) {
m_channels_history_loading.erase(id);
if (msgs.size() == 0) {
@@ -528,12 +546,9 @@ void Abaddon::ActionChatInputSubmit(std::string msg, Snowflake channel, Snowflak
m_discord.SendChatMessage(msg, channel);
}
void Abaddon::ActionChatDeleteMessage(Snowflake channel_id, Snowflake id) {
m_discord.DeleteMessage(channel_id, id);
}
void Abaddon::ActionChatEditMessage(Snowflake channel_id, Snowflake id) {
const auto msg = m_discord.GetMessage(id);
if (!msg.has_value()) return;
EditMessageDialog dlg(*m_main_window);
dlg.SetContent(msg->Content);
auto response = dlg.run();
@@ -617,21 +632,41 @@ void Abaddon::ActionAddRecipient(Snowflake channel_id) {
}
}
void Abaddon::ActionViewPins(Snowflake channel_id) {
const auto data = m_discord.GetChannel(channel_id);
if (!data.has_value()) return;
auto window = new PinnedWindow(*data);
ManageHeapWindow(window);
window->show();
}
void Abaddon::ActionViewThreads(Snowflake channel_id) {
auto data = m_discord.GetChannel(channel_id);
if (!data.has_value()) return;
if (data->IsThread()) {
data = m_discord.GetChannel(*data->ParentID);
if (!data.has_value()) return;
}
auto window = new ThreadsWindow(*data);
ManageHeapWindow(window);
window->show();
}
bool Abaddon::ShowConfirm(const Glib::ustring &prompt, Gtk::Window *window) {
ConfirmDialog dlg(window != nullptr ? *window : *m_main_window);
dlg.SetConfirmText(prompt);
return dlg.run() == Gtk::RESPONSE_OK;
}
void Abaddon::ActionReloadSettings() {
m_settings.Reload();
}
void Abaddon::ActionReloadCSS() {
try {
Gtk::StyleContext::remove_provider_for_screen(Gdk::Screen::get_default(), m_css_provider);
m_css_provider->load_from_path(m_settings.GetMainCSS());
m_css_provider->load_from_path(GetCSSPath("/" + m_settings.GetMainCSS()));
Gtk::StyleContext::add_provider_for_screen(Gdk::Screen::get_default(), m_css_provider, GTK_STYLE_PROVIDER_PRIORITY_APPLICATION);
Gtk::StyleContext::remove_provider_for_screen(Gdk::Screen::get_default(), m_css_low_provider);
m_css_low_provider->load_from_path(GetCSSPath("/application-low-priority.css"));
Gtk::StyleContext::add_provider_for_screen(Gdk::Screen::get_default(), m_css_low_provider, GTK_STYLE_PROVIDER_PRIORITY_APPLICATION - 1);
} catch (Glib::Error &e) {
Gtk::MessageDialog dlg(*m_main_window, "css failed to load (" + e.what() + ")", false, Gtk::MESSAGE_ERROR, Gtk::BUTTONS_OK, true);
dlg.set_position(Gtk::WIN_POS_CENTER);
@@ -648,6 +683,8 @@ EmojiResource &Abaddon::GetEmojis() {
}
int main(int argc, char **argv) {
if (std::getenv("ABADDON_NO_FC") == nullptr)
Platform::SetupFonts();
#if defined(_WIN32) && defined(_MSC_VER)
TCHAR buf[2] { 0 };
GetEnvironmentVariableA("GTK_CSD", buf, sizeof(buf));

View File

@@ -36,7 +36,6 @@ public:
void ActionChannelOpened(Snowflake id);
void ActionChatInputSubmit(std::string msg, Snowflake channel, Snowflake referenced_message);
void ActionChatLoadHistory(Snowflake id);
void ActionChatDeleteMessage(Snowflake channel_id, Snowflake id);
void ActionChatEditMessage(Snowflake channel_id, Snowflake id);
void ActionInsertMention(Snowflake id);
void ActionLeaveGuild(Snowflake id);
@@ -47,10 +46,11 @@ public:
void ActionReactionRemove(Snowflake id, const Glib::ustring &param);
void ActionGuildSettings(Snowflake id);
void ActionAddRecipient(Snowflake channel_id);
void ActionViewPins(Snowflake channel_id);
void ActionViewThreads(Snowflake channel_id);
bool ShowConfirm(const Glib::ustring &prompt, Gtk::Window *window = nullptr);
void ActionReloadSettings();
void ActionReloadCSS();
ImageManager &GetImageManager();
@@ -66,17 +66,13 @@ public:
void DiscordOnMessageDelete(Snowflake id, Snowflake channel_id);
void DiscordOnMessageUpdate(Snowflake id, Snowflake channel_id);
void DiscordOnGuildMemberListUpdate(Snowflake guild_id);
void DiscordOnGuildCreate(const GuildData &guild);
void DiscordOnGuildDelete(Snowflake guild_id);
void DiscordOnChannelDelete(Snowflake channel_id);
void DiscordOnChannelUpdate(Snowflake channel_id);
void DiscordOnChannelCreate(Snowflake channel_id);
void DiscordOnGuildUpdate(Snowflake guild_id);
void DiscordOnThreadMemberListUpdate(const ThreadMemberListUpdateData &data);
void DiscordOnReactionAdd(Snowflake message_id, const Glib::ustring &param);
void DiscordOnReactionRemove(Snowflake message_id, const Glib::ustring &param);
void DiscordOnGuildJoinRequestCreate(const GuildJoinRequestCreateData &data);
void DiscordOnMessageSent(const Message &data);
void DiscordOnDisconnect(bool is_reconnecting, GatewayCloseCode close_code);
void DiscordOnThreadUpdate(const ThreadUpdateData &data);
const SettingsManager &GetSettings() const;
@@ -84,13 +80,18 @@ public:
void ShowUserMenu(const GdkEvent *event, Snowflake id, Snowflake guild_id);
void ManageHeapWindow(Gtk::Window *window);
static std::string GetCSSPath();
static std::string GetResPath();
static std::string GetCSSPath(const std::string &path);
static std::string GetResPath(const std::string &path);
protected:
void ShowGuildVerificationGateDialog(Snowflake guild_id);
void SetupUserMenu();
void ManageHeapWindow(Gtk::Window *window);
Snowflake m_shown_user_menu_id;
Snowflake m_shown_user_menu_guild_id;
@@ -128,5 +129,6 @@ private:
mutable std::mutex m_mutex;
Glib::RefPtr<Gtk::Application> m_gtk_app;
Glib::RefPtr<Gtk::CssProvider> m_css_provider;
std::unique_ptr<MainWindow> m_main_window; // wah wah cant create a gtkstylecontext fuck you
Glib::RefPtr<Gtk::CssProvider> m_css_low_provider; // registered with a lower priority to allow better customization
std::unique_ptr<MainWindow> m_main_window; // wah wah cant create a gtkstylecontext fuck you
};

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -8,172 +8,226 @@
#include <sigc++/sigc++.h>
#include "../discord/discord.hpp"
static const constexpr int ChannelEmojiSize = 16;
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 ChannelListRow : public Gtk::ListBoxRow {
public:
bool IsUserCollapsed;
Snowflake ID;
std::unordered_set<ChannelListRow *> Children;
ChannelListRow *Parent = nullptr;
enum class RenderType : uint8_t {
Guild,
Category,
TextChannel,
Thread,
virtual void Collapse();
virtual void Expand();
static void MakeReadOnly(Gtk::TextView *tv);
DMHeader,
DM,
};
class ChannelListRowDMHeader : public ChannelListRow {
class CellRendererChannels : public Gtk::CellRenderer {
public:
ChannelListRowDMHeader();
CellRendererChannels();
virtual ~CellRendererChannels();
Glib::PropertyProxy<RenderType> property_type();
Glib::PropertyProxy<Glib::ustring> property_name();
Glib::PropertyProxy<Glib::RefPtr<Gdk::Pixbuf>> property_icon();
Glib::PropertyProxy<Glib::RefPtr<Gdk::PixbufAnimation>> property_icon_animation();
Glib::PropertyProxy<bool> property_expanded();
Glib::PropertyProxy<bool> property_nsfw();
protected:
Gtk::EventBox *m_ev;
Gtk::Box *m_box;
Gtk::Label *m_lbl;
};
void get_preferred_width_vfunc(Gtk::Widget &widget, int &minimum_width, int &natural_width) const override;
void get_preferred_width_for_height_vfunc(Gtk::Widget &widget, int height, int &minimum_width, int &natural_width) const override;
void get_preferred_height_vfunc(Gtk::Widget &widget, int &minimum_height, int &natural_height) const override;
void get_preferred_height_for_width_vfunc(Gtk::Widget &widget, int width, int &minimum_height, int &natural_height) const override;
void render_vfunc(const Cairo::RefPtr<Cairo::Context> &cr,
Gtk::Widget &widget,
const Gdk::Rectangle &background_area,
const Gdk::Rectangle &cell_area,
Gtk::CellRendererState flags) override;
class StatusIndicator;
class ChannelListRowDMChannel : public ChannelListRow {
public:
ChannelListRowDMChannel(const ChannelData *data);
// guild functions
void get_preferred_width_vfunc_guild(Gtk::Widget &widget, int &minimum_width, int &natural_width) const;
void get_preferred_width_for_height_vfunc_guild(Gtk::Widget &widget, int height, int &minimum_width, int &natural_width) const;
void get_preferred_height_vfunc_guild(Gtk::Widget &widget, int &minimum_height, int &natural_height) const;
void get_preferred_height_for_width_vfunc_guild(Gtk::Widget &widget, int width, int &minimum_height, int &natural_height) const;
void 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);
protected:
Gtk::EventBox *m_ev;
Gtk::Box *m_box;
StatusIndicator *m_status = nullptr;
Gtk::TextView *m_lbl;
Gtk::Image *m_icon = nullptr;
// category
void get_preferred_width_vfunc_category(Gtk::Widget &widget, int &minimum_width, int &natural_width) const;
void get_preferred_width_for_height_vfunc_category(Gtk::Widget &widget, int height, int &minimum_width, int &natural_width) const;
void get_preferred_height_vfunc_category(Gtk::Widget &widget, int &minimum_height, int &natural_height) const;
void get_preferred_height_for_width_vfunc_category(Gtk::Widget &widget, int width, int &minimum_height, int &natural_height) const;
void 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);
Gtk::Menu m_menu;
Gtk::MenuItem *m_menu_close; // leave if group
Gtk::MenuItem *m_menu_copy_id;
};
// text channel
void get_preferred_width_vfunc_channel(Gtk::Widget &widget, int &minimum_width, int &natural_width) const;
void get_preferred_width_for_height_vfunc_channel(Gtk::Widget &widget, int height, int &minimum_width, int &natural_width) const;
void get_preferred_height_vfunc_channel(Gtk::Widget &widget, int &minimum_height, int &natural_height) const;
void get_preferred_height_for_width_vfunc_channel(Gtk::Widget &widget, int width, int &minimum_height, int &natural_height) const;
void 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);
class ChannelListRowGuild : public ChannelListRow {
public:
ChannelListRowGuild(const GuildData *data);
// thread
void get_preferred_width_vfunc_thread(Gtk::Widget &widget, int &minimum_width, int &natural_width) const;
void get_preferred_width_for_height_vfunc_thread(Gtk::Widget &widget, int height, int &minimum_width, int &natural_width) const;
void get_preferred_height_vfunc_thread(Gtk::Widget &widget, int &minimum_height, int &natural_height) const;
void get_preferred_height_for_width_vfunc_thread(Gtk::Widget &widget, int width, int &minimum_height, int &natural_height) const;
void 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);
int GuildIndex;
// dm header
void get_preferred_width_vfunc_dmheader(Gtk::Widget &widget, int &minimum_width, int &natural_width) const;
void get_preferred_width_for_height_vfunc_dmheader(Gtk::Widget &widget, int height, int &minimum_width, int &natural_width) const;
void get_preferred_height_vfunc_dmheader(Gtk::Widget &widget, int &minimum_height, int &natural_height) const;
void get_preferred_height_for_width_vfunc_dmheader(Gtk::Widget &widget, int width, int &minimum_height, int &natural_height) const;
void 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);
protected:
Gtk::EventBox *m_ev;
Gtk::Box *m_box;
Gtk::TextView *m_lbl;
Gtk::Image *m_icon;
Gtk::Menu m_menu;
Gtk::MenuItem *m_menu_copyid;
Gtk::MenuItem *m_menu_leave;
Gtk::MenuItem *m_menu_settings;
// dm
void get_preferred_width_vfunc_dm(Gtk::Widget &widget, int &minimum_width, int &natural_width) const;
void get_preferred_width_for_height_vfunc_dm(Gtk::Widget &widget, int height, int &minimum_width, int &natural_width) const;
void get_preferred_height_vfunc_dm(Gtk::Widget &widget, int &minimum_height, int &natural_height) const;
void get_preferred_height_for_width_vfunc_dm(Gtk::Widget &widget, int width, int &minimum_height, int &natural_height) const;
void 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);
private:
typedef sigc::signal<void> type_signal_copy_id;
typedef sigc::signal<void> type_signal_leave;
typedef sigc::signal<void> type_signal_settings;
Gtk::CellRendererText m_renderer_text;
type_signal_copy_id m_signal_copy_id;
type_signal_leave m_signal_leave;
type_signal_settings m_signal_settings;
Glib::Property<RenderType> m_property_type; // all
Glib::Property<Glib::ustring> m_property_name; // all
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
Glib::Property<bool> m_property_nsfw; // channel
public:
type_signal_copy_id signal_copy_id();
type_signal_leave signal_leave();
type_signal_settings signal_settings();
// same pitfalls as in https://github.com/uowuo/abaddon/blob/60404783bd4ce9be26233fe66fc3a74475d9eaa3/components/cellrendererpixbufanimation.hpp#L32-L39
// this will manifest though since guild icons can change
// 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 ChannelListRowCategory : public ChannelListRow {
public:
ChannelListRowCategory(const ChannelData *data);
virtual void Collapse();
virtual void Expand();
protected:
Gtk::EventBox *m_ev;
Gtk::Box *m_box;
Gtk::TextView *m_lbl;
Gtk::Arrow *m_arrow;
Gtk::Menu m_menu;
Gtk::MenuItem *m_menu_copyid;
private:
typedef sigc::signal<void> type_signal_copy_id;
type_signal_copy_id m_signal_copy_id;
public:
type_signal_copy_id signal_copy_id();
};
class ChannelListRowChannel : public ChannelListRow {
public:
ChannelListRowChannel(const ChannelData *data);
protected:
Gtk::EventBox *m_ev;
Gtk::Box *m_box;
Gtk::TextView *m_lbl;
Gtk::Menu m_menu;
Gtk::MenuItem *m_menu_copyid;
private:
typedef sigc::signal<void> type_signal_copy_id;
type_signal_copy_id m_signal_copy_id;
public:
type_signal_copy_id signal_copy_id();
};
class ChannelList {
class ChannelList : public Gtk::ScrolledWindow {
public:
ChannelList();
Gtk::Widget *GetRoot() const;
void UpdateListing();
void UpdateNewGuild(Snowflake id);
void UpdateRemoveGuild(Snowflake id);
void UpdateRemoveChannel(Snowflake id);
void UpdateChannel(Snowflake id);
void UpdateCreateDMChannel(Snowflake id);
void UpdateCreateChannel(Snowflake id);
void UpdateGuild(Snowflake id);
void UpdateListing();
void SetActiveChannel(Snowflake id);
protected:
Gtk::ListBox *m_list;
Gtk::ScrolledWindow *m_main;
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);
ChannelListRowDMHeader *m_dm_header_row = nullptr;
void OnThreadJoined(Snowflake id);
void OnThreadRemoved(Snowflake id);
void OnThreadDelete(const ThreadDeleteData &data);
void OnThreadUpdate(const ThreadUpdateData &data);
void OnThreadListSync(const ThreadListSyncData &data);
void CollapseRow(ChannelListRow *row);
void ExpandRow(ChannelListRow *row);
void DeleteRow(ChannelListRow *row);
Gtk::TreeView m_view;
void UpdateChannelCategory(Snowflake id);
class ModelColumns : public Gtk::TreeModel::ColumnRecord {
public:
ModelColumns();
void on_row_activated(Gtk::ListBoxRow *row);
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;
};
int m_guild_count;
void OnMenuCopyID(Snowflake id);
void OnGuildMenuLeave(Snowflake id);
void OnGuildMenuSettings(Snowflake id);
ModelColumns m_columns;
Glib::RefPtr<Gtk::TreeStore> m_model;
Gtk::Menu m_channel_menu;
Gtk::MenuItem *m_channel_menu_copyid;
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);
// i would use one map but in really old guilds there can be a channel w/ same id as the guild so this hacky shit has to do
std::unordered_map<Snowflake, ChannelListRow *> m_guild_id_to_row;
std::unordered_map<Snowflake, ChannelListRow *> m_id_to_row;
void UpdateChannelCategory(const ChannelData &channel);
void InsertGuildAt(Snowflake id, int pos);
// 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 CheckBumpDM(Snowflake channel_id);
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;

View File

@@ -21,8 +21,9 @@ ChatInputIndicator::ChatInputIndicator()
m_label.show();
// try loading gif
if (!std::filesystem::exists("./res/typing_indicator.gif")) return;
auto gif_data = ReadWholeFile("./res/typing_indicator.gif");
const static auto path = Abaddon::GetResPath("/typing_indicator.gif");
if (!std::filesystem::exists(path)) return;
auto gif_data = ReadWholeFile(path);
auto loader = Gdk::PixbufLoader::create();
loader->signal_size_prepared().connect([&](int inw, int inh) {
int w, h;

361
components/chatlist.cpp Normal file
View File

@@ -0,0 +1,361 @@
#include "chatmessage.hpp"
#include "chatlist.hpp"
#include "../abaddon.hpp"
#include "../constants.hpp"
ChatList::ChatList() {
m_list.get_style_context()->add_class("messages");
set_can_focus(false);
set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_ALWAYS);
signal_edge_reached().connect(sigc::mem_fun(*this, &ChatList::OnScrollEdgeOvershot));
auto v = get_vadjustment();
v->signal_value_changed().connect([this, v] {
m_should_scroll_to_bottom = v->get_upper() - v->get_page_size() <= v->get_value();
});
m_list.signal_size_allocate().connect([this](Gtk::Allocation &) {
if (m_should_scroll_to_bottom)
ScrollToBottom();
});
m_list.set_focus_hadjustment(get_hadjustment());
m_list.set_focus_vadjustment(get_vadjustment());
m_list.set_selection_mode(Gtk::SELECTION_NONE);
m_list.set_hexpand(true);
m_list.set_vexpand(true);
add(m_list);
m_list.show();
m_menu_copy_id = Gtk::manage(new Gtk::MenuItem("Copy ID"));
m_menu_copy_id->signal_activate().connect([this] {
Gtk::Clipboard::get()->set_text(std::to_string(m_menu_selected_message));
});
m_menu_copy_id->show();
m_menu.append(*m_menu_copy_id);
m_menu_delete_message = Gtk::manage(new Gtk::MenuItem("Delete Message"));
m_menu_delete_message->signal_activate().connect([this] {
Abaddon::Get().GetDiscordClient().DeleteMessage(m_active_channel, m_menu_selected_message);
});
m_menu_delete_message->show();
m_menu.append(*m_menu_delete_message);
m_menu_edit_message = Gtk::manage(new Gtk::MenuItem("Edit Message"));
m_menu_edit_message->signal_activate().connect([this] {
m_signal_action_message_edit.emit(m_active_channel, m_menu_selected_message);
});
m_menu_edit_message->show();
m_menu.append(*m_menu_edit_message);
m_menu_copy_content = Gtk::manage(new Gtk::MenuItem("Copy Content"));
m_menu_copy_content->signal_activate().connect([this] {
const auto msg = Abaddon::Get().GetDiscordClient().GetMessage(m_menu_selected_message);
if (msg.has_value())
Gtk::Clipboard::get()->set_text(msg->Content);
});
m_menu_copy_content->show();
m_menu.append(*m_menu_copy_content);
m_menu_reply_to = Gtk::manage(new Gtk::MenuItem("Reply To"));
m_menu_reply_to->signal_activate().connect([this] {
m_signal_action_reply_to.emit(m_menu_selected_message);
});
m_menu_reply_to->show();
m_menu.append(*m_menu_reply_to);
m_menu_unpin = Gtk::manage(new Gtk::MenuItem("Unpin"));
m_menu_unpin->signal_activate().connect([this] {
Abaddon::Get().GetDiscordClient().Unpin(m_active_channel, m_menu_selected_message, [](...) {});
});
m_menu.append(*m_menu_unpin);
m_menu_pin = Gtk::manage(new Gtk::MenuItem("Pin"));
m_menu_pin->signal_activate().connect([this] {
Abaddon::Get().GetDiscordClient().Pin(m_active_channel, m_menu_selected_message, [](...) {});
});
m_menu.append(*m_menu_pin);
m_menu.show();
}
void ChatList::Clear() {
auto children = m_list.get_children();
auto it = children.begin();
while (it != children.end()) {
delete *it;
it++;
}
}
void ChatList::SetActiveChannel(Snowflake id) {
m_active_channel = id;
}
void ChatList::ProcessNewMessage(const Message &data, bool prepend) {
auto &discord = Abaddon::Get().GetDiscordClient();
if (!discord.IsStarted()) return;
// delete preview message when gateway sends it back
if (!data.IsPending && data.Nonce.has_value() && data.Author.ID == discord.GetUserData().ID) {
for (auto [id, widget] : m_id_to_widget) {
if (dynamic_cast<ChatMessageItemContainer *>(widget)->Nonce == *data.Nonce) {
RemoveMessageAndHeader(widget);
m_id_to_widget.erase(id);
break;
}
}
}
ChatMessageHeader *last_row = nullptr;
bool should_attach = false;
if (!m_separate_all && m_num_rows > 0) {
if (prepend)
last_row = dynamic_cast<ChatMessageHeader *>(m_list.get_row_at_index(0));
else
last_row = dynamic_cast<ChatMessageHeader *>(m_list.get_row_at_index(m_num_rows - 1));
if (last_row != nullptr) {
const uint64_t diff = std::max(data.ID, last_row->NewestID) - std::min(data.ID, last_row->NewestID);
if (last_row->UserID == data.Author.ID && (prepend || (diff < SnowflakeSplitDifference * Snowflake::SecondsInterval)))
should_attach = true;
}
}
m_num_messages++;
if (m_should_scroll_to_bottom && !prepend) {
while (m_num_messages > MaxMessagesForChatCull) {
auto first_it = m_id_to_widget.begin();
RemoveMessageAndHeader(first_it->second);
m_id_to_widget.erase(first_it);
}
}
ChatMessageHeader *header;
if (should_attach) {
header = last_row;
} else {
const auto guild_id = *discord.GetChannel(m_active_channel)->GuildID;
const auto user_id = data.Author.ID;
const auto user = discord.GetUser(user_id);
if (!user.has_value()) return;
header = Gtk::manage(new ChatMessageHeader(data));
header->signal_action_insert_mention().connect([this, user_id]() {
m_signal_action_insert_mention.emit(user_id);
});
header->signal_action_open_user_menu().connect([this, user_id, guild_id](const GdkEvent *event) {
m_signal_action_open_user_menu.emit(event, user_id, guild_id);
});
m_num_rows++;
}
auto *content = ChatMessageItemContainer::FromMessage(data);
if (content != nullptr) {
header->AddContent(content, prepend);
m_id_to_widget[data.ID] = content;
const auto cb = [this, id = data.ID](GdkEventButton *ev) -> bool {
if (ev->type == GDK_BUTTON_PRESS && ev->button == GDK_BUTTON_SECONDARY) {
m_menu_selected_message = id;
const auto &client = Abaddon::Get().GetDiscordClient();
const auto data = client.GetMessage(id);
if (!data.has_value()) return false;
const auto channel = client.GetChannel(m_active_channel);
bool is_dm = channel.has_value() && (channel->Type == ChannelType::DM || channel->Type == ChannelType::GROUP_DM);
const bool has_manage = client.HasChannelPermission(client.GetUserData().ID, m_active_channel, Permission::MANAGE_MESSAGES);
m_menu_edit_message->set_visible(!m_use_pinned_menu);
m_menu_reply_to->set_visible(!m_use_pinned_menu);
m_menu_unpin->set_visible((is_dm || has_manage) && data->IsPinned);
m_menu_pin->set_visible((is_dm || has_manage) && !data->IsPinned);
if (data->IsDeleted()) {
m_menu_delete_message->set_sensitive(false);
m_menu_edit_message->set_sensitive(false);
} else {
const bool can_edit = client.GetUserData().ID == data->Author.ID;
const bool can_delete = can_edit || has_manage;
m_menu_delete_message->set_sensitive(can_delete);
m_menu_edit_message->set_sensitive(can_edit);
}
m_menu.popup_at_pointer(reinterpret_cast<GdkEvent *>(ev));
}
return false;
};
content->signal_button_press_event().connect(cb);
if (!data.IsPending) {
content->signal_action_reaction_add().connect([this, id = data.ID](const Glib::ustring &param) {
m_signal_action_reaction_add.emit(id, param);
});
content->signal_action_reaction_remove().connect([this, id = data.ID](const Glib::ustring &param) {
m_signal_action_reaction_remove.emit(id, param);
});
content->signal_action_channel_click().connect([this](const Snowflake &id) {
m_signal_action_channel_click.emit(id);
});
}
}
header->set_margin_left(5);
header->show_all();
if (!should_attach) {
if (prepend)
m_list.prepend(*header);
else
m_list.add(*header);
}
}
void ChatList::DeleteMessage(Snowflake id) {
auto widget = m_id_to_widget.find(id);
if (widget == m_id_to_widget.end()) return;
auto *x = dynamic_cast<ChatMessageItemContainer *>(widget->second);
if (x != nullptr)
x->UpdateAttributes();
}
void ChatList::RefetchMessage(Snowflake id) {
auto widget = m_id_to_widget.find(id);
if (widget == m_id_to_widget.end()) return;
auto *x = dynamic_cast<ChatMessageItemContainer *>(widget->second);
if (x != nullptr) {
x->UpdateContent();
x->UpdateAttributes();
}
}
Snowflake ChatList::GetOldestListedMessage() {
return m_id_to_widget.begin()->first;
}
void ChatList::UpdateMessageReactions(Snowflake id) {
auto it = m_id_to_widget.find(id);
if (it == m_id_to_widget.end()) return;
auto *widget = dynamic_cast<ChatMessageItemContainer *>(it->second);
if (widget == nullptr) return;
widget->UpdateReactions();
}
void ChatList::SetFailedByNonce(const std::string &nonce) {
for (auto [id, widget] : m_id_to_widget) {
if (auto *container = dynamic_cast<ChatMessageItemContainer *>(widget); container->Nonce == nonce) {
container->SetFailed();
break;
}
}
}
std::vector<Snowflake> ChatList::GetRecentAuthors() {
const auto &discord = Abaddon::Get().GetDiscordClient();
std::vector<Snowflake> ret;
std::map<Snowflake, Gtk::Widget *> ordered(m_id_to_widget.begin(), m_id_to_widget.end());
for (auto it = ordered.crbegin(); it != ordered.crend(); it++) {
const auto *widget = dynamic_cast<ChatMessageItemContainer *>(it->second);
if (widget == nullptr) continue;
const auto msg = discord.GetMessage(widget->ID);
if (!msg.has_value()) continue;
if (std::find(ret.begin(), ret.end(), msg->Author.ID) == ret.end())
ret.push_back(msg->Author.ID);
}
const auto chan = discord.GetChannel(m_active_channel);
if (chan->GuildID.has_value()) {
const auto others = discord.GetUsersInGuild(*chan->GuildID);
for (const auto id : others)
if (std::find(ret.begin(), ret.end(), id) == ret.end())
ret.push_back(id);
}
return ret;
}
void ChatList::SetSeparateAll(bool separate) {
m_separate_all = true;
}
void ChatList::SetUsePinnedMenu() {
m_use_pinned_menu = true;
}
void ChatList::ActuallyRemoveMessage(Snowflake id) {
auto it = m_id_to_widget.find(id);
if (it != m_id_to_widget.end())
RemoveMessageAndHeader(it->second);
}
void ChatList::OnScrollEdgeOvershot(Gtk::PositionType pos) {
if (pos == Gtk::POS_TOP)
m_signal_action_chat_load_history.emit(m_active_channel);
}
void ChatList::ScrollToBottom() {
auto x = get_vadjustment();
x->set_value(x->get_upper());
}
void ChatList::RemoveMessageAndHeader(Gtk::Widget *widget) {
auto *header = dynamic_cast<ChatMessageHeader *>(widget->get_ancestor(Gtk::ListBoxRow::get_type()));
if (header != nullptr) {
if (header->GetChildContent().size() == 1) {
m_num_rows--;
delete header;
} else {
delete widget;
}
} else {
delete widget;
}
m_num_messages--;
}
ChatList::type_signal_action_message_edit ChatList::signal_action_message_edit() {
return m_signal_action_message_edit;
}
ChatList::type_signal_action_chat_submit ChatList::signal_action_chat_submit() {
return m_signal_action_chat_submit;
}
ChatList::type_signal_action_chat_load_history ChatList::signal_action_chat_load_history() {
return m_signal_action_chat_load_history;
}
ChatList::type_signal_action_channel_click ChatList::signal_action_channel_click() {
return m_signal_action_channel_click;
}
ChatList::type_signal_action_insert_mention ChatList::signal_action_insert_mention() {
return m_signal_action_insert_mention;
}
ChatList::type_signal_action_open_user_menu ChatList::signal_action_open_user_menu() {
return m_signal_action_open_user_menu;
}
ChatList::type_signal_action_reaction_add ChatList::signal_action_reaction_add() {
return m_signal_action_reaction_add;
}
ChatList::type_signal_action_reaction_remove ChatList::signal_action_reaction_remove() {
return m_signal_action_reaction_remove;
}
ChatList::type_signal_action_reply_to ChatList::signal_action_reply_to() {
return m_signal_action_reply_to;
}

106
components/chatlist.hpp Normal file
View File

@@ -0,0 +1,106 @@
#pragma once
#include <gtkmm.h>
#include <map>
#include <vector>
#include "../discord/snowflake.hpp"
class ChatList : public Gtk::ScrolledWindow {
public:
ChatList();
void Clear();
void SetActiveChannel(Snowflake id);
template<typename Iter>
void SetMessages(Iter begin, Iter end);
template<typename Iter>
void PrependMessages(Iter begin, Iter end);
void ProcessNewMessage(const Message &data, bool prepend);
void DeleteMessage(Snowflake id);
void RefetchMessage(Snowflake id);
Snowflake GetOldestListedMessage();
void UpdateMessageReactions(Snowflake id);
void SetFailedByNonce(const std::string &nonce);
std::vector<Snowflake> GetRecentAuthors();
void SetSeparateAll(bool separate);
void SetUsePinnedMenu(); // i think i need a better way to do menus
void ActuallyRemoveMessage(Snowflake id); // perhaps not the best method name
private:
void OnScrollEdgeOvershot(Gtk::PositionType pos);
void ScrollToBottom();
void RemoveMessageAndHeader(Gtk::Widget *widget);
bool m_use_pinned_menu = false;
Gtk::Menu m_menu;
Gtk::MenuItem *m_menu_copy_id;
Gtk::MenuItem *m_menu_copy_content;
Gtk::MenuItem *m_menu_delete_message;
Gtk::MenuItem *m_menu_edit_message;
Gtk::MenuItem *m_menu_reply_to;
Gtk::MenuItem *m_menu_unpin;
Gtk::MenuItem *m_menu_pin;
Snowflake m_menu_selected_message;
Snowflake m_active_channel;
int m_num_messages = 0;
int m_num_rows = 0;
std::map<Snowflake, Gtk::Widget *> m_id_to_widget;
bool m_should_scroll_to_bottom = true;
Gtk::ListBox m_list;
bool m_separate_all = false;
public:
// these are all forwarded by the parent
using type_signal_action_message_edit = sigc::signal<void, Snowflake, Snowflake>;
using type_signal_action_chat_submit = sigc::signal<void, std::string, Snowflake, Snowflake>;
using type_signal_action_chat_load_history = sigc::signal<void, Snowflake>;
using type_signal_action_channel_click = sigc::signal<void, Snowflake>;
using type_signal_action_insert_mention = sigc::signal<void, Snowflake>;
using type_signal_action_open_user_menu = sigc::signal<void, const GdkEvent *, Snowflake, Snowflake>;
using type_signal_action_reaction_add = sigc::signal<void, Snowflake, Glib::ustring>;
using type_signal_action_reaction_remove = sigc::signal<void, Snowflake, Glib::ustring>;
using type_signal_action_reply_to = sigc::signal<void, Snowflake>;
type_signal_action_message_edit signal_action_message_edit();
type_signal_action_chat_submit signal_action_chat_submit();
type_signal_action_chat_load_history signal_action_chat_load_history();
type_signal_action_channel_click signal_action_channel_click();
type_signal_action_insert_mention signal_action_insert_mention();
type_signal_action_open_user_menu signal_action_open_user_menu();
type_signal_action_reaction_add signal_action_reaction_add();
type_signal_action_reaction_remove signal_action_reaction_remove();
type_signal_action_reply_to signal_action_reply_to();
private:
type_signal_action_message_edit m_signal_action_message_edit;
type_signal_action_chat_submit m_signal_action_chat_submit;
type_signal_action_chat_load_history m_signal_action_chat_load_history;
type_signal_action_channel_click m_signal_action_channel_click;
type_signal_action_insert_mention m_signal_action_insert_mention;
type_signal_action_open_user_menu m_signal_action_open_user_menu;
type_signal_action_reaction_add m_signal_action_reaction_add;
type_signal_action_reaction_remove m_signal_action_reaction_remove;
type_signal_action_reply_to m_signal_action_reply_to;
};
template<typename Iter>
inline void ChatList::SetMessages(Iter begin, Iter end) {
Clear();
m_num_rows = 0;
m_num_messages = 0;
m_id_to_widget.clear();
for (Iter it = begin; it != end; it++)
ProcessNewMessage(*it, false);
ScrollToBottom();
}
template<typename Iter>
inline void ChatList::PrependMessages(Iter begin, Iter end) {
for (Iter it = begin; it != end; it++)
ProcessNewMessage(*it, true);
}

View File

@@ -9,32 +9,11 @@ constexpr static int AvatarSize = 32;
constexpr static int EmbedImageWidth = 400;
constexpr static int EmbedImageHeight = 300;
constexpr static int ThumbnailSize = 100;
constexpr static int StickerComponentSize = 160;
ChatMessageItemContainer::ChatMessageItemContainer() {
m_main = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_VERTICAL));
add(*m_main);
m_menu_copy_id = Gtk::manage(new Gtk::MenuItem("Copy ID"));
m_menu_copy_id->signal_activate().connect(sigc::mem_fun(*this, &ChatMessageItemContainer::on_menu_copy_id));
m_menu.append(*m_menu_copy_id);
m_menu_delete_message = Gtk::manage(new Gtk::MenuItem("Delete Message"));
m_menu_delete_message->signal_activate().connect(sigc::mem_fun(*this, &ChatMessageItemContainer::on_menu_delete_message));
m_menu.append(*m_menu_delete_message);
m_menu_edit_message = Gtk::manage(new Gtk::MenuItem("Edit Message"));
m_menu_edit_message->signal_activate().connect(sigc::mem_fun(*this, &ChatMessageItemContainer::on_menu_edit_message));
m_menu.append(*m_menu_edit_message);
m_menu_copy_content = Gtk::manage(new Gtk::MenuItem("Copy Content"));
m_menu_copy_content->signal_activate().connect(sigc::mem_fun(*this, &ChatMessageItemContainer::on_menu_copy_content));
m_menu.append(*m_menu_copy_content);
m_menu_reply_to = Gtk::manage(new Gtk::MenuItem("Reply To"));
m_menu_reply_to->signal_activate().connect(sigc::mem_fun(*this, &ChatMessageItemContainer::on_menu_reply_to));
m_menu.append(*m_menu_reply_to);
m_menu.show_all();
ChatMessageItemContainer::ChatMessageItemContainer()
: m_main(Gtk::ORIENTATION_VERTICAL) {
add(m_main);
m_link_menu_copy = Gtk::manage(new Gtk::MenuItem("Copy Link"));
m_link_menu_copy->signal_activate().connect(sigc::mem_fun(*this, &ChatMessageItemContainer::on_link_menu_copy));
@@ -43,68 +22,74 @@ ChatMessageItemContainer::ChatMessageItemContainer() {
m_link_menu.show_all();
}
ChatMessageItemContainer *ChatMessageItemContainer::FromMessage(Snowflake id) {
const auto data = Abaddon::Get().GetDiscordClient().GetMessage(id);
if (!data.has_value()) return nullptr;
ChatMessageItemContainer *ChatMessageItemContainer::FromMessage(const Message &data) {
auto *container = Gtk::manage(new ChatMessageItemContainer);
container->ID = data->ID;
container->ChannelID = data->ChannelID;
container->ID = data.ID;
container->ChannelID = data.ChannelID;
if (data->Nonce.has_value())
container->Nonce = *data->Nonce;
if (data.Nonce.has_value())
container->Nonce = *data.Nonce;
if (data->Content.size() > 0 || data->Type != MessageType::DEFAULT) {
container->m_text_component = container->CreateTextComponent(&*data);
if (data.Content.size() > 0 || data.Type != MessageType::DEFAULT) {
container->m_text_component = container->CreateTextComponent(data);
container->AttachEventHandlers(*container->m_text_component);
container->m_main->add(*container->m_text_component);
container->m_main.add(*container->m_text_component);
}
if ((data->MessageReference.has_value() || data->Interaction.has_value()) && data->Type != MessageType::CHANNEL_FOLLOW_ADD) {
auto *widget = container->CreateReplyComponent(*data);
container->m_main->add(*widget);
container->m_main->child_property_position(*widget) = 0; // eek
if ((data.MessageReference.has_value() || data.Interaction.has_value()) && data.Type != MessageType::CHANNEL_FOLLOW_ADD) {
auto *widget = container->CreateReplyComponent(data);
if (widget != nullptr) {
container->m_main.add(*widget);
container->m_main.child_property_position(*widget) = 0; // eek
}
}
// there should only ever be 1 embed (i think?)
if (data->Embeds.size() == 1) {
const auto &embed = data->Embeds[0];
if (data.Embeds.size() == 1) {
const auto &embed = data.Embeds[0];
if (IsEmbedImageOnly(embed)) {
auto *widget = container->CreateImageComponent(*embed.Thumbnail->ProxyURL, *embed.Thumbnail->URL, *embed.Thumbnail->Width, *embed.Thumbnail->Height);
container->AttachEventHandlers(*widget);
container->m_main->add(*widget);
container->m_main.add(*widget);
} else {
container->m_embed_component = container->CreateEmbedComponent(embed);
container->AttachEventHandlers(*container->m_embed_component);
container->m_main->add(*container->m_embed_component);
container->m_main.add(*container->m_embed_component);
}
}
// i dont think attachments can be edited
// also this can definitely be done much better holy shit
for (const auto &a : data->Attachments) {
for (const auto &a : data.Attachments) {
if (IsURLViewableImage(a.ProxyURL) && a.Width.has_value() && a.Height.has_value()) {
auto *widget = container->CreateImageComponent(a.ProxyURL, a.URL, *a.Width, *a.Height);
container->m_main->add(*widget);
container->m_main.add(*widget);
} else {
auto *widget = container->CreateAttachmentComponent(a);
container->m_main->add(*widget);
container->m_main.add(*widget);
}
}
// only 1?
if (data->Stickers.has_value()) {
const auto &sticker = data->Stickers.value()[0];
/*
DEPRECATED
if (data.Stickers.has_value()) {
const auto &sticker = data.Stickers.value()[0];
// todo: lottie, proper apng
if (sticker.FormatType == StickerFormatType::PNG || sticker.FormatType == StickerFormatType::APNG) {
auto *widget = container->CreateStickerComponent(sticker);
container->m_main->add(*widget);
}
}*/
if (data.StickerItems.has_value()) {
auto *widget = container->CreateStickersComponent(*data.StickerItems);
container->m_main.add(*widget);
}
if (data->Reactions.has_value() && data->Reactions->size() > 0) {
container->m_reactions_component = container->CreateReactionsComponent(*data);
container->m_main->add(*container->m_reactions_component);
if (data.Reactions.has_value() && data.Reactions->size() > 0) {
container->m_reactions_component = container->CreateReactionsComponent(data);
container->m_main.add(*container->m_reactions_component);
}
container->UpdateAttributes();
@@ -126,7 +111,7 @@ void ChatMessageItemContainer::UpdateContent() {
if (data->Embeds.size() == 1) {
m_embed_component = CreateEmbedComponent(data->Embeds[0]);
AttachEventHandlers(*m_embed_component);
m_main->add(*m_embed_component);
m_main.add(*m_embed_component);
}
}
@@ -138,13 +123,15 @@ void ChatMessageItemContainer::UpdateReactions() {
if (data->Reactions.has_value() && data->Reactions->size() > 0) {
m_reactions_component = CreateReactionsComponent(*data);
m_reactions_component->show_all();
m_main->add(*m_reactions_component);
m_main.add(*m_reactions_component);
}
}
void ChatMessageItemContainer::SetFailed() {
m_text_component->get_style_context()->remove_class("pending");
m_text_component->get_style_context()->add_class("failed");
if (m_text_component != nullptr) {
m_text_component->get_style_context()->remove_class("pending");
m_text_component->get_style_context()->add_class("failed");
}
}
void ChatMessageItemContainer::UpdateAttributes() {
@@ -160,7 +147,7 @@ void ChatMessageItemContainer::UpdateAttributes() {
m_attrib_label = Gtk::manage(new Gtk::Label);
m_attrib_label->set_halign(Gtk::ALIGN_START);
m_attrib_label->show();
m_main->add(*m_attrib_label); // todo: maybe insert markup into existing text widget's buffer if the circumstances are right (or pack horizontally)
m_main.add(*m_attrib_label); // todo: maybe insert markup into existing text widget's buffer if the circumstances are right (or pack horizontally)
}
if (deleted)
@@ -181,10 +168,10 @@ void ChatMessageItemContainer::AddClickHandler(Gtk::Widget *widget, std::string
// clang-format on
}
Gtk::TextView *ChatMessageItemContainer::CreateTextComponent(const Message *data) {
Gtk::TextView *ChatMessageItemContainer::CreateTextComponent(const Message &data) {
auto *tv = Gtk::manage(new Gtk::TextView);
if (data->IsPending)
if (data.IsPending)
tv->get_style_context()->add_class("pending");
tv->get_style_context()->add_class("message-text");
tv->set_can_focus(false);
@@ -289,6 +276,20 @@ void ChatMessageItemContainer::UpdateTextComponent(Gtk::TextView *tv) {
case MessageType::GUILD_DISCOVERY_GRACE_PERIOD_FINAL_WARNING: {
b->insert_markup(s, "<i><span color='#999999'>This server has failed Discovery activity requirements for 3 weeks in a row. If this server fails for 1 more week, it will be removed from Discovery.</span></i>");
} break;
case MessageType::THREAD_CREATED: {
const auto author = Abaddon::Get().GetDiscordClient().GetUser(data->Author.ID);
if (data->MessageReference.has_value() && data->MessageReference->ChannelID.has_value()) {
auto iter = b->insert_markup(s, "<i><span color='#999999'>" + author->GetEscapedBoldName() + " started a thread: </span></i>");
auto tag = b->create_tag();
tag->property_weight() = Pango::WEIGHT_BOLD;
m_channel_tagmap[tag] = *data->MessageReference->ChannelID;
b->insert_with_tag(iter, data->Content, tag);
tv->signal_button_press_event().connect(sigc::mem_fun(*this, &ChatMessageItemContainer::OnClickChannel), false);
} else {
b->insert_markup(s, "<i><span color='#999999'>" + author->GetEscapedBoldName() + " started a thread: </span><b>" + Glib::Markup::escape_text(data->Content) + "</b></i>");
}
} break;
default: break;
}
}
@@ -502,7 +503,7 @@ Gtk::Widget *ChatMessageItemContainer::CreateAttachmentComponent(const Attachmen
return ev;
}
Gtk::Widget *ChatMessageItemContainer::CreateStickerComponent(const StickerData &data) {
Gtk::Widget *ChatMessageItemContainer::CreateStickerComponentDeprecated(const StickerData &data) {
auto *box = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL));
auto *imgw = Gtk::manage(new Gtk::Image);
box->add(*imgw);
@@ -519,6 +520,27 @@ Gtk::Widget *ChatMessageItemContainer::CreateStickerComponent(const StickerData
return box;
}
Gtk::Widget *ChatMessageItemContainer::CreateStickersComponent(const std::vector<StickerItem> &data) {
auto *box = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_VERTICAL));
for (const auto &sticker : data) {
// no lottie
if (sticker.FormatType != StickerFormatType::PNG && sticker.FormatType != StickerFormatType::APNG) continue;
auto *ev = Gtk::manage(new Gtk::EventBox);
auto *img = Gtk::manage(new LazyImage(sticker.GetURL(), StickerComponentSize, StickerComponentSize, false));
img->set_size_request(StickerComponentSize, StickerComponentSize); // should this go in LazyImage ?
img->show();
ev->show();
ev->add(*img);
box->add(*ev);
}
box->show();
AttachEventHandlers(*box);
return box;
}
Gtk::Widget *ChatMessageItemContainer::CreateReactionsComponent(const Message &data) {
auto *flow = Gtk::manage(new Gtk::FlowBox);
flow->set_orientation(Gtk::ORIENTATION_HORIZONTAL);
@@ -601,6 +623,8 @@ Gtk::Widget *ChatMessageItemContainer::CreateReactionsComponent(const Message &d
}
Gtk::Widget *ChatMessageItemContainer::CreateReplyComponent(const Message &data) {
if (data.Type == MessageType::THREAD_CREATED) return nullptr;
auto *box = Gtk::manage(new Gtk::Box);
auto *lbl = Gtk::manage(new Gtk::Label);
lbl->set_single_line_mode(true);
@@ -808,11 +832,11 @@ void ChatMessageItemContainer::HandleCustomEmojis(Gtk::TextView &tv) {
}
void ChatMessageItemContainer::HandleEmojis(Gtk::TextView &tv) {
static bool emojis = Abaddon::Get().GetSettings().GetShowEmojis();
if (emojis) {
HandleStockEmojis(tv);
HandleCustomEmojis(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);
}
void ChatMessageItemContainer::CleanupEmojis(Glib::RefPtr<Gtk::TextBuffer> buf) {
@@ -866,8 +890,10 @@ void ChatMessageItemContainer::HandleChannelMentions(Glib::RefPtr<Gtk::TextBuffe
}
auto tag = buf->create_tag();
m_channel_tagmap[tag] = channel_id;
tag->property_weight() = Pango::WEIGHT_BOLD;
if (chan->Type == ChannelType::GUILD_TEXT) {
m_channel_tagmap[tag] = channel_id;
tag->property_weight() = Pango::WEIGHT_BOLD;
}
const auto chars_start = g_utf8_pointer_to_offset(text.c_str(), text.c_str() + mstart);
const auto chars_end = g_utf8_pointer_to_offset(text.c_str(), text.c_str() + mend);
@@ -989,52 +1015,6 @@ bool ChatMessageItemContainer::OnLinkClick(GdkEventButton *ev) {
return false;
}
void ChatMessageItemContainer::ShowMenu(GdkEvent *event) {
const auto &client = Abaddon::Get().GetDiscordClient();
const auto data = client.GetMessage(ID);
if (data->IsDeleted()) {
m_menu_delete_message->set_sensitive(false);
m_menu_edit_message->set_sensitive(false);
} else {
const bool can_edit = client.GetUserData().ID == data->Author.ID;
const bool can_delete = can_edit || client.HasChannelPermission(client.GetUserData().ID, ChannelID, Permission::MANAGE_MESSAGES);
m_menu_delete_message->set_sensitive(can_delete);
m_menu_edit_message->set_sensitive(can_edit);
}
m_menu.popup_at_pointer(event);
}
void ChatMessageItemContainer::on_menu_copy_id() {
Gtk::Clipboard::get()->set_text(std::to_string(ID));
}
void ChatMessageItemContainer::on_menu_delete_message() {
m_signal_action_delete.emit();
}
void ChatMessageItemContainer::on_menu_edit_message() {
m_signal_action_edit.emit();
}
void ChatMessageItemContainer::on_menu_copy_content() {
const auto msg = Abaddon::Get().GetDiscordClient().GetMessage(ID);
if (msg.has_value())
Gtk::Clipboard::get()->set_text(msg->Content);
}
void ChatMessageItemContainer::on_menu_reply_to() {
m_signal_action_reply_to.emit(ID);
}
ChatMessageItemContainer::type_signal_action_delete ChatMessageItemContainer::signal_action_delete() {
return m_signal_action_delete;
}
ChatMessageItemContainer::type_signal_action_edit ChatMessageItemContainer::signal_action_edit() {
return m_signal_action_edit;
}
ChatMessageItemContainer::type_signal_channel_click ChatMessageItemContainer::signal_action_channel_click() {
return m_signal_action_channel_click;
}
@@ -1047,14 +1027,10 @@ ChatMessageItemContainer::type_signal_action_reaction_remove ChatMessageItemCont
return m_signal_action_reaction_remove;
}
ChatMessageItemContainer::type_signal_action_reply_to ChatMessageItemContainer::signal_action_reply_to() {
return m_signal_action_reply_to;
}
void ChatMessageItemContainer::AttachEventHandlers(Gtk::Widget &widget) {
const auto on_button_press_event = [this](GdkEventButton *event) -> bool {
if (event->type == GDK_BUTTON_PRESS && event->button == GDK_BUTTON_SECONDARY) {
ShowMenu(reinterpret_cast<GdkEvent *>(event));
const auto on_button_press_event = [this](GdkEventButton *e) -> bool {
if (e->type == GDK_BUTTON_PRESS && e->button == GDK_BUTTON_SECONDARY) {
event(reinterpret_cast<GdkEvent *>(e)); // illegal ooooooh
return true;
}
@@ -1063,28 +1039,22 @@ void ChatMessageItemContainer::AttachEventHandlers(Gtk::Widget &widget) {
widget.signal_button_press_event().connect(on_button_press_event, false);
}
ChatMessageHeader::ChatMessageHeader(const Message *data) {
UserID = data->Author.ID;
ChannelID = data->ChannelID;
m_main_box = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL));
m_content_box = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_VERTICAL));
m_content_box_ev = Gtk::manage(new Gtk::EventBox);
m_meta_box = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL));
m_meta_ev = Gtk::manage(new Gtk::EventBox);
m_author = Gtk::manage(new Gtk::Label);
m_timestamp = Gtk::manage(new Gtk::Label);
m_avatar_ev = Gtk::manage(new Gtk::EventBox);
ChatMessageHeader::ChatMessageHeader(const Message &data)
: m_main_box(Gtk::ORIENTATION_HORIZONTAL)
, m_content_box(Gtk::ORIENTATION_VERTICAL)
, m_meta_box(Gtk::ORIENTATION_HORIZONTAL)
, m_avatar(Abaddon::Get().GetImageManager().GetPlaceholder(AvatarSize)) {
UserID = data.Author.ID;
ChannelID = data.ChannelID;
const auto author = Abaddon::Get().GetDiscordClient().GetUser(UserID);
auto &img = Abaddon::Get().GetImageManager();
m_avatar = Gtk::manage(new Gtk::Image(img.GetPlaceholder(AvatarSize)));
auto cb = [this](const Glib::RefPtr<Gdk::Pixbuf> &pb) {
m_static_avatar = pb->scale_simple(AvatarSize, AvatarSize, Gdk::INTERP_BILINEAR);
m_avatar->property_pixbuf() = m_static_avatar;
m_avatar.property_pixbuf() = m_static_avatar;
};
img.LoadFromURL(author->GetAvatarURL(), sigc::track_obj(cb, *this));
img.LoadFromURL(author->GetAvatarURL(data.GuildID), sigc::track_obj(cb, *this));
if (author->HasAnimatedAvatar()) {
auto cb = [this](const Glib::RefPtr<Gdk::PixbufAnimation> &pb) {
@@ -1094,23 +1064,23 @@ ChatMessageHeader::ChatMessageHeader(const Message *data) {
}
get_style_context()->add_class("message-container");
m_author->get_style_context()->add_class("message-container-author");
m_timestamp->get_style_context()->add_class("message-container-timestamp");
m_avatar->get_style_context()->add_class("message-container-avatar");
m_author.get_style_context()->add_class("message-container-author");
m_timestamp.get_style_context()->add_class("message-container-timestamp");
m_avatar.get_style_context()->add_class("message-container-avatar");
m_avatar->set_valign(Gtk::ALIGN_START);
m_avatar->set_margin_right(10);
m_avatar.set_valign(Gtk::ALIGN_START);
m_avatar.set_margin_right(10);
m_author->set_markup(data->Author.GetEscapedBoldName());
m_author->set_single_line_mode(true);
m_author->set_line_wrap(false);
m_author->set_ellipsize(Pango::ELLIPSIZE_END);
m_author->set_xalign(0.f);
m_author->set_can_focus(false);
m_author.set_markup(data.Author.GetEscapedBoldName());
m_author.set_single_line_mode(true);
m_author.set_line_wrap(false);
m_author.set_ellipsize(Pango::ELLIPSIZE_END);
m_author.set_xalign(0.f);
m_author.set_can_focus(false);
m_meta_ev->signal_button_press_event().connect(sigc::mem_fun(*this, &ChatMessageHeader::on_author_button_press));
m_meta_ev.signal_button_press_event().connect(sigc::mem_fun(*this, &ChatMessageHeader::on_author_button_press));
if (author->IsBot || data->WebhookID.has_value()) {
if (author->IsBot || data.WebhookID.has_value()) {
m_extra = Gtk::manage(new Gtk::Label);
m_extra->get_style_context()->add_class("message-container-extra");
m_extra->set_single_line_mode(true);
@@ -1120,62 +1090,62 @@ ChatMessageHeader::ChatMessageHeader(const Message *data) {
}
if (author->IsBot)
m_extra->set_markup("<b>BOT</b>");
else if (data->WebhookID.has_value())
else if (data.WebhookID.has_value())
m_extra->set_markup("<b>Webhook</b>");
m_timestamp->set_text(data->ID.GetLocalTimestamp());
m_timestamp->set_hexpand(true);
m_timestamp->set_halign(Gtk::ALIGN_END);
m_timestamp->set_ellipsize(Pango::ELLIPSIZE_END);
m_timestamp->set_opacity(0.5);
m_timestamp->set_single_line_mode(true);
m_timestamp->set_margin_start(12);
m_timestamp->set_can_focus(false);
m_timestamp.set_text(data.ID.GetLocalTimestamp());
m_timestamp.set_hexpand(true);
m_timestamp.set_halign(Gtk::ALIGN_END);
m_timestamp.set_ellipsize(Pango::ELLIPSIZE_END);
m_timestamp.set_opacity(0.5);
m_timestamp.set_single_line_mode(true);
m_timestamp.set_margin_start(12);
m_timestamp.set_can_focus(false);
m_main_box->set_hexpand(true);
m_main_box->set_vexpand(true);
m_main_box->set_can_focus(true);
m_main_box.set_hexpand(true);
m_main_box.set_vexpand(true);
m_main_box.set_can_focus(true);
m_meta_box->set_hexpand(true);
m_meta_box->set_can_focus(false);
m_meta_box.set_hexpand(true);
m_meta_box.set_can_focus(false);
m_content_box->set_can_focus(false);
m_content_box.set_can_focus(false);
const auto on_enter_cb = [this](const GdkEventCrossing *event) -> bool {
if (m_anim_avatar)
m_avatar->property_pixbuf_animation() = m_anim_avatar;
m_avatar.property_pixbuf_animation() = m_anim_avatar;
return false;
};
const auto on_leave_cb = [this](const GdkEventCrossing *event) -> bool {
if (m_anim_avatar)
m_avatar->property_pixbuf() = m_static_avatar;
m_avatar.property_pixbuf() = m_static_avatar;
return false;
};
m_content_box_ev->add_events(Gdk::ENTER_NOTIFY_MASK | Gdk::LEAVE_NOTIFY_MASK);
m_meta_ev->add_events(Gdk::ENTER_NOTIFY_MASK | Gdk::LEAVE_NOTIFY_MASK);
m_avatar_ev->add_events(Gdk::ENTER_NOTIFY_MASK | Gdk::LEAVE_NOTIFY_MASK);
m_content_box_ev.add_events(Gdk::ENTER_NOTIFY_MASK | Gdk::LEAVE_NOTIFY_MASK);
m_meta_ev.add_events(Gdk::ENTER_NOTIFY_MASK | Gdk::LEAVE_NOTIFY_MASK);
m_avatar_ev.add_events(Gdk::ENTER_NOTIFY_MASK | Gdk::LEAVE_NOTIFY_MASK);
if (Abaddon::Get().GetSettings().GetShowAnimations()) {
m_content_box_ev->signal_enter_notify_event().connect(on_enter_cb);
m_content_box_ev->signal_leave_notify_event().connect(on_leave_cb);
m_meta_ev->signal_enter_notify_event().connect(on_enter_cb);
m_meta_ev->signal_leave_notify_event().connect(on_leave_cb);
m_avatar_ev->signal_enter_notify_event().connect(on_enter_cb);
m_avatar_ev->signal_leave_notify_event().connect(on_leave_cb);
m_content_box_ev.signal_enter_notify_event().connect(on_enter_cb);
m_content_box_ev.signal_leave_notify_event().connect(on_leave_cb);
m_meta_ev.signal_enter_notify_event().connect(on_enter_cb);
m_meta_ev.signal_leave_notify_event().connect(on_leave_cb);
m_avatar_ev.signal_enter_notify_event().connect(on_enter_cb);
m_avatar_ev.signal_leave_notify_event().connect(on_leave_cb);
}
m_meta_box->add(*m_author);
m_meta_box.add(m_author);
if (m_extra != nullptr)
m_meta_box->add(*m_extra);
m_meta_box.add(*m_extra);
m_meta_box->add(*m_timestamp);
m_meta_ev->add(*m_meta_box);
m_content_box->add(*m_meta_ev);
m_avatar_ev->add(*m_avatar);
m_main_box->add(*m_avatar_ev);
m_content_box_ev->add(*m_content_box);
m_main_box->add(*m_content_box_ev);
add(*m_main_box);
m_meta_box.add(m_timestamp);
m_meta_ev.add(m_meta_box);
m_content_box.add(m_meta_ev);
m_avatar_ev.add(m_avatar);
m_main_box.add(m_avatar_ev);
m_content_box_ev.add(m_content_box);
m_main_box.add(m_content_box_ev);
add(m_main_box);
set_margin_bottom(8);
@@ -1187,8 +1157,8 @@ ChatMessageHeader::ChatMessageHeader(const Message *data) {
auto guild_member_update_cb = [this](const auto &, const auto &) { UpdateNameColor(); };
discord.signal_guild_member_update().connect(sigc::track_obj(guild_member_update_cb, *this));
UpdateNameColor();
AttachUserMenuHandler(*m_meta_ev);
AttachUserMenuHandler(*m_avatar_ev);
AttachUserMenuHandler(m_meta_ev);
AttachUserMenuHandler(m_avatar_ev);
}
void ChatMessageHeader::UpdateNameColor() {
@@ -1205,7 +1175,7 @@ void ChatMessageHeader::UpdateNameColor() {
else
md = "<span weight='bold'>" + user->GetEscapedName() + "</span>";
m_author->set_markup(md);
m_author.set_markup(md);
}
std::vector<Gtk::Widget *> ChatMessageHeader::GetChildContent() {
@@ -1215,7 +1185,11 @@ std::vector<Gtk::Widget *> ChatMessageHeader::GetChildContent() {
void ChatMessageHeader::AttachUserMenuHandler(Gtk::Widget &widget) {
widget.signal_button_press_event().connect([this](GdkEventButton *ev) -> bool {
if (ev->type == GDK_BUTTON_PRESS && ev->button == GDK_BUTTON_SECONDARY) {
m_signal_action_open_user_menu.emit(reinterpret_cast<GdkEvent *>(ev));
auto info = Abaddon::Get().GetDiscordClient().GetChannel(ChannelID);
Snowflake guild_id;
if (info.has_value() && info->GuildID.has_value())
guild_id = *info->GuildID;
Abaddon::Get().ShowUserMenu(reinterpret_cast<GdkEvent *>(ev), UserID, guild_id);
return true;
}
@@ -1242,12 +1216,13 @@ ChatMessageHeader::type_signal_action_open_user_menu ChatMessageHeader::signal_a
void ChatMessageHeader::AddContent(Gtk::Widget *widget, bool prepend) {
m_content_widgets.push_back(widget);
widget->signal_unmap().connect([this, widget]() {
const auto cb = [this, widget]() {
m_content_widgets.erase(std::remove(m_content_widgets.begin(), m_content_widgets.end(), widget), m_content_widgets.end());
});
m_content_box->add(*widget);
};
widget->signal_unmap().connect(sigc::track_obj(cb, *this, *widget), false);
m_content_box.add(*widget);
if (prepend)
m_content_box->reorder_child(*widget, 1);
m_content_box.reorder_child(*widget, 1);
if (auto *x = dynamic_cast<ChatMessageItemContainer *>(widget)) {
if (x->ID > NewestID)
NewestID = x->ID;

View File

@@ -10,7 +10,7 @@ public:
std::string Nonce;
ChatMessageItemContainer();
static ChatMessageItemContainer *FromMessage(Snowflake id);
static ChatMessageItemContainer *FromMessage(const Message &data);
// attributes = edited, deleted
void UpdateAttributes();
@@ -20,12 +20,13 @@ public:
protected:
void AddClickHandler(Gtk::Widget *widget, std::string);
Gtk::TextView *CreateTextComponent(const Message *data); // Message.Content
Gtk::TextView *CreateTextComponent(const Message &data); // Message.Content
void UpdateTextComponent(Gtk::TextView *tv);
Gtk::Widget *CreateEmbedComponent(const EmbedData &data); // Message.Embeds[0]
Gtk::Widget *CreateImageComponent(const std::string &proxy_url, const std::string &url, int inw, int inh);
Gtk::Widget *CreateAttachmentComponent(const AttachmentData &data); // non-image attachments
Gtk::Widget *CreateStickerComponent(const StickerData &data);
Gtk::Widget *CreateStickerComponentDeprecated(const StickerData &data);
Gtk::Widget *CreateStickersComponent(const std::vector<StickerItem> &data);
Gtk::Widget *CreateReactionsComponent(const Message &data);
Gtk::Widget *CreateReplyComponent(const Message &data);
@@ -56,23 +57,9 @@ protected:
std::map<Glib::RefPtr<Gtk::TextTag>, Snowflake> m_channel_tagmap;
void AttachEventHandlers(Gtk::Widget &widget);
void ShowMenu(GdkEvent *event);
Gtk::Menu m_menu;
Gtk::MenuItem *m_menu_copy_id;
Gtk::MenuItem *m_menu_copy_content;
Gtk::MenuItem *m_menu_delete_message;
Gtk::MenuItem *m_menu_edit_message;
Gtk::MenuItem *m_menu_reply_to;
void on_menu_copy_id();
void on_menu_delete_message();
void on_menu_edit_message();
void on_menu_copy_content();
void on_menu_reply_to();
Gtk::EventBox *m_ev;
Gtk::Box *m_main;
Gtk::EventBox *_ev;
Gtk::Box m_main;
Gtk::Label *m_attrib_label = nullptr;
Gtk::TextView *m_text_component = nullptr;
@@ -80,29 +67,18 @@ protected:
Gtk::Widget *m_reactions_component = nullptr;
public:
typedef sigc::signal<void> type_signal_action_delete;
typedef sigc::signal<void> type_signal_action_edit;
typedef sigc::signal<void, Snowflake> type_signal_channel_click;
typedef sigc::signal<void, Glib::ustring> type_signal_action_reaction_add;
typedef sigc::signal<void, Glib::ustring> type_signal_action_reaction_remove;
typedef sigc::signal<void, Snowflake> type_signal_action_reply_to;
typedef sigc::signal<void> type_signal_enter;
typedef sigc::signal<void> type_signal_leave;
type_signal_action_delete signal_action_delete();
type_signal_action_edit signal_action_edit();
type_signal_channel_click signal_action_channel_click();
type_signal_action_reaction_add signal_action_reaction_add();
type_signal_action_reaction_remove signal_action_reaction_remove();
type_signal_action_reply_to signal_action_reply_to();
private:
type_signal_action_delete m_signal_action_delete;
type_signal_action_edit m_signal_action_edit;
type_signal_channel_click m_signal_action_channel_click;
type_signal_action_reaction_add m_signal_action_reaction_add;
type_signal_action_reaction_remove m_signal_action_reaction_remove;
type_signal_action_reply_to m_signal_action_reply_to;
};
class ChatMessageHeader : public Gtk::ListBoxRow {
@@ -111,28 +87,28 @@ public:
Snowflake ChannelID;
Snowflake NewestID = 0;
ChatMessageHeader(const Message *data);
ChatMessageHeader(const Message &data);
void AddContent(Gtk::Widget *widget, bool prepend);
void UpdateNameColor();
std::vector<Gtk::Widget*> GetChildContent();
std::vector<Gtk::Widget *> GetChildContent();
protected:
void AttachUserMenuHandler(Gtk::Widget &widget);
bool on_author_button_press(GdkEventButton *ev);
std::vector<Gtk::Widget*> m_content_widgets;
std::vector<Gtk::Widget *> m_content_widgets;
Gtk::Box *m_main_box;
Gtk::Box *m_content_box;
Gtk::EventBox *m_content_box_ev;
Gtk::Box *m_meta_box;
Gtk::EventBox *m_meta_ev;
Gtk::Label *m_author;
Gtk::Label *m_timestamp;
Gtk::Box m_main_box;
Gtk::Box m_content_box;
Gtk::EventBox m_content_box_ev;
Gtk::Box m_meta_box;
Gtk::EventBox m_meta_ev;
Gtk::Label m_author;
Gtk::Label m_timestamp;
Gtk::Label *m_extra = nullptr;
Gtk::Image *m_avatar;
Gtk::EventBox *m_avatar_ev;
Gtk::Image m_avatar;
Gtk::EventBox m_avatar_ev;
Glib::RefPtr<Gdk::Pixbuf> m_static_avatar;
Glib::RefPtr<Gdk::PixbufAnimation> m_anim_avatar;

View File

@@ -4,15 +4,13 @@
#include "chatinputindicator.hpp"
#include "ratelimitindicator.hpp"
#include "chatinput.hpp"
constexpr static uint64_t SnowflakeSplitDifference = 600;
#include "chatlist.hpp"
ChatWindow::ChatWindow() {
Abaddon::Get().GetDiscordClient().signal_message_send_fail().connect(sigc::mem_fun(*this, &ChatWindow::OnMessageSendFail));
m_main = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_VERTICAL));
m_list = Gtk::manage(new Gtk::ListBox);
m_scroll = Gtk::manage(new Gtk::ScrolledWindow);
m_chat = Gtk::manage(new ChatList);
m_input = Gtk::manage(new ChatInput);
m_input_indicator = Gtk::manage(new ChatInputIndicator);
m_rate_limit_indicator = Gtk::manage(new RateLimitIndicator);
@@ -29,33 +27,14 @@ ChatWindow::ChatWindow() {
m_input_indicator->show();
m_main->get_style_context()->add_class("messages");
m_list->get_style_context()->add_class("messages");
m_main->set_hexpand(true);
m_main->set_vexpand(true);
m_scroll->signal_edge_reached().connect(sigc::mem_fun(*this, &ChatWindow::OnScrollEdgeOvershot));
auto v = m_scroll->get_vadjustment();
v->signal_value_changed().connect([this, v] {
m_should_scroll_to_bottom = v->get_upper() - v->get_page_size() <= v->get_value();
});
m_scroll->set_can_focus(false);
m_scroll->set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_ALWAYS);
m_scroll->show();
m_list->signal_size_allocate().connect([this](Gtk::Allocation &) {
if (m_should_scroll_to_bottom)
ScrollToBottom();
});
m_list->set_selection_mode(Gtk::SELECTION_NONE);
m_list->set_hexpand(true);
m_list->set_vexpand(true);
m_list->set_focus_hadjustment(m_scroll->get_hadjustment());
m_list->set_focus_vadjustment(m_scroll->get_vadjustment());
m_list->show();
m_topic.get_style_context()->add_class("channel-topic");
m_topic.add(m_topic_text);
m_topic_text.set_halign(Gtk::ALIGN_START);
m_topic_text.show();
m_input->signal_submit().connect(sigc::mem_fun(*this, &ChatWindow::OnInputSubmit));
m_input->signal_escape().connect([this]() {
@@ -71,41 +50,47 @@ ChatWindow::ChatWindow() {
});
m_completer.SetGetRecentAuthors([this]() -> auto {
const auto &discord = Abaddon::Get().GetDiscordClient();
std::vector<Snowflake> ret;
std::map<Snowflake, Gtk::Widget *> ordered(m_id_to_widget.begin(), m_id_to_widget.end());
for (auto it = ordered.crbegin(); it != ordered.crend(); it++) {
const auto *widget = dynamic_cast<ChatMessageItemContainer *>(it->second);
if (widget == nullptr) continue;
const auto msg = discord.GetMessage(widget->ID);
if (!msg.has_value()) continue;
if (std::find(ret.begin(), ret.end(), msg->Author.ID) == ret.end())
ret.push_back(msg->Author.ID);
}
const auto chan = discord.GetChannel(m_active_channel);
if (chan->GuildID.has_value()) {
const auto others = discord.GetUsersInGuild(*chan->GuildID);
for (const auto id : others)
if (std::find(ret.begin(), ret.end(), id) == ret.end())
ret.push_back(id);
}
return ret;
return m_chat->GetRecentAuthors();
});
m_completer.show();
m_chat->signal_action_channel_click().connect([this](Snowflake id) {
m_signal_action_channel_click.emit(id);
});
m_chat->signal_action_chat_load_history().connect([this](Snowflake id) {
m_signal_action_chat_load_history.emit(id);
});
m_chat->signal_action_chat_submit().connect([this](const std::string &str, Snowflake channel_id, Snowflake referenced_id) {
m_signal_action_chat_submit.emit(str, channel_id, referenced_id);
});
m_chat->signal_action_insert_mention().connect([this](Snowflake id) {
// lowkey gross
m_signal_action_insert_mention.emit(id);
});
m_chat->signal_action_message_edit().connect([this](Snowflake channel_id, Snowflake message_id) {
m_signal_action_message_edit.emit(channel_id, message_id);
});
m_chat->signal_action_reaction_add().connect([this](Snowflake id, const Glib::ustring &param) {
m_signal_action_reaction_add.emit(id, param);
});
m_chat->signal_action_reaction_remove().connect([this](Snowflake id, const Glib::ustring &param) {
m_signal_action_reaction_remove.emit(id, param);
});
m_chat->signal_action_reply_to().connect([this](Snowflake id) {
StartReplying(id);
});
m_chat->show();
m_meta->set_hexpand(true);
m_meta->set_halign(Gtk::ALIGN_FILL);
m_meta->show();
m_meta->add(*m_input_indicator);
m_meta->add(*m_rate_limit_indicator);
m_scroll->add(*m_list);
m_main->add(*m_scroll);
//m_scroll->add(*m_list);
m_main->add(m_topic);
m_main->add(*m_chat);
m_main->add(m_completer);
m_main->add(*m_input);
m_main->add(*m_meta);
@@ -117,63 +102,36 @@ Gtk::Widget *ChatWindow::GetRoot() const {
}
void ChatWindow::Clear() {
SetMessages(std::set<Snowflake>());
m_chat->Clear();
}
void ChatWindow::SetMessages(const std::set<Snowflake> &msgs) {
// empty the listbox
auto children = m_list->get_children();
auto it = children.begin();
while (it != children.end()) {
delete *it;
it++;
}
m_num_rows = 0;
m_num_messages = 0;
m_id_to_widget.clear();
for (const auto &id : msgs) {
ProcessNewMessage(id, false);
}
void ChatWindow::SetMessages(const std::vector<Message> &msgs) {
m_chat->SetMessages(msgs.begin(), msgs.end());
}
void ChatWindow::SetActiveChannel(Snowflake id) {
m_active_channel = id;
m_chat->SetActiveChannel(id);
m_input_indicator->SetActiveChannel(id);
m_rate_limit_indicator->SetActiveChannel(id);
if (m_is_replying)
StopReplying();
}
void ChatWindow::AddNewMessage(Snowflake id) {
ProcessNewMessage(id, false);
void ChatWindow::AddNewMessage(const Message &data) {
m_chat->ProcessNewMessage(data, false);
}
void ChatWindow::DeleteMessage(Snowflake id) {
auto widget = m_id_to_widget.find(id);
if (widget == m_id_to_widget.end()) return;
auto *x = dynamic_cast<ChatMessageItemContainer *>(widget->second);
if (x != nullptr)
x->UpdateAttributes();
m_chat->DeleteMessage(id);
}
void ChatWindow::UpdateMessage(Snowflake id) {
auto widget = m_id_to_widget.find(id);
if (widget == m_id_to_widget.end()) return;
auto *x = dynamic_cast<ChatMessageItemContainer *>(widget->second);
if (x != nullptr) {
x->UpdateContent();
x->UpdateAttributes();
}
m_chat->RefetchMessage(id);
}
void ChatWindow::AddNewHistory(const std::vector<Snowflake> &id) {
std::set<Snowflake> ids(id.begin(), id.end());
for (auto it = ids.rbegin(); it != ids.rend(); it++)
ProcessNewMessage(*it, true);
void ChatWindow::AddNewHistory(const std::vector<Message> &msgs) {
m_chat->PrependMessages(msgs.crbegin(), msgs.crend());
}
void ChatWindow::InsertChatInput(std::string text) {
@@ -181,15 +139,16 @@ void ChatWindow::InsertChatInput(std::string text) {
}
Snowflake ChatWindow::GetOldestListedMessage() {
return m_id_to_widget.begin()->first;
return m_chat->GetOldestListedMessage();
}
void ChatWindow::UpdateReactions(Snowflake id) {
auto it = m_id_to_widget.find(id);
if (it == m_id_to_widget.end()) return;
auto *widget = dynamic_cast<ChatMessageItemContainer *>(it->second);
if (widget == nullptr) return;
widget->UpdateReactions();
m_chat->UpdateMessageReactions(id);
}
void ChatWindow::SetTopic(const std::string &text) {
m_topic_text.set_text(text);
m_topic.set_visible(text.length() > 0);
}
Snowflake ChatWindow::GetActiveChannel() const {
@@ -200,6 +159,9 @@ bool ChatWindow::OnInputSubmit(const Glib::ustring &text) {
if (!m_rate_limit_indicator->CanSpeak())
return false;
if (text.size() == 0)
return false;
if (m_active_channel.IsValid())
m_signal_action_chat_submit.emit(text, m_active_channel, m_replying_to); // m_replying_to is checked for invalid in the handler
if (m_is_replying)
@@ -218,122 +180,6 @@ bool ChatWindow::OnKeyPressEvent(GdkEventKey *e) {
return false;
}
ChatMessageItemContainer *ChatWindow::CreateMessageComponent(Snowflake id) {
auto *container = ChatMessageItemContainer::FromMessage(id);
return container;
}
void ChatWindow::RemoveMessageAndHeader(Gtk::Widget *widget) {
ChatMessageHeader *header = dynamic_cast<ChatMessageHeader *>(widget->get_ancestor(Gtk::ListBoxRow::get_type()));
if (header != nullptr) {
if (header->GetChildContent().size() == 1) {
m_num_rows--;
delete header;
} else
delete widget;
} else
delete widget;
m_num_messages--;
}
constexpr static int MaxMessagesForCull = 50; // this has to be 50 cuz that magic number is used in a couple other places and i dont feel like replacing them
void ChatWindow::ProcessNewMessage(Snowflake id, bool prepend) {
const auto &client = Abaddon::Get().GetDiscordClient();
if (!client.IsStarted()) return; // e.g. load channel and then dc
const auto data = client.GetMessage(id);
if (!data.has_value()) return;
if (!data->IsPending && data->Nonce.has_value() && data->Author.ID == client.GetUserData().ID) {
for (auto [id, widget] : m_id_to_widget) {
if (dynamic_cast<ChatMessageItemContainer *>(widget)->Nonce == *data->Nonce) {
RemoveMessageAndHeader(widget);
m_id_to_widget.erase(id);
break;
}
}
}
ChatMessageHeader *last_row = nullptr;
bool should_attach = false;
if (m_num_rows > 0) {
if (prepend)
last_row = dynamic_cast<ChatMessageHeader *>(m_list->get_row_at_index(0));
else
last_row = dynamic_cast<ChatMessageHeader *>(m_list->get_row_at_index(m_num_rows - 1));
if (last_row != nullptr) {
const uint64_t diff = std::max(id, last_row->NewestID) - std::min(id, last_row->NewestID);
if (last_row->UserID == data->Author.ID && (prepend || (diff < SnowflakeSplitDifference * Snowflake::SecondsInterval)))
should_attach = true;
}
}
m_num_messages++;
if (m_should_scroll_to_bottom && !prepend)
while (m_num_messages > MaxMessagesForCull) {
auto first_it = m_id_to_widget.begin();
RemoveMessageAndHeader(first_it->second);
m_id_to_widget.erase(first_it);
}
ChatMessageHeader *header;
if (should_attach) {
header = last_row;
} else {
const auto guild_id = *client.GetChannel(m_active_channel)->GuildID;
const auto user_id = data->Author.ID;
const auto user = client.GetUser(user_id);
if (!user.has_value()) return;
header = Gtk::manage(new ChatMessageHeader(&*data));
header->signal_action_insert_mention().connect([this, user_id]() {
m_signal_action_insert_mention.emit(user_id);
});
header->signal_action_open_user_menu().connect([this, user_id, guild_id](const GdkEvent *event) {
m_signal_action_open_user_menu.emit(event, user_id, guild_id);
});
m_num_rows++;
}
auto *content = CreateMessageComponent(id);
if (content != nullptr) {
header->AddContent(content, prepend);
m_id_to_widget[id] = content;
if (!data->IsPending) {
content->signal_action_delete().connect([this, id] {
m_signal_action_message_delete.emit(m_active_channel, id);
});
content->signal_action_edit().connect([this, id] {
m_signal_action_message_edit.emit(m_active_channel, id);
});
content->signal_action_reaction_add().connect([this, id](const Glib::ustring &param) {
m_signal_action_reaction_add.emit(id, param);
});
content->signal_action_reaction_remove().connect([this, id](const Glib::ustring &param) {
m_signal_action_reaction_remove.emit(id, param);
});
content->signal_action_channel_click().connect([this](const Snowflake &id) {
m_signal_action_channel_click.emit(id);
});
content->signal_action_reply_to().connect(sigc::mem_fun(*this, &ChatWindow::StartReplying));
}
}
header->set_margin_left(5);
header->show_all();
if (!should_attach) {
if (prepend)
m_list->prepend(*header);
else
m_list->add(*header);
}
}
void ChatWindow::StartReplying(Snowflake message_id) {
const auto &discord = Abaddon::Get().GetDiscordClient();
const auto message = *discord.GetMessage(message_id);
@@ -360,22 +206,8 @@ void ChatWindow::OnScrollEdgeOvershot(Gtk::PositionType pos) {
m_signal_action_chat_load_history.emit(m_active_channel);
}
void ChatWindow::ScrollToBottom() {
auto x = m_scroll->get_vadjustment();
x->set_value(x->get_upper());
}
void ChatWindow::OnMessageSendFail(const std::string &nonce, float retry_after) {
for (auto [id, widget] : m_id_to_widget) {
if (auto *container = dynamic_cast<ChatMessageItemContainer *>(widget); container->Nonce == nonce) {
container->SetFailed();
break;
}
}
}
ChatWindow::type_signal_action_message_delete ChatWindow::signal_action_message_delete() {
return m_signal_action_message_delete;
m_chat->SetFailedByNonce(nonce);
}
ChatWindow::type_signal_action_message_edit ChatWindow::signal_action_message_edit() {
@@ -398,10 +230,6 @@ ChatWindow::type_signal_action_insert_mention ChatWindow::signal_action_insert_m
return m_signal_action_insert_mention;
}
ChatWindow::type_signal_action_open_user_menu ChatWindow::signal_action_open_user_menu() {
return m_signal_action_open_user_menu;
}
ChatWindow::type_signal_action_reaction_add ChatWindow::signal_action_reaction_add() {
return m_signal_action_reaction_add;
}

View File

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

View File

@@ -291,7 +291,7 @@ bool MultiBackwardSearch(const Gtk::TextIter &iter, const Glib::ustring &chars,
if (!iter.backward_search(tmp, flags, tstart, tend)) continue;
// if previous found, compare to see if closer to out iter
if (any) {
if (tstart > out)
if (tstart.get_offset() > out.get_offset())
out = tstart;
} else
out = tstart;
@@ -308,7 +308,7 @@ bool MultiForwardSearch(const Gtk::TextIter &iter, const Glib::ustring &chars, G
if (!iter.forward_search(tmp, flags, tstart, tend)) continue;
// if previous found, compare to see if closer to out iter
if (any) {
if (tstart < out)
if (tstart.get_offset() < out.get_offset())
out = tstart;
} else
out = tstart;

354
components/friendslist.cpp Normal file
View File

@@ -0,0 +1,354 @@
#include "friendslist.hpp"
#include "../abaddon.hpp"
#include "lazyimage.hpp"
using namespace std::string_literals;
FriendsList::FriendsList()
: Gtk::Box(Gtk::ORIENTATION_VERTICAL)
, m_filter_mode(FILTER_FRIENDS) {
get_style_context()->add_class("friends-list");
auto &discord = Abaddon::Get().GetDiscordClient();
discord.signal_relationship_add().connect(sigc::mem_fun(*this, &FriendsList::OnRelationshipAdd));
discord.signal_relationship_remove().connect(sigc::mem_fun(*this, &FriendsList::OnRelationshipRemove));
PopulateRelationships();
signal_map().connect(sigc::mem_fun(*this, &FriendsList::PopulateRelationships));
constexpr static std::array<const char *, 4> strs = {
"Friends",
"Online",
"Pending",
"Blocked",
};
for (const auto &x : strs) {
auto *btn = Gtk::manage(new Gtk::RadioButton(m_group, x));
m_buttons.add(*btn);
btn->show();
btn->signal_toggled().connect([this, btn, str = x] {
if (!btn->get_active()) return;
switch (str[0]) { // hehe
case 'F':
m_filter_mode = FILTER_FRIENDS;
break;
case 'O':
m_filter_mode = FILTER_ONLINE;
break;
case 'P':
m_filter_mode = FILTER_PENDING;
break;
case 'B':
m_filter_mode = FILTER_BLOCKED;
break;
}
m_list.invalidate_filter();
});
}
m_buttons.set_homogeneous(true);
m_buttons.set_halign(Gtk::ALIGN_CENTER);
m_add.set_halign(Gtk::ALIGN_CENTER);
m_add.set_margin_top(5);
m_add.set_margin_bottom(5);
m_scroll.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC);
m_list.set_sort_func(sigc::mem_fun(*this, &FriendsList::ListSortFunc));
m_list.set_filter_func(sigc::mem_fun(*this, &FriendsList::ListFilterFunc));
m_list.set_selection_mode(Gtk::SELECTION_NONE);
m_list.set_hexpand(true);
m_list.set_vexpand(true);
m_scroll.add(m_list);
add(m_add);
add(m_buttons);
add(m_scroll);
m_add.show();
m_scroll.show();
m_buttons.show();
m_list.show();
}
FriendsListFriendRow *FriendsList::MakeRow(const UserData &user, RelationshipType type) {
auto *row = Gtk::manage(new FriendsListFriendRow(type, user));
row->signal_action_remove().connect(sigc::bind(sigc::mem_fun(*this, &FriendsList::OnActionRemove), user.ID));
row->signal_action_accept().connect(sigc::bind(sigc::mem_fun(*this, &FriendsList::OnActionAccept), user.ID));
return row;
}
void FriendsList::OnRelationshipAdd(const RelationshipAddData &data) {
for (auto *row_ : m_list.get_children()) {
auto *row = dynamic_cast<FriendsListFriendRow *>(row_);
if (row == nullptr || row->ID != data.ID) continue;
delete row;
break;
}
auto *row = MakeRow(data.User, data.Type);
m_list.add(*row);
row->show();
}
void FriendsList::OnRelationshipRemove(Snowflake id, RelationshipType type) {
for (auto *row_ : m_list.get_children()) {
auto *row = dynamic_cast<FriendsListFriendRow *>(row_);
if (row == nullptr || row->ID != id) continue;
delete row;
return;
}
}
void FriendsList::OnActionAccept(Snowflake id) {
const auto cb = [this](DiscordError code) {
if (code != DiscordError::NONE) {
Gtk::MessageDialog dlg(*dynamic_cast<Gtk::Window *>(get_toplevel()), "Failed to accept", false, Gtk::MESSAGE_ERROR, Gtk::BUTTONS_OK, true);
dlg.set_position(Gtk::WIN_POS_CENTER);
dlg.run();
}
};
Abaddon::Get().GetDiscordClient().PutRelationship(id, sigc::track_obj(cb, *this));
}
void FriendsList::OnActionRemove(Snowflake id) {
auto &discord = Abaddon::Get().GetDiscordClient();
const auto user = discord.GetUser(id);
if (auto *window = dynamic_cast<Gtk::Window *>(get_toplevel())) {
Glib::ustring str;
switch (*discord.GetRelationship(id)) {
case RelationshipType::Blocked:
str = "Are you sure you want to unblock " + user->Username + "#" + user->Discriminator + "?";
break;
case RelationshipType::Friend:
str = "Are you sure you want to remove " + user->Username + "#" + user->Discriminator + "?";
break;
case RelationshipType::PendingIncoming:
str = "Are you sure you want to ignore " + user->Username + "#" + user->Discriminator + "?";
break;
case RelationshipType::PendingOutgoing:
str = "Are you sure you want to cancel your request to " + user->Username + "#" + user->Discriminator + "?";
break;
default:
break;
}
if (Abaddon::Get().ShowConfirm(str, window)) {
const auto cb = [this, window](DiscordError code) {
if (code == DiscordError::NONE) return;
Gtk::MessageDialog dlg(*window, "Failed to remove user", false, Gtk::MESSAGE_ERROR, Gtk::BUTTONS_OK, true);
dlg.set_position(Gtk::WIN_POS_CENTER);
dlg.run();
};
discord.RemoveRelationship(id, sigc::track_obj(cb, *this));
}
}
}
void FriendsList::PopulateRelationships() {
for (auto child : m_list.get_children())
delete child;
auto &discord = Abaddon::Get().GetDiscordClient();
for (const auto &[id, type] : discord.GetRelationships()) {
const auto user = discord.GetUser(id);
if (!user.has_value()) continue;
auto *row = MakeRow(*user, type);
m_list.add(*row);
row->show();
}
}
int FriendsList::ListSortFunc(Gtk::ListBoxRow *a_, Gtk::ListBoxRow *b_) {
auto *a = dynamic_cast<FriendsListFriendRow *>(a_);
auto *b = dynamic_cast<FriendsListFriendRow *>(b_);
if (a == nullptr || b == nullptr) return 0;
return a->Name.compare(b->Name);
}
bool FriendsList::ListFilterFunc(Gtk::ListBoxRow *row_) {
auto *row = dynamic_cast<FriendsListFriendRow *>(row_);
if (row == nullptr) return false;
switch (m_filter_mode) {
case FILTER_FRIENDS:
return row->Type == RelationshipType::Friend;
case FILTER_ONLINE:
return row->Type == RelationshipType::Friend && row->Status != PresenceStatus::Offline;
case FILTER_PENDING:
return row->Type == RelationshipType::PendingIncoming || row->Type == RelationshipType::PendingOutgoing;
case FILTER_BLOCKED:
return row->Type == RelationshipType::Blocked;
default:
return false;
}
}
FriendsListAddComponent::FriendsListAddComponent()
: Gtk::Box(Gtk::ORIENTATION_VERTICAL)
, m_label("Add a Friend", Gtk::ALIGN_START)
, m_box(Gtk::ORIENTATION_HORIZONTAL)
, m_add("Add")
, m_status("", Gtk::ALIGN_START) {
m_box.add(m_entry);
m_box.add(m_add);
m_box.add(m_status);
m_add.signal_clicked().connect(sigc::mem_fun(*this, &FriendsListAddComponent::Submit));
m_label.set_halign(Gtk::ALIGN_CENTER);
m_entry.set_placeholder_text("Enter a Username#1234");
m_entry.signal_key_press_event().connect(sigc::mem_fun(*this, &FriendsListAddComponent::OnKeyPress), false);
add(m_label);
add(m_box);
show_all_children();
}
void FriendsListAddComponent::Submit() {
if (m_requesting) return;
auto text = m_entry.get_text();
m_label.set_text("Invalid input"); // cheeky !!
m_entry.set_text("");
const auto hashpos = text.find("#");
if (hashpos == Glib::ustring::npos) return;
const auto username = text.substr(0, hashpos);
const auto discriminator = text.substr(hashpos + 1);
if (username.size() == 0 || discriminator.size() != 4) return;
if (discriminator.find_first_not_of("0123456789") != Glib::ustring::npos) return;
m_requesting = true;
m_label.set_text("Hang on...");
const auto cb = [this](DiscordError code) {
m_requesting = false;
if (code == DiscordError::NONE) {
m_label.set_text("Success!");
} else {
m_label.set_text("Failed: "s + GetDiscordErrorDisplayString(code));
}
};
Abaddon::Get().GetDiscordClient().SendFriendRequest(username, std::stoul(discriminator), sigc::track_obj(cb, *this));
}
bool FriendsListAddComponent::OnKeyPress(GdkEventKey *e) {
if (e->keyval == GDK_KEY_Return) {
Submit();
return true;
}
return false;
}
FriendsListFriendRow::FriendsListFriendRow(RelationshipType type, const UserData &data)
: Name(data.Username + "#" + data.Discriminator)
, Type(type)
, ID(data.ID)
, Status(Abaddon::Get().GetDiscordClient().GetUserStatus(data.ID))
, m_accept("Accept") {
auto *ev = Gtk::manage(new Gtk::EventBox);
auto *box = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL));
auto *img = Gtk::manage(new LazyImage(32, 32, true));
auto *namebox = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL));
auto *namelbl = Gtk::manage(new Gtk::Label("", Gtk::ALIGN_START));
m_status_lbl = Gtk::manage(new Gtk::Label("", Gtk::ALIGN_START));
auto *lblbox = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_VERTICAL));
auto &discord = Abaddon::Get().GetDiscordClient();
discord.signal_presence_update().connect(sigc::mem_fun(*this, &FriendsListFriendRow::OnPresenceUpdate));
static bool show_animations = Abaddon::Get().GetSettings().GetShowAnimations();
if (data.HasAnimatedAvatar() && show_animations) {
img->SetAnimated(true);
img->SetURL(data.GetAvatarURL("gif", "32"));
} else {
img->SetURL(data.GetAvatarURL("png", "32"));
}
namelbl->set_markup(data.GetEscapedBoldName());
UpdatePresenceLabel();
AddWidgetMenuHandler(ev, m_menu, [this] {
m_accept.set_visible(Type == RelationshipType::PendingIncoming);
switch (Type) {
case RelationshipType::Blocked:
case RelationshipType::Friend:
m_remove.set_label("Remove");
break;
case RelationshipType::PendingIncoming:
m_remove.set_label("Ignore");
break;
case RelationshipType::PendingOutgoing:
m_remove.set_label("Cancel");
break;
default:
break;
}
});
m_remove.signal_activate().connect([this] {
m_signal_remove.emit();
});
m_accept.signal_activate().connect([this] {
m_signal_accept.emit();
});
m_menu.append(m_accept);
m_menu.append(m_remove);
m_menu.show_all();
lblbox->set_valign(Gtk::ALIGN_CENTER);
img->set_margin_end(5);
namebox->add(*namelbl);
lblbox->add(*namebox);
lblbox->add(*m_status_lbl);
box->add(*img);
box->add(*lblbox);
ev->add(*box);
add(*ev);
show_all_children();
}
void FriendsListFriendRow::UpdatePresenceLabel() {
switch (Type) {
case RelationshipType::PendingIncoming:
m_status_lbl->set_text("Incoming Friend Request");
break;
case RelationshipType::PendingOutgoing:
m_status_lbl->set_text("Outgoing Friend Request");
break;
default:
m_status_lbl->set_text(GetPresenceDisplayString(Status));
break;
}
}
void FriendsListFriendRow::OnPresenceUpdate(const UserData &user, PresenceStatus status) {
if (user.ID != ID) return;
Status = status;
UpdatePresenceLabel();
changed();
}
FriendsListFriendRow::type_signal_remove FriendsListFriendRow::signal_action_remove() {
return m_signal_remove;
}
FriendsListFriendRow::type_signal_accept FriendsListFriendRow::signal_action_accept() {
return m_signal_accept;
}
FriendsListWindow::FriendsListWindow() {
add(m_friends);
set_default_size(500, 500);
get_style_context()->add_class("app-window");
get_style_context()->add_class("app-popup");
m_friends.show();
}

View File

@@ -0,0 +1,92 @@
#pragma once
#include <gtkmm.h>
#include "../discord/objects.hpp"
class FriendsListAddComponent : public Gtk::Box {
public:
FriendsListAddComponent();
private:
void Submit();
bool OnKeyPress(GdkEventKey *e);
Gtk::Label m_label;
Gtk::Label m_status;
Gtk::Entry m_entry;
Gtk::Button m_add;
Gtk::Box m_box;
bool m_requesting = false;
};
class FriendsListFriendRow;
class FriendsList : public Gtk::Box {
public:
FriendsList();
private:
FriendsListFriendRow *MakeRow(const UserData &user, RelationshipType type);
void OnRelationshipAdd(const RelationshipAddData &data);
void OnRelationshipRemove(Snowflake id, RelationshipType type);
void OnActionAccept(Snowflake id);
void OnActionRemove(Snowflake id);
void PopulateRelationships();
enum FilterMode {
FILTER_FRIENDS,
FILTER_ONLINE,
FILTER_PENDING,
FILTER_BLOCKED,
};
FilterMode m_filter_mode;
int ListSortFunc(Gtk::ListBoxRow *a, Gtk::ListBoxRow *b);
bool ListFilterFunc(Gtk::ListBoxRow *row);
FriendsListAddComponent m_add;
Gtk::RadioButtonGroup m_group;
Gtk::ButtonBox m_buttons;
Gtk::ScrolledWindow m_scroll;
Gtk::ListBox m_list;
};
class FriendsListFriendRow : public Gtk::ListBoxRow {
public:
FriendsListFriendRow(RelationshipType type, const UserData &str);
Snowflake ID;
RelationshipType Type;
Glib::ustring Name;
PresenceStatus Status;
private:
void UpdatePresenceLabel();
void OnPresenceUpdate(const UserData &user, PresenceStatus status);
Gtk::Label *m_status_lbl;
Gtk::Menu m_menu;
Gtk::MenuItem m_remove; // or cancel or ignore
Gtk::MenuItem m_accept; // incoming
using type_signal_remove = sigc::signal<void>;
using type_signal_accept = sigc::signal<void>;
type_signal_remove m_signal_remove;
type_signal_accept m_signal_accept;
public:
type_signal_remove signal_action_remove();
type_signal_accept signal_action_accept();
};
class FriendsListWindow : public Gtk::Window {
public:
FriendsListWindow();
private:
FriendsList m_friends;
};

View File

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

View File

@@ -6,7 +6,7 @@
constexpr static const int MaxMemberListRows = 200;
MemberListUserRow::MemberListUserRow(const GuildData *guild, const UserData &data) {
MemberListUserRow::MemberListUserRow(const std::optional<GuildData> &guild, const UserData &data) {
ID = data.ID;
m_ev = Gtk::manage(new Gtk::EventBox);
m_box = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL));
@@ -15,9 +15,10 @@ MemberListUserRow::MemberListUserRow(const GuildData *guild, const UserData &dat
m_status_indicator = Gtk::manage(new StatusIndicator(ID));
static bool crown = Abaddon::Get().GetSettings().GetShowOwnerCrown();
if (crown && guild != nullptr && guild->OwnerID == data.ID) {
if (crown && guild.has_value() && guild->OwnerID == data.ID) {
try {
auto pixbuf = Gdk::Pixbuf::create_from_file("./res/crown.png", 12, 12);
const static auto crown_path = Abaddon::GetResPath("/crown.png");
auto pixbuf = Gdk::Pixbuf::create_from_file(crown_path, 12, 12);
m_crown = Gtk::manage(new Gtk::Image(pixbuf));
m_crown->set_valign(Gtk::ALIGN_CENTER);
m_crown->set_margin_end(8);
@@ -26,7 +27,10 @@ MemberListUserRow::MemberListUserRow(const GuildData *guild, const UserData &dat
m_status_indicator->set_margin_start(3);
m_avatar->SetURL(data.GetAvatarURL("png"));
if (guild.has_value())
m_avatar->SetURL(data.GetAvatarURL(guild->ID, "png"));
else
m_avatar->SetURL(data.GetAvatarURL("png"));
get_style_context()->add_class("members-row");
get_style_context()->add_class("members-row-member");
@@ -40,7 +44,7 @@ MemberListUserRow::MemberListUserRow(const GuildData *guild, const UserData &dat
std::string display = data.Username;
if (show_discriminator)
display += "#" + data.Discriminator;
if (guild != nullptr) {
if (guild.has_value()) {
if (const auto col_id = data.GetHoistedRole(guild->ID, true); col_id.IsValid()) {
auto color = Abaddon::Get().GetDiscordClient().GetRole(col_id)->Color;
m_label->set_use_markup(true);
@@ -114,7 +118,7 @@ void MemberList::UpdateMemberList() {
int num_rows = 0;
for (const auto &user : chan->GetDMRecipients()) {
if (num_rows++ > MaxMemberListRows) break;
auto *row = Gtk::manage(new MemberListUserRow(nullptr, user));
auto *row = Gtk::manage(new MemberListUserRow(std::nullopt, user));
m_id_to_row[user.ID] = row;
AttachUserMenuHandler(row, user.ID);
m_listbox->add(*row);
@@ -123,7 +127,12 @@ void MemberList::UpdateMemberList() {
return;
}
auto ids = discord.GetUsersInGuild(m_guild_id);
std::set<Snowflake> ids;
if (chan->IsThread()) {
const auto x = discord.GetUsersInThread(m_chan_id);
ids = { x.begin(), x.end() };
} else
ids = discord.GetUsersInGuild(m_guild_id);
// process all the shit first so its in proper order
std::map<int, RoleData> pos_to_role;
@@ -156,7 +165,7 @@ void MemberList::UpdateMemberList() {
const auto guild = *discord.GetGuild(m_guild_id);
auto add_user = [this, &user_to_color, &num_rows, guild](const UserData &data) -> bool {
if (num_rows++ > MaxMemberListRows) return false;
auto *row = Gtk::manage(new MemberListUserRow(&guild, data));
auto *row = Gtk::manage(new MemberListUserRow(guild, data));
m_id_to_row[data.ID] = row;
AttachUserMenuHandler(row, data.ID);
m_listbox->add(*row);
@@ -210,14 +219,10 @@ void MemberList::UpdateMemberList() {
void MemberList::AttachUserMenuHandler(Gtk::ListBoxRow *row, Snowflake id) {
row->signal_button_press_event().connect([this, row, id](GdkEventButton *e) -> bool {
if (e->type == GDK_BUTTON_PRESS && e->button == GDK_BUTTON_SECONDARY) {
m_signal_action_show_user_menu.emit(reinterpret_cast<const GdkEvent *>(e), id, m_guild_id);
Abaddon::Get().ShowUserMenu(reinterpret_cast<const GdkEvent *>(e), id, m_guild_id);
return true;
}
return false;
});
}
MemberList::type_signal_action_show_user_menu MemberList::signal_action_show_user_menu() {
return m_signal_action_show_user_menu;
}

View File

@@ -9,7 +9,7 @@ class LazyImage;
class StatusIndicator;
class MemberListUserRow : public Gtk::ListBoxRow {
public:
MemberListUserRow(const GuildData *guild, const UserData &data);
MemberListUserRow(const std::optional<GuildData> &guild, const UserData &data);
Snowflake ID;
@@ -41,12 +41,4 @@ private:
Snowflake m_chan_id;
std::unordered_map<Snowflake, Gtk::ListBoxRow *> m_id_to_row;
public:
typedef sigc::signal<void, const GdkEvent *, Snowflake, Snowflake> type_signal_action_show_user_menu;
type_signal_action_show_user_menu signal_action_show_user_menu();
private:
type_signal_action_show_user_menu m_signal_action_show_user_menu;
};

View File

@@ -15,9 +15,10 @@ RateLimitIndicator::RateLimitIndicator()
add(m_img);
m_label.show();
if (std::filesystem::exists("./res/clock.png")) {
const static auto clock_path = Abaddon::GetResPath("/clock.png");
if (std::filesystem::exists(clock_path)) {
try {
const auto pixbuf = Gdk::Pixbuf::create_from_file("./res/clock.png");
const auto pixbuf = Gdk::Pixbuf::create_from_file(clock_path);
int w, h;
GetImageDimensions(pixbuf->get_width(), pixbuf->get_height(), w, h, 20, 10);
m_img.property_pixbuf() = pixbuf->scale_simple(w, h, Gdk::INTERP_BILINEAR);
@@ -31,9 +32,9 @@ RateLimitIndicator::RateLimitIndicator()
void RateLimitIndicator::SetActiveChannel(Snowflake id) {
m_active_channel = id;
const auto channel = *Abaddon::Get().GetDiscordClient().GetChannel(m_active_channel);
if (channel.RateLimitPerUser.has_value())
m_rate_limit = *channel.RateLimitPerUser;
const auto channel = Abaddon::Get().GetDiscordClient().GetChannel(m_active_channel);
if (channel.has_value() && channel->RateLimitPerUser.has_value())
m_rate_limit = *channel->RateLimitPerUser;
else
m_rate_limit = 0;
@@ -123,7 +124,10 @@ void RateLimitIndicator::OnMessageSendFail(const std::string &nonce, float retry
}
void RateLimitIndicator::OnChannelUpdate(Snowflake channel_id) {
const auto r = Abaddon::Get().GetDiscordClient().GetChannel(channel_id)->RateLimitPerUser;
if (channel_id != m_active_channel) return;
const auto chan = Abaddon::Get().GetDiscordClient().GetChannel(m_active_channel);
if (!chan.has_value()) return;
const auto r = chan->RateLimitPerUser;
if (r.has_value())
m_rate_limit = *r;
else

View File

@@ -18,8 +18,8 @@ StatusIndicator::StatusIndicator(Snowflake user_id)
get_style_context()->add_class("status-indicator");
Abaddon::Get().GetDiscordClient().signal_guild_member_list_update().connect(sigc::hide(sigc::mem_fun(*this, &StatusIndicator::CheckStatus)));
auto cb = [this](Snowflake id, PresenceStatus status) {
if (id == m_id) CheckStatus();
auto cb = [this](const UserData &user, PresenceStatus status) {
if (user.ID == m_id) CheckStatus();
};
Abaddon::Get().GetDiscordClient().signal_presence_update().connect(sigc::track_obj(cb, *this));

1
config.h.in Normal file
View File

@@ -0,0 +1 @@
#define ABADDON_DEFAULT_RESOURCE_DIR "@ABADDON_RESOURCE_DIR@"

4
constants.hpp Normal file
View File

@@ -0,0 +1,4 @@
#include <cstdint>
constexpr static uint64_t SnowflakeSplitDifference = 600;
constexpr static int MaxMessagesForChatCull = 50; // this has to be 50 (for now) cuz that magic number is used in a couple other places and i dont feel like replacing them

View File

@@ -0,0 +1,89 @@
/*
application wide stuff
has to be separate to allow main.css to override certain things
*/
.app-window label:not(:disabled) {
color: @text_color;
}
.app-window entry {
background: @secondary_color;
color: @text_color;
border: 1px solid #1c2e40;
}
.app-window button {
background: @secondary_color;
color: @text_color;
text-shadow: none;
box-shadow: none;
}
.app-window button:checked {
border-top: 0px;
border-left: 0px;
border-right: 0px;
border-bottom: 3px solid #39a2ed;
color: #ffffff;
}
.app-window button:not(:checked) {
border: 3px #0000ff;
}
.app-window.background {
background: @background_color;
}
.app-window treeview {
color: @text_color;
}
.app-window treeview:not(:selected) {
background: @secondary_color;
}
.app-popup list {
background: @secondary_color;
}
.app-window paned separator {
background: @background_color;
}
.app-window scrollbar {
background: @background_color;
border-left: 1px solid transparent;
}
.app-window menubar, menu {
background: @background_color;
color: #cccccc;
}
.app-window textview text {
caret-color: #ababab;
}
.app-window check,
.app-window radio {
background-clip: padding-box;
background: @secondary_color;
border-color: #070707;
box-shadow: 0 1px rgba(0, 0, 0, 0);
color: #dddddd;
}
.app-window check:checked,
.app-window radio:checked {
background-clip: border-box;
background: #0b4285;
border-color: #092444;
box-shadow: 0 1px rgba(0, 0, 0, 0);
color: #dddddd;
}
.app-window colorswatch {
box-shadow: 0 1px rgba(0, 0, 0, 0);
}

View File

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

View File

@@ -1,5 +1,12 @@
#include "token.hpp"
std::string trim(const std::string& str) {
const auto first = str.find_first_not_of(' ');
if (first == std::string::npos) return str;
const auto last = str.find_last_not_of(' ');
return str.substr(first, last - first + 1);
}
TokenDialog::TokenDialog(Gtk::Window &parent)
: Gtk::Dialog("Set Token", parent, true)
, m_layout(Gtk::ORIENTATION_VERTICAL)
@@ -11,7 +18,7 @@ TokenDialog::TokenDialog(Gtk::Window &parent)
get_style_context()->add_class("app-popup");
m_ok.signal_clicked().connect([&]() {
m_token = m_entry.get_text();
m_token = trim(m_entry.get_text());
response(Gtk::RESPONSE_OK);
});

View File

@@ -26,6 +26,20 @@ constexpr inline const char *GetPresenceString(PresenceStatus s) {
return "";
}
constexpr inline const char* GetPresenceDisplayString(PresenceStatus s) {
switch (s) {
case PresenceStatus::Online:
return "Online";
case PresenceStatus::Offline:
return "Offline";
case PresenceStatus::Idle:
return "Away";
case PresenceStatus::DND:
return "Do Not Disturb";
}
return "";
}
enum class ActivityType : int {
Game = 0,
Streaming = 1,

View File

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

View File

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

View File

@@ -1,6 +1,20 @@
#include "../abaddon.hpp"
#include "channel.hpp"
void from_json(const nlohmann::json &j, ThreadMetadataData &m) {
JS_D("archived", m.IsArchived);
JS_D("auto_archive_duration", m.AutoArchiveDuration);
JS_D("archive_timestamp", m.ArchiveTimestamp);
JS_O("locked", m.IsLocked);
}
void from_json(const nlohmann::json &j, ThreadMemberObject &m) {
JS_O("id", m.ThreadID);
JS_O("user_id", m.UserID);
JS_D("join_timestamp", m.JoinTimestamp);
JS_D("flags", m.Flags);
}
void from_json(const nlohmann::json &j, ChannelData &m) {
JS_D("id", m.ID);
JS_D("type", m.Type);
@@ -21,6 +35,8 @@ void from_json(const nlohmann::json &j, ChannelData &m) {
JS_O("application_id", m.ApplicationID);
JS_ON("parent_id", m.ParentID);
JS_ON("last_pin_timestamp", m.LastPinTimestamp);
JS_O("thread_metadata", m.ThreadMetadata);
JS_O("member", m.ThreadMember);
}
void ChannelData::update_from_json(const nlohmann::json &j) {
@@ -43,6 +59,20 @@ void ChannelData::update_from_json(const nlohmann::json &j) {
JS_RD("last_pin_timestamp", LastPinTimestamp);
}
bool ChannelData::NSFW() const {
return IsNSFW.has_value() && *IsNSFW;
}
bool ChannelData::IsThread() const noexcept {
return Type == ChannelType::GUILD_PUBLIC_THREAD ||
Type == ChannelType::GUILD_PRIVATE_THREAD ||
Type == ChannelType::GUILD_NEWS_THREAD;
}
bool ChannelData::IsJoinedThread() const {
return Abaddon::Get().GetDiscordClient().IsThreadJoined(ID);
}
std::optional<PermissionOverwrite> ChannelData::GetOverwrite(Snowflake id) const {
return Abaddon::Get().GetDiscordClient().GetPermissionOverwrite(ID, id);
}

View File

@@ -15,12 +15,49 @@ enum class ChannelType : int {
GUILD_NEWS = 5,
GUILD_STORE = 6,
/* 7 and 8 were used for LFG */
/* 9 and 10 were used for threads */
PUBLIC_THREAD = 11,
PRIVATE_THREAD = 12,
/* 9 was used for threads */
GUILD_NEWS_THREAD = 10,
GUILD_PUBLIC_THREAD = 11,
GUILD_PRIVATE_THREAD = 12,
GUILD_STAGE_VOICE = 13,
};
enum class StagePrivacy {
PUBLIC = 1,
GUILD_ONLY = 2,
};
constexpr const char *GetStagePrivacyDisplayString(StagePrivacy e) {
switch (e) {
case StagePrivacy::PUBLIC:
return "Public";
case StagePrivacy::GUILD_ONLY:
return "Guild Only";
default:
return "Unknown";
}
}
// should be moved somewhere?
struct ThreadMetadataData {
bool IsArchived;
int AutoArchiveDuration;
std::string ArchiveTimestamp;
std::optional<bool> IsLocked;
friend void from_json(const nlohmann::json &j, ThreadMetadataData &m);
};
struct ThreadMemberObject {
std::optional<Snowflake> ThreadID;
std::optional<Snowflake> UserID;
std::string JoinTimestamp;
int Flags;
friend void from_json(const nlohmann::json &j, ThreadMemberObject &m);
};
struct ChannelData {
Snowflake ID;
ChannelType Type;
@@ -41,10 +78,15 @@ struct ChannelData {
std::optional<Snowflake> ApplicationID;
std::optional<Snowflake> ParentID; // null
std::optional<std::string> LastPinTimestamp; // null
std::optional<ThreadMetadataData> ThreadMetadata;
std::optional<ThreadMemberObject> ThreadMember;
friend void from_json(const nlohmann::json &j, ChannelData &m);
void update_from_json(const nlohmann::json &j);
bool NSFW() const;
bool IsThread() const noexcept;
bool IsJoinedThread() const;
std::optional<PermissionOverwrite> GetOverwrite(Snowflake id) const;
std::vector<UserData> GetDMRecipients() const;
};

File diff suppressed because it is too large Load Diff

View File

@@ -6,20 +6,17 @@
#include <sigc++/sigc++.h>
#include <nlohmann/json.hpp>
#include <thread>
#include <unordered_map>
#include <map>
#include <set>
#include <unordered_set>
#include <mutex>
#include <zlib.h>
#include <glibmm.h>
#include <queue>
// bruh
#ifdef GetMessage
#undef GetMessage
#endif
// https://stackoverflow.com/questions/29775153/stopping-long-sleep-threads/29775639#29775639
class HeartbeatWaiter {
public:
template<class R, class P>
@@ -49,10 +46,6 @@ class Abaddon;
class DiscordClient {
friend class Abaddon;
public:
static const constexpr char *DiscordGateway = "wss://gateway.discord.gg/?v=9&encoding=json&compress=zlib-stream";
static const constexpr char *DiscordAPI = "https://discord.com/api/v9";
public:
DiscordClient(bool mem_store = false);
void Start();
@@ -72,13 +65,14 @@ public:
const UserData &GetUserData() const;
const UserSettings &GetUserSettings() const;
std::vector<Snowflake> GetUserSortedGuilds() const;
std::set<Snowflake> GetMessagesForChannel(Snowflake id) const;
std::vector<Message> GetMessagesForChannel(Snowflake id, size_t limit = 50) const;
std::vector<Snowflake> GetMessageIDsForChannel(Snowflake id) const;
std::set<Snowflake> GetPrivateChannels() const;
EPremiumType GetSelfPremiumType() const;
void FetchMessagesInChannel(Snowflake id, std::function<void(const std::vector<Snowflake> &)> cb);
void FetchMessagesInChannelBefore(Snowflake channel_id, Snowflake before_id, std::function<void(const std::vector<Snowflake> &)> cb);
void FetchMessagesInChannel(Snowflake id, sigc::slot<void(const std::vector<Message> &)> cb);
void FetchMessagesInChannelBefore(Snowflake channel_id, Snowflake before_id, sigc::slot<void(const std::vector<Message> &)> cb);
std::optional<Message> GetMessage(Snowflake id) const;
std::optional<ChannelData> GetChannel(Snowflake id) const;
std::optional<EmojiData> GetEmoji(Snowflake id) const;
@@ -90,9 +84,13 @@ public:
std::optional<BanData> GetBan(Snowflake guild_id, Snowflake user_id) const;
Snowflake GetMemberHoistedRole(Snowflake guild_id, Snowflake user_id, bool with_color = false) const;
std::optional<RoleData> GetMemberHighestRole(Snowflake guild_id, Snowflake user_id) const;
std::unordered_set<Snowflake> GetUsersInGuild(Snowflake id) const;
std::unordered_set<Snowflake> GetChannelsInGuild(Snowflake id) const;
std::set<Snowflake> GetUsersInGuild(Snowflake id) const;
std::set<Snowflake> GetChannelsInGuild(Snowflake id) const;
std::vector<Snowflake> GetUsersInThread(Snowflake id) const;
std::vector<ChannelData> GetActiveThreads(Snowflake channel_id) const;
void GetArchivedPublicThreads(Snowflake channel_id, sigc::slot<void(DiscordError, const ArchivedThreadsResponseData &)> callback);
bool IsThreadJoined(Snowflake thread_id) const;
bool HasGuildPermission(Snowflake user_id, Snowflake guild_id, Permission perm) const;
bool HasAnyChannelPermission(Snowflake user_id, Snowflake channel_id, Permission perm) const;
@@ -108,6 +106,7 @@ public:
void DeleteMessage(Snowflake channel_id, Snowflake id);
void EditMessage(Snowflake channel_id, Snowflake id, std::string content);
void SendLazyLoad(Snowflake id);
void SendThreadLazyLoad(Snowflake id);
void JoinGuild(std::string code);
void LeaveGuild(Snowflake id);
void KickUser(Snowflake user_id, Snowflake guild_id);
@@ -115,40 +114,51 @@ public:
void UpdateStatus(PresenceStatus status, bool is_afk);
void UpdateStatus(PresenceStatus status, bool is_afk, const ActivityData &obj);
void CreateDM(Snowflake user_id);
void CreateDM(Snowflake user_id, sigc::slot<void(bool success, Snowflake channel_id)> callback);
void CreateDM(Snowflake user_id, sigc::slot<void(DiscordError code, Snowflake channel_id)> callback);
void CloseDM(Snowflake channel_id);
std::optional<Snowflake> FindDM(Snowflake user_id); // wont find group dms
void AddReaction(Snowflake id, Glib::ustring param);
void RemoveReaction(Snowflake id, Glib::ustring param);
void SetGuildName(Snowflake id, const Glib::ustring &name);
void SetGuildName(Snowflake id, const Glib::ustring &name, sigc::slot<void(bool success)> callback);
void SetGuildName(Snowflake id, const Glib::ustring &name, sigc::slot<void(DiscordError code)> callback);
void SetGuildIcon(Snowflake id, const std::string &data);
void SetGuildIcon(Snowflake id, const std::string &data, sigc::slot<void(bool success)> callback);
void SetGuildIcon(Snowflake id, const std::string &data, sigc::slot<void(DiscordError code)> callback);
void UnbanUser(Snowflake guild_id, Snowflake user_id);
void UnbanUser(Snowflake guild_id, Snowflake user_id, sigc::slot<void(bool success)> callback);
void UnbanUser(Snowflake guild_id, Snowflake user_id, sigc::slot<void(DiscordError code)> callback);
void DeleteInvite(const std::string &code);
void DeleteInvite(const std::string &code, sigc::slot<void(bool success)> callback);
void DeleteInvite(const std::string &code, sigc::slot<void(DiscordError code)> callback);
void AddGroupDMRecipient(Snowflake channel_id, Snowflake user_id);
void RemoveGroupDMRecipient(Snowflake channel_id, Snowflake user_id);
void ModifyRolePermissions(Snowflake guild_id, Snowflake role_id, Permission permissions, sigc::slot<void(bool success)> callback);
void ModifyRoleName(Snowflake guild_id, Snowflake role_id, const Glib::ustring &name, sigc::slot<void(bool success)> callback);
void ModifyRoleColor(Snowflake guild_id, Snowflake role_id, uint32_t color, sigc::slot<void(bool success)> callback);
void ModifyRoleColor(Snowflake guild_id, Snowflake role_id, Gdk::RGBA color, sigc::slot<void(bool success)> callback);
void ModifyRolePosition(Snowflake guild_id, Snowflake role_id, int position, sigc::slot<void(bool success)> callback);
void ModifyEmojiName(Snowflake guild_id, Snowflake emoji_id, const Glib::ustring &name, sigc::slot<void(bool success)> callback);
void DeleteEmoji(Snowflake guild_id, Snowflake emoji_id, sigc::slot<void(bool success)> callback);
void ModifyRolePermissions(Snowflake guild_id, Snowflake role_id, Permission permissions, sigc::slot<void(DiscordError code)> callback);
void ModifyRoleName(Snowflake guild_id, Snowflake role_id, const Glib::ustring &name, sigc::slot<void(DiscordError code)> callback);
void ModifyRoleColor(Snowflake guild_id, Snowflake role_id, uint32_t color, sigc::slot<void(DiscordError code)> callback);
void ModifyRoleColor(Snowflake guild_id, Snowflake role_id, Gdk::RGBA color, sigc::slot<void(DiscordError code)> callback);
void ModifyRolePosition(Snowflake guild_id, Snowflake role_id, int position, sigc::slot<void(DiscordError code)> callback);
void ModifyEmojiName(Snowflake guild_id, Snowflake emoji_id, const Glib::ustring &name, sigc::slot<void(DiscordError code)> callback);
void DeleteEmoji(Snowflake guild_id, Snowflake emoji_id, sigc::slot<void(DiscordError code)> callback);
std::optional<GuildApplicationData> GetGuildApplication(Snowflake guild_id) const;
void RemoveRelationship(Snowflake id, sigc::slot<void(DiscordError code)> callback);
void SendFriendRequest(const Glib::ustring &username, int discriminator, sigc::slot<void(DiscordError code)> callback);
void PutRelationship(Snowflake id, sigc::slot<void(DiscordError code)> callback); // send fr by id, accept incoming
void Pin(Snowflake channel_id, Snowflake message_id, sigc::slot<void(DiscordError code)> callback);
void Unpin(Snowflake channel_id, Snowflake message_id, sigc::slot<void(DiscordError code)> callback);
void LeaveThread(Snowflake channel_id, const std::string &location, sigc::slot<void(DiscordError code)> callback);
void ArchiveThread(Snowflake channel_id, sigc::slot<void(DiscordError code)> callback);
void UnArchiveThread(Snowflake channel_id, sigc::slot<void(DiscordError code)> callback);
bool CanModifyRole(Snowflake guild_id, Snowflake role_id) const;
bool CanModifyRole(Snowflake guild_id, Snowflake role_id, Snowflake user_id) const;
// real client doesn't seem to use the single role endpoints so neither do we
template<typename Iter>
auto SetMemberRoles(Snowflake guild_id, Snowflake user_id, Iter begin, Iter end, sigc::slot<void(bool success)> callback) {
auto SetMemberRoles(Snowflake guild_id, Snowflake user_id, Iter begin, Iter end, sigc::slot<void(DiscordError code)> callback) {
ModifyGuildMemberObject obj;
obj.Roles = { begin, end };
m_http.MakePATCH("/guilds/" + std::to_string(guild_id) + "/members/" + std::to_string(user_id), nlohmann::json(obj).dump(), [this, callback](const http::response_type &response) {
callback(CheckCode(response, 200));
if (CheckCode(response))
callback(DiscordError::NONE);
else
callback(GetCodeFromResponse(response));
});
}
@@ -167,18 +177,23 @@ public:
void FetchUserProfile(Snowflake user_id, sigc::slot<void(UserProfileData)> callback);
void FetchUserNote(Snowflake user_id, sigc::slot<void(std::string note)> callback);
void SetUserNote(Snowflake user_id, std::string note);
void SetUserNote(Snowflake user_id, std::string note, sigc::slot<void(bool success)> callback);
void SetUserNote(Snowflake user_id, std::string note, sigc::slot<void(DiscordError code)> callback);
void FetchUserRelationships(Snowflake user_id, sigc::slot<void(std::vector<UserData>)> callback);
void FetchPinned(Snowflake id, sigc::slot<void(std::vector<Message>, DiscordError code)> callback);
bool IsVerificationRequired(Snowflake guild_id);
void GetVerificationGateInfo(Snowflake guild_id, sigc::slot<void(std::optional<VerificationGateInfoObject>)> callback);
void AcceptVerificationGate(Snowflake guild_id, VerificationGateInfoObject info, sigc::slot<void(bool success)> callback);
void AcceptVerificationGate(Snowflake guild_id, VerificationGateInfoObject info, sigc::slot<void(DiscordError code)> callback);
void UpdateToken(std::string token);
void SetUserAgent(std::string agent);
PresenceStatus GetUserStatus(Snowflake id) const;
std::unordered_set<Snowflake> GetRelationships(RelationshipType type) const;
std::map<Snowflake, RelationshipType> GetRelationships() const;
std::set<Snowflake> GetRelationships(RelationshipType type) const;
std::optional<RelationshipType> GetRelationship(Snowflake id) const;
private:
static const constexpr int InflateChunkSize = 0x10000;
@@ -186,6 +201,11 @@ private:
std::vector<uint8_t> m_decompress_buf;
z_stream m_zstream;
std::string GetAPIURL();
std::string GetGatewayURL();
static DiscordError GetCodeFromResponse(const http::response_type &response);
void ProcessNewGuild(GuildData &guild);
void HandleGatewayMessageRaw(std::string str);
@@ -222,6 +242,15 @@ private:
void HandleGatewayGuildJoinRequestCreate(const GatewayMessage &msg);
void HandleGatewayGuildJoinRequestUpdate(const GatewayMessage &msg);
void HandleGatewayGuildJoinRequestDelete(const GatewayMessage &msg);
void HandleGatewayRelationshipRemove(const GatewayMessage &msg);
void HandleGatewayRelationshipAdd(const GatewayMessage &msg);
void HandleGatewayThreadCreate(const GatewayMessage &msg);
void HandleGatewayThreadDelete(const GatewayMessage &msg);
void HandleGatewayThreadListSync(const GatewayMessage &msg);
void HandleGatewayThreadMembersUpdate(const GatewayMessage &msg);
void HandleGatewayThreadMemberUpdate(const GatewayMessage &msg);
void HandleGatewayThreadUpdate(const GatewayMessage &msg);
void HandleGatewayThreadMemberListUpdate(const GatewayMessage &msg);
void HandleGatewayReadySupplemental(const GatewayMessage &msg);
void HandleGatewayReconnect(const GatewayMessage &msg);
void HandleGatewayInvalidSession(const GatewayMessage &msg);
@@ -239,18 +268,14 @@ private:
std::string m_token;
void AddMessageToChannel(Snowflake msg_id, Snowflake channel_id);
std::unordered_map<Snowflake, std::unordered_set<Snowflake>> m_chan_to_message_map;
void AddUserToGuild(Snowflake user_id, Snowflake guild_id);
std::unordered_map<Snowflake, std::unordered_set<Snowflake>> m_guild_to_users;
std::unordered_map<Snowflake, std::unordered_set<Snowflake>> m_guild_to_channels;
std::unordered_map<Snowflake, GuildApplicationData> m_guild_join_requests;
std::unordered_map<Snowflake, PresenceStatus> m_user_to_status;
std::unordered_map<Snowflake, RelationshipType> m_user_relationships;
std::map<Snowflake, std::set<Snowflake>> m_guild_to_users;
std::map<Snowflake, std::set<Snowflake>> m_guild_to_channels;
std::map<Snowflake, GuildApplicationData> m_guild_join_requests;
std::map<Snowflake, PresenceStatus> m_user_to_status;
std::map<Snowflake, RelationshipType> m_user_relationships;
std::set<Snowflake> m_joined_threads;
std::map<Snowflake, std::vector<Snowflake>> m_thread_members;
UserData m_user_data;
UserSettings m_user_settings;
@@ -260,8 +285,9 @@ private:
Websocket m_websocket;
std::atomic<bool> m_client_connected = false;
std::atomic<bool> m_ready_received = false;
bool m_client_started = false;
std::unordered_map<std::string, GatewayEvent> m_event_map;
std::map<std::string, GatewayEvent> m_event_map;
void LoadEventMap();
std::thread m_heartbeat_thread;
@@ -283,6 +309,9 @@ private:
Glib::Dispatcher m_generic_dispatch;
std::queue<std::function<void()>> m_generic_queue;
std::set<Snowflake> m_channels_pinned_requested;
std::set<Snowflake> m_channels_lazy_loaded;
// signals
public:
typedef sigc::signal<void> type_signal_gateway_ready;
@@ -294,7 +323,7 @@ public:
typedef sigc::signal<void, Snowflake> type_signal_guild_delete;
typedef sigc::signal<void, Snowflake> type_signal_channel_delete;
typedef sigc::signal<void, Snowflake> type_signal_channel_update;
typedef sigc::signal<void, Snowflake> type_signal_channel_create;
typedef sigc::signal<void, ChannelData> type_signal_channel_create;
typedef sigc::signal<void, Snowflake> type_signal_guild_update;
typedef sigc::signal<void, Snowflake, Snowflake> type_signal_role_update; // guild id, role id
typedef sigc::signal<void, Snowflake, Snowflake> type_signal_role_create; // guild id, role id
@@ -307,15 +336,30 @@ public:
typedef sigc::signal<void, Snowflake, Snowflake> type_signal_guild_ban_add; // guild id, user id
typedef sigc::signal<void, InviteData> type_signal_invite_create;
typedef sigc::signal<void, InviteDeleteObject> type_signal_invite_delete;
typedef sigc::signal<void, Snowflake, PresenceStatus> type_signal_presence_update;
typedef sigc::signal<void, UserData, PresenceStatus> type_signal_presence_update;
typedef sigc::signal<void, Snowflake, std::string> type_signal_note_update;
typedef sigc::signal<void, Snowflake, std::vector<EmojiData>> type_signal_guild_emojis_update; // guild id
typedef sigc::signal<void, GuildJoinRequestCreateData> type_signal_guild_join_request_create;
typedef sigc::signal<void, GuildJoinRequestUpdateData> type_signal_guild_join_request_update;
typedef sigc::signal<void, GuildJoinRequestDeleteData> type_signal_guild_join_request_delete;
typedef sigc::signal<void, Snowflake, RelationshipType> type_signal_relationship_remove;
typedef sigc::signal<void, RelationshipAddData> type_signal_relationship_add;
typedef sigc::signal<void, ChannelData> type_signal_thread_create;
typedef sigc::signal<void, ThreadDeleteData> type_signal_thread_delete;
typedef sigc::signal<void, ThreadListSyncData> type_signal_thread_list_sync;
typedef sigc::signal<void, ThreadMembersUpdateData> type_signal_thread_members_update;
typedef sigc::signal<void, ThreadUpdateData> type_signal_thread_update;
typedef sigc::signal<void, ThreadMemberListUpdateData> type_signal_thread_member_list_update;
// not discord dispatch events
typedef sigc::signal<void, Snowflake> type_signal_added_to_thread;
typedef sigc::signal<void, Snowflake> type_signal_removed_from_thread;
typedef sigc::signal<void, Message> type_signal_message_unpinned;
typedef sigc::signal<void, Message> type_signal_message_pinned;
typedef sigc::signal<void, Message> type_signal_message_sent;
typedef sigc::signal<void, std::string /* nonce */, float /* retry_after */> type_signal_message_send_fail; // retry after param will be 0 if it failed for a reason that isnt slowmode
typedef sigc::signal<void, bool, GatewayCloseCode> type_signal_disconnected; // bool true if reconnecting
typedef sigc::signal<void, bool, GatewayCloseCode> type_signal_disconnected; // bool true if reconnecting
typedef sigc::signal<void> type_signal_connected;
type_signal_gateway_ready signal_gateway_ready();
@@ -346,6 +390,19 @@ public:
type_signal_guild_join_request_create signal_guild_join_request_create();
type_signal_guild_join_request_update signal_guild_join_request_update();
type_signal_guild_join_request_delete signal_guild_join_request_delete();
type_signal_relationship_remove signal_relationship_remove();
type_signal_relationship_add signal_relationship_add();
type_signal_message_unpinned signal_message_unpinned();
type_signal_message_pinned signal_message_pinned();
type_signal_thread_create signal_thread_create();
type_signal_thread_delete signal_thread_delete();
type_signal_thread_list_sync signal_thread_list_sync();
type_signal_thread_members_update signal_thread_members_update();
type_signal_thread_update signal_thread_update();
type_signal_thread_member_list_update signal_thread_member_list_update();
type_signal_added_to_thread signal_added_to_thread();
type_signal_removed_from_thread signal_removed_from_thread();
type_signal_message_sent signal_message_sent();
type_signal_message_send_fail signal_message_send_fail();
type_signal_disconnected signal_disconnected();
@@ -380,6 +437,19 @@ protected:
type_signal_guild_join_request_create m_signal_guild_join_request_create;
type_signal_guild_join_request_update m_signal_guild_join_request_update;
type_signal_guild_join_request_delete m_signal_guild_join_request_delete;
type_signal_relationship_remove m_signal_relationship_remove;
type_signal_relationship_add m_signal_relationship_add;
type_signal_message_unpinned m_signal_message_unpinned;
type_signal_message_pinned m_signal_message_pinned;
type_signal_thread_create m_signal_thread_create;
type_signal_thread_delete m_signal_thread_delete;
type_signal_thread_list_sync m_signal_thread_list_sync;
type_signal_thread_members_update m_signal_thread_members_update;
type_signal_thread_update m_signal_thread_update;
type_signal_thread_member_list_update m_signal_thread_member_list_update;
type_signal_removed_from_thread m_signal_removed_from_thread;
type_signal_added_to_thread m_signal_added_to_thread;
type_signal_message_sent m_signal_message_sent;
type_signal_message_send_fail m_signal_message_send_fail;
type_signal_disconnected m_signal_disconnected;

36
discord/errors.hpp Normal file
View File

@@ -0,0 +1,36 @@
#pragma once
enum class DiscordError {
GENERIC = 0,
INVALID_FORM_BODY = 50035,
RELATIONSHIP_INCOMING_DISABLED = 80000,
RELATIONSHIP_INCOMING_BLOCKED = 80001,
RELATIONSHIP_INVALID_USER_BOT = 80002, // this is misspelled in discord's source lul
RELATIONSHIP_INVALID_SELF = 80003,
RELATIONSHIP_INVALID_DISCORD_TAG = 80004,
RELATIONSHIP_ALREADY_FRIENDS = 80007,
NONE = -1,
};
constexpr const char *GetDiscordErrorDisplayString(DiscordError error) {
switch (error) {
case DiscordError::INVALID_FORM_BODY:
return "Something's wrong with your input";
case DiscordError::RELATIONSHIP_INCOMING_DISABLED:
return "This user isn't accepting friend requests";
case DiscordError::RELATIONSHIP_INCOMING_BLOCKED:
return "You are blocked by this user";
case DiscordError::RELATIONSHIP_INVALID_USER_BOT:
return "You can't send a request to a bot";
case DiscordError::RELATIONSHIP_INVALID_SELF:
return "You can't send a request to yourself";
case DiscordError::RELATIONSHIP_INVALID_DISCORD_TAG:
return "No users with that tag exist";
case DiscordError::RELATIONSHIP_ALREADY_FRIENDS:
return "You are already friends with that user";
case DiscordError::GENERIC:
default:
return "An error occurred";
}
}

View File

@@ -43,6 +43,7 @@ void from_json(const nlohmann::json &j, GuildData &m) {
// JS_O("voice_states", m.VoiceStates);
// JS_O("members", m.Members);
JS_O("channels", m.Channels);
JS_O("threads", m.Threads);
// JS_O("presences", m.Presences);
JS_ON("max_presences", m.MaxPresences);
JS_O("max_members", m.MaxMembers);

View File

@@ -80,6 +80,7 @@ struct GuildData {
std::optional<int> MaxVideoChannelUsers;
std::optional<int> ApproximateMemberCount;
std::optional<int> ApproximatePresenceCount;
std::optional<std::vector<ChannelData>> Threads; // only with permissions to view, id only
// undocumented
// std::map<std::string, Unknown> GuildHashes;

View File

@@ -1,11 +1,14 @@
#include "httpclient.hpp"
//#define USE_LOCAL_PROXY
HTTPClient::HTTPClient(std::string api_base)
: m_api_base(api_base) {
HTTPClient::HTTPClient() {
m_dispatcher.connect(sigc::mem_fun(*this, &HTTPClient::RunCallbacks));
}
void HTTPClient::SetBase(const std::string &url) {
m_api_base = url;
}
void HTTPClient::SetUserAgent(std::string agent) {
m_agent = agent;
}

View File

@@ -11,7 +11,9 @@
class HTTPClient {
public:
HTTPClient(std::string api_base);
HTTPClient();
void SetBase(const std::string &url);
void SetUserAgent(std::string agent);
void SetAuth(std::string auth);

View File

@@ -10,6 +10,8 @@ void from_json(const nlohmann::json &j, GuildMember &m) {
JS_D("deaf", m.IsDeafened);
JS_D("mute", m.IsMuted);
JS_O("user_id", m.UserID);
JS_ON("avatar", m.Avatar);
JS_O("pending", m.IsPending);
}
std::vector<RoleData> GuildMember::GetSortedRoles() const {
@@ -33,4 +35,6 @@ void GuildMember::update_from_json(const nlohmann::json &j) {
JS_RD("nick", Nickname);
JS_RD("joined_at", JoinedAt);
JS_RD("premium_since", PremiumSince);
JS_RD("avatar", Avatar);
JS_RD("pending", IsPending);
}

View File

@@ -8,13 +8,17 @@
struct GuildMember {
std::optional<UserData> User; // only reliable to access id. only opt in MESSAGE_*
std::string Nickname; // null
std::string Nickname;
std::vector<Snowflake> Roles;
std::string JoinedAt;
std::optional<std::string> PremiumSince; // null
bool IsDeafened;
bool IsMuted;
std::optional<Snowflake> UserID; // present in merged_members
std::optional<bool> IsPending; // this uses `pending` not `is_pending`
// undocuemtned moment !!!1
std::optional<std::string> Avatar;
std::vector<RoleData> GetSortedRoles() const;

View File

@@ -218,6 +218,7 @@ void from_json(const nlohmann::json &j, Message &m) {
m.ReferencedMessage = nullptr;
}
JS_O("interaction", m.Interaction);
JS_O("sticker_items", m.StickerItems);
}
void Message::from_json_edited(const nlohmann::json &j) {
@@ -244,6 +245,7 @@ void Message::from_json_edited(const nlohmann::json &j) {
JS_O("flags", Flags);
JS_O("stickers", Stickers);
JS_O("interaction", Interaction);
JS_O("sticker_items", StickerItems);
}
void Message::SetDeleted() {

View File

@@ -27,7 +27,7 @@ enum class MessageType {
GUILD_DISCOVERY_REQUALIFIED = 15, // yep
GUILD_DISCOVERY_GRACE_PERIOD_INITIAL_WARNING = 16, // yep
GUILD_DISCOVERY_GRACE_PERIOD_FINAL_WARNING = 17, // yep
THREAD_CREATED = 18, // nope
THREAD_CREATED = 18, // yep
INLINE_REPLY = 19, // yep
APPLICATION_COMMAND = 20, // yep
THREAD_STARTER_MESSAGE = 21, // nope
@@ -199,6 +199,7 @@ struct Message {
std::optional<std::vector<StickerData>> Stickers;
std::optional<std::shared_ptr<Message>> ReferencedMessage; // has_value && null means deleted
std::optional<MessageInteractionData> Interaction;
std::optional<std::vector<StickerItem>> StickerItems;
friend void from_json(const nlohmann::json &j, Message &m);
void from_json_edited(const nlohmann::json &j); // for MESSAGE_UPDATE

View File

@@ -85,10 +85,16 @@ void to_json(nlohmann::json &j, const LazyLoadRequestMessage &m) {
for (const auto &[key, chans] : *m.Channels)
j["d"]["channels"][std::to_string(key)] = chans;
}
j["d"]["typing"] = m.ShouldGetTyping;
j["d"]["activities"] = m.ShouldGetActivities;
if (m.ShouldGetTyping)
j["d"]["typing"] = *m.ShouldGetTyping;
if (m.ShouldGetActivities)
j["d"]["activities"] = *m.ShouldGetActivities;
if (m.ShouldGetThreads)
j["d"]["threads"] = *m.ShouldGetThreads;
if (m.Members.has_value())
j["d"]["members"] = *m.Members;
if (m.ThreadIDs.has_value())
j["d"]["thread_member_lists"] = *m.ThreadIDs;
}
void to_json(nlohmann::json &j, const UpdateStatusMessage &m) {
@@ -102,7 +108,7 @@ void to_json(nlohmann::json &j, const UpdateStatusMessage &m) {
j["d"]["status"] = "online";
break;
case PresenceStatus::Offline:
j["d"]["status"] = "offline";
j["d"]["status"] = "invisible";
break;
case PresenceStatus::Idle:
j["d"]["status"] = "idle";
@@ -452,3 +458,77 @@ void from_json(const nlohmann::json &j, RateLimitedResponse &m) {
JS_O("message", m.Message);
JS_D("retry_after", m.RetryAfter);
}
void from_json(const nlohmann::json &j, RelationshipRemoveData &m) {
JS_D("id", m.ID);
JS_D("type", m.Type);
}
void from_json(const nlohmann::json &j, RelationshipAddData &m) {
JS_D("id", m.ID);
JS_D("type", m.Type);
JS_D("user", m.User);
}
void to_json(nlohmann::json &j, const FriendRequestObject &m) {
j["username"] = m.Username;
j["discriminator"] = m.Discriminator;
}
void to_json(nlohmann::json &j, const PutRelationshipObject &m) {
JS_IF("type", m.Type);
}
void from_json(const nlohmann::json &j, ThreadCreateData &m) {
j.get_to(m.Channel);
}
void from_json(const nlohmann::json &j, ThreadDeleteData &m) {
JS_D("id", m.ID);
JS_D("guild_id", m.GuildID);
JS_D("parent_id", m.ParentID);
JS_D("type", m.Type);
}
void from_json(const nlohmann::json &j, ThreadListSyncData &m) {
JS_D("threads", m.Threads);
JS_D("guild_id", m.GuildID);
}
void from_json(const nlohmann::json &j, ThreadMembersUpdateData &m) {
JS_D("id", m.ID);
JS_D("guild_id", m.GuildID);
JS_D("member_count", m.MemberCount);
JS_O("added_members", m.AddedMembers);
JS_O("removed_member_ids", m.RemovedMemberIDs);
}
void from_json(const nlohmann::json &j, ArchivedThreadsResponseData &m) {
JS_D("threads", m.Threads);
JS_D("members", m.Members);
JS_D("has_more", m.HasMore);
}
void from_json(const nlohmann::json &j, ThreadMemberUpdateData &m) {
m.Member = j;
}
void from_json(const nlohmann::json &j, ThreadUpdateData &m) {
m.Thread = j;
}
void from_json(const nlohmann::json &j, ThreadMemberListUpdateData::UserEntry &m) {
JS_D("user_id", m.UserID);
JS_D("member", m.Member);
}
void from_json(const nlohmann::json &j, ThreadMemberListUpdateData &m) {
JS_D("thread_id", m.ThreadID);
JS_D("guild_id", m.GuildID);
JS_D("members", m.Members);
}
void to_json(nlohmann::json &j, const ModifyChannelObject &m) {
JS_IF("archived", m.Archived);
JS_IF("locked", m.Locked);
}

View File

@@ -19,6 +19,7 @@
#include "ban.hpp"
#include "auditlog.hpp"
#include "relationship.hpp"
#include "errors.hpp"
// most stuff below should just be objects that get processed and thrown away immediately
@@ -68,6 +69,15 @@ enum class GatewayEvent : int {
GUILD_JOIN_REQUEST_CREATE,
GUILD_JOIN_REQUEST_UPDATE,
GUILD_JOIN_REQUEST_DELETE,
RELATIONSHIP_REMOVE,
RELATIONSHIP_ADD,
THREAD_CREATE,
THREAD_UPDATE,
THREAD_DELETE,
THREAD_LIST_SYNC,
THREAD_MEMBER_UPDATE,
THREAD_MEMBERS_UPDATE,
THREAD_MEMBER_LIST_UPDATE,
};
enum class GatewayCloseCode : uint16_t {
@@ -195,10 +205,12 @@ struct GuildMemberListUpdateMessage {
struct LazyLoadRequestMessage {
Snowflake GuildID;
bool ShouldGetTyping = false;
bool ShouldGetActivities = false;
std::optional<std::vector<std::string>> Members; // snowflake?
std::optional<std::unordered_map<Snowflake, std::vector<std::pair<int, int>>>> Channels; // channel ID -> range of sidebar
std::optional<bool> ShouldGetTyping;
std::optional<bool> ShouldGetActivities;
std::optional<bool> ShouldGetThreads;
std::optional<std::vector<std::string>> Members; // snowflake?
std::optional<std::map<Snowflake, std::vector<std::pair<int, int>>>> Channels; // channel ID -> range of sidebar
std::optional<std::vector<Snowflake>> ThreadIDs;
friend void to_json(nlohmann::json &j, const LazyLoadRequestMessage &m);
};
@@ -626,3 +638,110 @@ struct RateLimitedResponse {
friend void from_json(const nlohmann::json &j, RateLimitedResponse &m);
};
struct RelationshipRemoveData {
Snowflake ID;
RelationshipType Type;
friend void from_json(const nlohmann::json &j, RelationshipRemoveData &m);
};
struct RelationshipAddData {
Snowflake ID;
// Nickname; same deal as the other comment somewhere else
RelationshipType Type;
UserData User;
// std::optional<bool> ShouldNotify; // i guess if the client should send a notification. not worth caring about
friend void from_json(const nlohmann::json &j, RelationshipAddData &m);
};
struct FriendRequestObject {
std::string Username;
int Discriminator;
friend void to_json(nlohmann::json &j, const FriendRequestObject &m);
};
struct PutRelationshipObject {
std::optional<RelationshipType> Type;
friend void to_json(nlohmann::json &j, const PutRelationshipObject &m);
};
struct ThreadCreateData {
ChannelData Channel;
friend void from_json(const nlohmann::json &j, ThreadCreateData &m);
};
struct ThreadDeleteData {
Snowflake ID;
Snowflake GuildID;
Snowflake ParentID;
ChannelType Type;
friend void from_json(const nlohmann::json &j, ThreadDeleteData &m);
};
// pretty different from docs
struct ThreadListSyncData {
std::vector<ChannelData> Threads;
Snowflake GuildID;
// std::optional<std::vector<???>> MostRecentMessages;
friend void from_json(const nlohmann::json &j, ThreadListSyncData &m);
};
struct ThreadMembersUpdateData {
Snowflake ID;
Snowflake GuildID;
int MemberCount;
std::optional<std::vector<ThreadMemberObject>> AddedMembers;
std::optional<std::vector<Snowflake>> RemovedMemberIDs;
friend void from_json(const nlohmann::json &j, ThreadMembersUpdateData &m);
};
struct ArchivedThreadsResponseData {
std::vector<ChannelData> Threads;
std::vector<ThreadMemberObject> Members;
bool HasMore;
friend void from_json(const nlohmann::json &j, ArchivedThreadsResponseData &m);
};
struct ThreadMemberUpdateData {
ThreadMemberObject Member;
friend void from_json(const nlohmann::json &j, ThreadMemberUpdateData &m);
};
struct ThreadUpdateData {
ChannelData Thread;
friend void from_json(const nlohmann::json &j, ThreadUpdateData &m);
};
struct ThreadMemberListUpdateData {
struct UserEntry {
Snowflake UserID;
// PresenceData Presence;
GuildMember Member;
friend void from_json(const nlohmann::json &j, UserEntry &m);
};
Snowflake ThreadID;
Snowflake GuildID;
std::vector<UserEntry> Members;
friend void from_json(const nlohmann::json &j, ThreadMemberListUpdateData &m);
};
struct ModifyChannelObject {
std::optional<bool> Archived;
std::optional<bool> Locked;
friend void to_json(nlohmann::json &j, const ModifyChannelObject &m);
};

View File

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

View File

@@ -5,6 +5,8 @@
constexpr static uint64_t DiscordEpochSeconds = 1420070400;
const Snowflake Snowflake::Invalid = -1ULL;
Snowflake::Snowflake()
: m_num(Invalid) {}

View File

@@ -26,7 +26,7 @@ struct Snowflake {
return m_num;
}
const static uint64_t Invalid = -1ULL; // makes sense to me
const static Snowflake Invalid; // makes sense to me
const static uint64_t SecondsInterval = 4194304000ULL; // the "difference" between two snowflakes one second apart
friend void from_json(const nlohmann::json &j, Snowflake &s);

View File

@@ -30,3 +30,23 @@ std::string StickerData::GetURL() const {
return "https://media.discordapp.net/stickers/" + std::to_string(ID) + "/" + *AssetHash + ".json";
return "";
}
void to_json(nlohmann::json &j, const StickerItem &m) {
j["id"] = m.ID;
j["name"] = m.Name;
j["format_type"] = m.FormatType;
}
void from_json(const nlohmann::json &j, StickerItem &m) {
JS_D("id", m.ID);
JS_D("name", m.Name);
JS_D("format_type", m.FormatType);
}
std::string StickerItem::GetURL() const {
if (FormatType == StickerFormatType::PNG || FormatType == StickerFormatType::APNG)
return "https://media.discordapp.net/stickers/" + std::to_string(ID) + ".png?size=256";
else if (FormatType == StickerFormatType::LOTTIE)
return "https://media.discordapp.net/stickers/" + std::to_string(ID) + ".json";
return "";
}

View File

@@ -27,3 +27,14 @@ struct StickerData {
std::string GetURL() const;
};
struct StickerItem {
StickerFormatType FormatType;
Snowflake ID;
std::string Name;
friend void to_json(nlohmann::json &j, const StickerItem &m);
friend void from_json(const nlohmann::json &j, StickerItem &m);
std::string GetURL() const;
};

View File

@@ -105,6 +105,16 @@ void Store::SetChannel(Snowflake id, const ChannelData &chan) {
Bind(m_set_chan_stmt, 17, chan.ParentID);
Bind(m_set_chan_stmt, 18, chan.LastPinTimestamp);
if (chan.ThreadMetadata.has_value()) {
Bind(m_set_chan_stmt, 19, chan.ThreadMetadata->IsArchived);
Bind(m_set_chan_stmt, 20, chan.ThreadMetadata->AutoArchiveDuration);
Bind(m_set_chan_stmt, 21, chan.ThreadMetadata->ArchiveTimestamp);
} else {
Bind(m_set_chan_stmt, 19, nullptr);
Bind(m_set_chan_stmt, 20, nullptr);
Bind(m_set_chan_stmt, 21, nullptr);
}
if (!RunInsert(m_set_chan_stmt))
fprintf(stderr, "channel insert failed: %s\n", sqlite3_errstr(m_db_err));
@@ -194,6 +204,12 @@ void Store::SetGuild(Snowflake id, const GuildData &guild) {
Bind(m_set_guild_stmt, 37, guild.ApproximateMemberCount);
Bind(m_set_guild_stmt, 38, guild.ApproximatePresenceCount);
Bind(m_set_guild_stmt, 39, guild.IsLazy);
if (guild.Threads.has_value()) {
snowflakes.clear();
for (const auto &x : *guild.Threads) snowflakes.push_back(x.ID);
Bind(m_set_guild_stmt, 40, nlohmann::json(snowflakes).dump());
} else
Bind(m_set_guild_stmt, 40, "[]"s);
if (!RunInsert(m_set_guild_stmt))
fprintf(stderr, "guild insert failed: %s\n", sqlite3_errstr(m_db_err));
@@ -210,6 +226,8 @@ void Store::SetGuildMember(Snowflake guild_id, Snowflake user_id, const GuildMem
Bind(m_set_member_stmt, 6, data.PremiumSince);
Bind(m_set_member_stmt, 7, data.IsDeafened);
Bind(m_set_member_stmt, 8, data.IsMuted);
Bind(m_set_member_stmt, 9, data.Avatar);
Bind(m_set_member_stmt, 10, data.IsPending);
if (!RunInsert(m_set_member_stmt))
fprintf(stderr, "member insert failed: %s\n", sqlite3_errstr(m_db_err));
@@ -262,6 +280,12 @@ void Store::SetMessage(Snowflake id, const Message &message) {
Bind(m_set_msg_stmt, 23, message.IsPending);
Bind(m_set_msg_stmt, 24, message.Nonce); // sorry
if (message.StickerItems.has_value()) {
std::string tmp = nlohmann::json(*message.StickerItems).dump();
Bind(m_set_msg_stmt, 25, tmp);
} else
Bind(m_set_msg_stmt, 25, nullptr);
if (!RunInsert(m_set_msg_stmt))
fprintf(stderr, "message insert failed: %s\n", sqlite3_errstr(m_db_err));
@@ -314,6 +338,86 @@ void Store::SetUser(Snowflake id, const UserData &user) {
}
}
Message Store::GetMessageBound(sqlite3_stmt *stmt) const {
Message ret;
Get(stmt, 0, ret.ID);
Get(stmt, 1, ret.ChannelID);
Get(stmt, 2, ret.GuildID);
Get(stmt, 3, ret.Author.ID); // yike
Get(stmt, 4, ret.Content);
Get(stmt, 5, ret.Timestamp);
Get(stmt, 6, ret.EditedTimestamp);
Get(stmt, 7, ret.IsTTS);
Get(stmt, 8, ret.DoesMentionEveryone);
std::string tmps;
Get(stmt, 9, tmps);
nlohmann::json::parse(tmps).get_to(ret.Mentions);
Get(stmt, 10, tmps);
nlohmann::json::parse(tmps).get_to(ret.Attachments);
Get(stmt, 11, tmps);
nlohmann::json::parse(tmps).get_to(ret.Embeds);
Get(stmt, 12, ret.IsPinned);
Get(stmt, 13, ret.WebhookID);
uint64_t tmpi;
Get(stmt, 14, tmpi);
ret.Type = static_cast<MessageType>(tmpi);
Get(stmt, 15, tmps);
if (tmps != "")
ret.Application = nlohmann::json::parse(tmps).get<MessageApplicationData>();
Get(stmt, 16, tmps);
if (tmps != "")
ret.MessageReference = nlohmann::json::parse(tmps).get<MessageReferenceData>();
Get(stmt, 17, tmpi);
ret.Flags = static_cast<MessageFlags>(tmpi);
Get(stmt, 18, tmps);
if (tmps != "")
ret.Stickers = nlohmann::json::parse(tmps).get<std::vector<StickerData>>();
Get(stmt, 19, tmps);
if (tmps != "")
ret.Reactions = nlohmann::json::parse(tmps).get<std::vector<ReactionData>>();
bool tmpb = false;
Get(stmt, 20, tmpb);
if (tmpb) ret.SetDeleted();
Get(stmt, 21, tmpb);
if (tmpb) ret.SetEdited();
Get(stmt, 22, ret.IsPending);
Get(stmt, 23, ret.Nonce);
Get(stmt, 24, tmps);
if (tmps != "")
ret.StickerItems = nlohmann::json::parse(tmps).get<std::vector<StickerItem>>();
// interaction data from join
if (!IsNull(stmt, 25)) {
auto &interaction = ret.Interaction.emplace();
Get(stmt, 25, interaction.ID);
Get(stmt, 26, interaction.Name);
Get(stmt, 27, interaction.Type);
Get(stmt, 28, interaction.User.ID);
}
Reset(stmt);
if (ret.MessageReference.has_value() && ret.MessageReference->MessageID.has_value()) {
auto ref = GetMessage(*ret.MessageReference->MessageID);
if (ref.has_value())
ret.ReferencedMessage = std::make_unique<Message>(std::move(*ref));
else
ret.ReferencedMessage = nullptr;
}
return ret;
}
void Store::SetMessageInteractionPair(Snowflake message_id, const MessageInteractionData &interaction) {
Bind(m_set_msg_interaction_stmt, 1, message_id);
Bind(m_set_msg_interaction_stmt, 2, interaction.ID);
@@ -361,6 +465,69 @@ std::vector<BanData> Store::GetBans(Snowflake guild_id) const {
return ret;
}
std::vector<Message> Store::GetLastMessages(Snowflake id, size_t num) const {
auto ids = GetChannelMessageIDs(id);
std::vector<Message> ret;
for (auto it = ids.cend() - std::min(ids.size(), num); it != ids.cend(); it++)
ret.push_back(*GetMessage(*it));
return ret;
}
std::vector<Snowflake> Store::GetChannelMessageIDs(Snowflake id) const {
std::vector<Snowflake> ret;
Bind(m_get_msg_ids_stmt, 1, id);
while (FetchOne(m_get_msg_ids_stmt)) {
Snowflake x;
Get(m_get_msg_ids_stmt, 0, x);
ret.push_back(x);
}
Reset(m_get_msg_ids_stmt);
if (m_db_err != SQLITE_DONE)
fprintf(stderr, "error while fetching ids: %s\n", sqlite3_errstr(m_db_err));
return ret;
}
std::vector<Message> Store::GetPinnedMessages(Snowflake channel_id) const {
std::vector<Message> ret;
Bind(m_get_pins_stmt, 1, channel_id);
while (FetchOne(m_get_pins_stmt)) {
Snowflake x;
Get(m_get_pins_stmt, 0, x);
auto msg = GetMessage(x);
if (msg.has_value())
ret.push_back(*msg);
}
Reset(m_get_pins_stmt);
if (m_db_err != SQLITE_DONE)
fprintf(stderr, "error while fetching pins: %s\n", sqlite3_errstr(m_db_err));
return ret;
}
std::vector<ChannelData> Store::GetActiveThreads(Snowflake channel_id) const {
std::vector<ChannelData> ret;
Bind(m_get_threads_stmt, 1, channel_id);
while (FetchOne(m_get_threads_stmt)) {
Snowflake x;
Get(m_get_threads_stmt, 0, x);
auto chan = GetChannel(x);
if (chan.has_value())
ret.push_back(*chan);
}
Reset(m_get_threads_stmt);
if (m_db_err != SQLITE_DONE)
fprintf(stderr, "error while fetching threads: %s\n", sqlite3_errstr(m_db_err));
return ret;
}
std::optional<ChannelData> Store::GetChannel(Snowflake id) const {
Bind(m_get_chan_stmt, 1, id);
if (!FetchOne(m_get_chan_stmt)) {
@@ -395,6 +562,12 @@ std::optional<ChannelData> Store::GetChannel(Snowflake id) const {
Get(m_get_chan_stmt, 15, ret.ApplicationID);
Get(m_get_chan_stmt, 16, ret.ParentID);
Get(m_get_chan_stmt, 17, ret.LastPinTimestamp);
if (!IsNull(m_get_chan_stmt, 18)) {
ret.ThreadMetadata.emplace();
Get(m_get_chan_stmt, 18, ret.ThreadMetadata->IsArchived);
Get(m_get_chan_stmt, 19, ret.ThreadMetadata->AutoArchiveDuration);
Get(m_get_chan_stmt, 20, ret.ThreadMetadata->ArchiveTimestamp);
}
Reset(m_get_chan_stmt);
@@ -494,6 +667,10 @@ std::optional<GuildData> Store::GetGuild(Snowflake id) const {
Get(m_get_guild_stmt, 36, ret.ApproximateMemberCount);
Get(m_get_guild_stmt, 37, ret.ApproximatePresenceCount);
Get(m_get_guild_stmt, 38, ret.IsLazy);
Get(m_get_guild_stmt, 39, tmp);
ret.Threads.emplace();
for (const auto &id : nlohmann::json::parse(tmp).get<std::vector<Snowflake>>())
ret.Threads->emplace_back().ID = id;
Reset(m_get_guild_stmt);
@@ -520,6 +697,8 @@ std::optional<GuildMember> Store::GetGuildMember(Snowflake guild_id, Snowflake u
Get(m_get_member_stmt, 5, ret.PremiumSince);
Get(m_get_member_stmt, 6, ret.IsDeafened);
Get(m_get_member_stmt, 7, ret.IsMuted);
Get(m_get_member_stmt, 8, ret.Avatar);
Get(m_get_member_stmt, 9, ret.IsPending);
Reset(m_get_member_stmt);
@@ -535,77 +714,7 @@ std::optional<Message> Store::GetMessage(Snowflake id) const {
return std::nullopt;
}
Message ret;
ret.ID = id;
Get(m_get_msg_stmt, 1, ret.ChannelID);
Get(m_get_msg_stmt, 2, ret.GuildID);
Get(m_get_msg_stmt, 3, ret.Author.ID); // yike
Get(m_get_msg_stmt, 4, ret.Content);
Get(m_get_msg_stmt, 5, ret.Timestamp);
Get(m_get_msg_stmt, 6, ret.EditedTimestamp);
Get(m_get_msg_stmt, 7, ret.IsTTS);
Get(m_get_msg_stmt, 8, ret.DoesMentionEveryone);
std::string tmps;
Get(m_get_msg_stmt, 9, tmps);
nlohmann::json::parse(tmps).get_to(ret.Mentions);
Get(m_get_msg_stmt, 10, tmps);
nlohmann::json::parse(tmps).get_to(ret.Attachments);
Get(m_get_msg_stmt, 11, tmps);
nlohmann::json::parse(tmps).get_to(ret.Embeds);
Get(m_get_msg_stmt, 12, ret.IsPinned);
Get(m_get_msg_stmt, 13, ret.WebhookID);
uint64_t tmpi;
Get(m_get_msg_stmt, 14, tmpi);
ret.Type = static_cast<MessageType>(tmpi);
Get(m_get_msg_stmt, 15, tmps);
if (tmps != "")
ret.Application = nlohmann::json::parse(tmps).get<MessageApplicationData>();
Get(m_get_msg_stmt, 16, tmps);
if (tmps != "")
ret.MessageReference = nlohmann::json::parse(tmps).get<MessageReferenceData>();
Get(m_get_msg_stmt, 17, tmpi);
ret.Flags = static_cast<MessageFlags>(tmpi);
Get(m_get_msg_stmt, 18, tmps);
if (tmps != "")
ret.Stickers = nlohmann::json::parse(tmps).get<std::vector<StickerData>>();
Get(m_get_msg_stmt, 19, tmps);
if (tmps != "")
ret.Reactions = nlohmann::json::parse(tmps).get<std::vector<ReactionData>>();
bool tmpb = false;
Get(m_get_msg_stmt, 20, tmpb);
if (tmpb) ret.SetDeleted();
Get(m_get_msg_stmt, 21, tmpb);
if (tmpb) ret.SetEdited();
Get(m_get_msg_stmt, 22, ret.IsPending);
Get(m_get_msg_stmt, 23, ret.Nonce);
// interaction data from join
if (!IsNull(m_get_msg_stmt, 24)) {
auto &interaction = ret.Interaction.emplace();
Get(m_get_msg_stmt, 24, interaction.ID);
Get(m_get_msg_stmt, 25, interaction.Name);
Get(m_get_msg_stmt, 26, interaction.Type);
Get(m_get_msg_stmt, 27, interaction.User.ID);
}
Reset(m_get_msg_stmt);
if (ret.MessageReference.has_value() && ret.MessageReference->MessageID.has_value()) {
auto ref = GetMessage(*ret.MessageReference->MessageID);
if (ref.has_value())
ret.ReferencedMessage = std::make_unique<Message>(std::move(*ref));
else
ret.ReferencedMessage = nullptr;
}
auto ret = GetMessageBound(m_get_msg_stmt);
return std::optional<Message>(std::move(ret));
}
@@ -696,6 +805,12 @@ void Store::ClearGuild(Snowflake id) {
void Store::ClearChannel(Snowflake id) {
m_channels.erase(id);
Bind(m_clear_chan_stmt, 1, id);
if ((m_db_err = sqlite3_step(m_clear_chan_stmt)) != SQLITE_DONE)
printf("clearing channel failed: %s\n", sqlite3_errstr(m_db_err));
Reset(m_clear_chan_stmt);
}
void Store::ClearBan(Snowflake guild_id, Snowflake user_id) {
@@ -729,7 +844,7 @@ void Store::EndTransaction() {
}
bool Store::CreateTables() {
constexpr const char *create_users = R"(
const char *create_users = R"(
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY,
username TEXT NOT NULL,
@@ -747,7 +862,7 @@ bool Store::CreateTables() {
)
)";
constexpr const char *create_permissions = R"(
const char *create_permissions = R"(
CREATE TABLE IF NOT EXISTS permissions (
id INTEGER NOT NULL,
channel_id INTEGER NOT NULL,
@@ -758,7 +873,7 @@ bool Store::CreateTables() {
)
)";
constexpr const char *create_messages = R"(
const char *create_messages = R"(
CREATE TABLE IF NOT EXISTS messages (
id INTEGER PRIMARY KEY,
channel_id INTEGER NOT NULL,
@@ -783,11 +898,12 @@ bool Store::CreateTables() {
deleted BOOL, /* extra */
edited BOOL, /* extra */
pending BOOL, /* extra */
nonce TEXT
nonce TEXT,
sticker_items TEXT /* json */
)
)";
constexpr const char *create_roles = R"(
const char *create_roles = R"(
CREATE TABLE IF NOT EXISTS roles (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
@@ -800,7 +916,7 @@ bool Store::CreateTables() {
)
)";
constexpr const char *create_emojis = R"(
const char *create_emojis = R"(
CREATE TABLE IF NOT EXISTS emojis (
id INTEGER PRIMARY KEY, /*though nullable, only custom emojis (with non-null ids) are stored*/
name TEXT NOT NULL, /*same as id*/
@@ -813,7 +929,7 @@ bool Store::CreateTables() {
)
)";
constexpr const char *create_members = R"(
const char *create_members = R"(
CREATE TABLE IF NOT EXISTS members (
user_id INTEGER NOT NULL,
guild_id INTEGER NOT NULL,
@@ -823,11 +939,13 @@ bool Store::CreateTables() {
premium_since TEXT,
deaf BOOL NOT NULL,
mute BOOL NOT NULL,
avatar TEXT,
pending BOOL,
PRIMARY KEY(user_id, guild_id)
)
)";
constexpr const char *create_guilds = R"(
const char *create_guilds = R"(
CREATE TABLE IF NOT EXISTS guilds (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
@@ -867,11 +985,12 @@ bool Store::CreateTables() {
max_video_users INTEGER,
approx_members INTEGER,
approx_presences INTEGER,
lazy BOOL
lazy BOOL,
threads TEXT NOT NULL /* json */
)
)";
constexpr const char *create_channels = R"(
const char *create_channels = R"(
CREATE TABLE IF NOT EXISTS channels (
id INTEGER PRIMARY KEY,
type INTEGER NOT NULL,
@@ -890,11 +1009,14 @@ bool Store::CreateTables() {
owner_id INTEGER,
application_id INTEGER,
parent_id INTEGER,
last_pin_timestamp TEXT
last_pin_timestamp TEXT,
archived BOOL, /* threads */
auto_archive INTEGER, /* threads */
archived_ts TEXT /* threads */
)
)";
constexpr const char *create_bans = R"(
const char *create_bans = R"(
CREATE TABLE IF NOT EXISTS bans (
guild_id INTEGER NOT NULL,
user_id INTEGER NOT NULL,
@@ -903,7 +1025,7 @@ bool Store::CreateTables() {
)
)";
constexpr const char *create_interactions = R"(
const char *create_interactions = R"(
CREATE TABLE IF NOT EXISTS message_interactions (
message_id INTEGER NOT NULL,
interaction_id INTEGER NOT NULL,
@@ -978,33 +1100,33 @@ bool Store::CreateTables() {
}
bool Store::CreateStatements() {
constexpr const char *set_user = R"(
const char *set_user = R"(
REPLACE INTO users VALUES (
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
)
)";
constexpr const char *get_user = R"(
const char *get_user = R"(
SELECT * FROM users WHERE id = ?
)";
constexpr const char *set_perm = R"(
const char *set_perm = R"(
REPLACE INTO permissions VALUES (
?, ?, ?, ?, ?
)
)";
constexpr const char *get_perm = R"(
const char *get_perm = R"(
SELECT * FROM permissions WHERE id = ? AND channel_id = ?
)";
constexpr const char *set_msg = R"(
const char *set_msg = R"(
REPLACE INTO messages VALUES (
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
)
)";
constexpr const char *get_msg = R"(
const char *get_msg = R"(
SELECT messages.*,
message_interactions.interaction_id as interaction_id,
message_interactions.name as interaction_name,
@@ -1017,80 +1139,105 @@ bool Store::CreateStatements() {
WHERE id = ?
)";
constexpr const char *set_role = R"(
const char *set_role = R"(
REPLACE INTO roles VALUES (
?, ?, ?, ?, ?, ?, ?, ?
)
)";
constexpr const char *get_role = R"(
const char *get_role = R"(
SELECT * FROM roles WHERE id = ?
)";
constexpr const char *set_emoji = R"(
const char *set_emoji = R"(
REPLACE INTO emojis VALUES (
?, ?, ?, ?, ?, ?, ?, ?
)
)";
constexpr const char *get_emoji = R"(
const char *get_emoji = R"(
SELECT * FROM emojis WHERE id = ?
)";
constexpr const char *set_member = R"(
const char *set_member = R"(
REPLACE INTO members VALUES (
?, ?, ?, ?, ?, ?, ?, ?
?, ?, ?, ?, ?, ?, ?, ?, ?, ?
)
)";
constexpr const char *get_member = R"(
const char *get_member = R"(
SELECT * FROM members WHERE user_id = ? AND guild_id = ?
)";
constexpr const char *set_guild = R"(
const char *set_guild = R"(
REPLACE INTO guilds VALUES (
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
)
)";
constexpr const char *get_guild = R"(
const char *get_guild = R"(
SELECT * FROM guilds WHERE id = ?
)";
constexpr const char *set_chan = R"(
const char *set_chan = R"(
REPLACE INTO channels VALUES (
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
)
)";
constexpr const char *get_chan = R"(
const char *get_chan = R"(
SELECT * FROM channels WHERE id = ?
)";
constexpr const char *set_ban = R"(
const char *set_ban = R"(
REPLACE INTO bans VALUES (
?, ?, ?
)
)";
constexpr const char *get_ban = R"(
const char *get_ban = R"(
SELECT * FROM bans WHERE guild_id = ? AND user_id = ?
)";
constexpr const char *clear_ban = R"(
const char *clear_ban = R"(
DELETE FROM bans WHERE guild_id = ? AND user_id = ?
)";
constexpr const char *get_bans = R"(
const char *get_bans = R"(
SELECT * FROM bans WHERE guild_id = ?
)";
constexpr const char *set_interaction = R"(
const char *set_interaction = R"(
REPLACE INTO message_interactions VALUES (
?, ?, ?, ?, ?
)
)";
const char *get_last_msgs = R"(
SELECT * FROM (
SELECT * FROM messages
WHERE channel_id = ?
ORDER BY id DESC
LIMIT ?
) T1 ORDER BY id ASC
)";
const char *get_msg_ids = R"(
SELECT id FROM messages WHERE channel_id = ? AND pending = 0 ORDER BY id ASC
)";
const char *get_pins = R"(
SELECT id FROM messages WHERE channel_id = ? AND pinned = 1 ORDER BY id ASC
)";
const char *get_threads = R"(
SELECT id FROM channels WHERE parent_id = ? AND (type = 10 OR type = 11 OR type = 12) AND archived = FALSE
)";
const char *clear_chan = R"(
DELETE FROM channels WHERE id = ?
)";
m_db_err = sqlite3_prepare_v2(m_db, set_user, -1, &m_set_user_stmt, nullptr);
if (m_db_err != SQLITE_OK) {
fprintf(stderr, "failed to prepare set user statement: %s\n", sqlite3_errstr(m_db_err));
@@ -1217,6 +1364,36 @@ bool Store::CreateStatements() {
return false;
}
m_db_err = sqlite3_prepare_v2(m_db, get_last_msgs, -1, &m_get_last_msgs_stmt, nullptr);
if (m_db_err != SQLITE_OK) {
fprintf(stderr, "failed to prepare get last messages statement: %s\n", sqlite3_errstr(m_db_err));
return false;
}
m_db_err = sqlite3_prepare_v2(m_db, get_msg_ids, -1, &m_get_msg_ids_stmt, nullptr);
if (m_db_err != SQLITE_OK) {
fprintf(stderr, "failed to prepare get msg ids statement: %s\n", sqlite3_errstr(m_db_err));
return false;
}
m_db_err = sqlite3_prepare_v2(m_db, get_pins, -1, &m_get_pins_stmt, nullptr);
if (m_db_err != SQLITE_OK) {
fprintf(stderr, "failed to prepare get pins statement: %s\n", sqlite3_errstr(m_db_err));
return false;
}
m_db_err = sqlite3_prepare_v2(m_db, get_threads, -1, &m_get_threads_stmt, nullptr);
if (m_db_err != SQLITE_OK) {
fprintf(stderr, "failed to prepare get threads statement: %s\n", sqlite3_errstr(m_db_err));
return false;
}
m_db_err = sqlite3_prepare_v2(m_db, clear_chan, -1, &m_clear_chan_stmt, nullptr);
if (m_db_err != SQLITE_OK) {
fprintf(stderr, "failed to prepare clear channel statement: %s\n", sqlite3_errstr(m_db_err));
return false;
}
return true;
}
@@ -1242,6 +1419,11 @@ void Store::Cleanup() {
sqlite3_finalize(m_clear_ban_stmt);
sqlite3_finalize(m_get_bans_stmt);
sqlite3_finalize(m_set_msg_interaction_stmt);
sqlite3_finalize(m_get_last_msgs_stmt);
sqlite3_finalize(m_get_msg_ids_stmt);
sqlite3_finalize(m_get_pins_stmt);
sqlite3_finalize(m_get_threads_stmt);
sqlite3_finalize(m_clear_chan_stmt);
}
void Store::Bind(sqlite3_stmt *stmt, int index, int num) const {

View File

@@ -41,6 +41,11 @@ public:
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);
@@ -63,6 +68,8 @@ public:
void EndTransaction();
private:
Message GetMessageBound(sqlite3_stmt *stmt) const;
void SetMessageInteractionPair(Snowflake message_id, const MessageInteractionData &interaction);
std::unordered_set<Snowflake> m_channels;
@@ -126,6 +133,11 @@ private:
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>

View File

@@ -13,11 +13,33 @@ bool UserData::HasAnimatedAvatar() const {
return Avatar.size() > 0 && Avatar[0] == 'a' && Avatar[1] == '_';
}
std::string UserData::GetAvatarURL(Snowflake guild_id, std::string ext, std::string size) const {
const auto member = Abaddon::Get().GetDiscordClient().GetMember(ID, guild_id);
if (member.has_value() && member->Avatar.has_value())
return "https://cdn.discordapp.com/guilds/" +
std::to_string(guild_id) + "/users/" + std::to_string(ID) +
"/avatars/" + *member->Avatar + "." +
ext + "?" + "size=" + size;
else
return GetAvatarURL(ext, size);
}
std::string UserData::GetAvatarURL(const std::optional<Snowflake> &guild_id, std::string ext, std::string size) const {
if (guild_id.has_value())
return GetAvatarURL(*guild_id, ext, size);
else
return GetAvatarURL(ext, size);
}
std::string UserData::GetAvatarURL(std::string ext, std::string size) const {
if (HasAvatar())
return "https://cdn.discordapp.com/avatars/" + std::to_string(ID) + "/" + Avatar + "." + ext + "?size=" + size;
else
return "https://cdn.discordapp.com/embed/avatars/" + std::to_string(std::stoul(Discriminator) % 5) + ".png"; // size isn't respected by the cdn
return GetDefaultAvatarURL();
}
std::string UserData::GetDefaultAvatarURL() const {
return "https://cdn.discordapp.com/embed/avatars/" + std::to_string(std::stoul(Discriminator) % 5) + ".png"; // size isn't respected by the cdn
}
Snowflake UserData::GetHoistedRole(Snowflake guild_id, bool with_color) const {
@@ -58,6 +80,8 @@ void from_json(const nlohmann::json &j, UserData &m) {
JS_O("mobile", m.IsMobile);
JS_ON("nsfw_allowed", m.IsNSFWAllowed);
JS_ON("phone", m.Phone);
JS_ON("bio", m.Bio);
JS_ON("banner", m.BannerHash);
}
void to_json(nlohmann::json &j, const UserData &m) {
@@ -130,6 +154,8 @@ const char *UserData::GetFlagName(uint64_t flag) {
return "verifiedbot";
case EarlyVerifiedBotDeveloper:
return "earlyverifiedbotdeveloper";
case CertifiedModerator:
return "certifiedmoderator";
default:
return "unknown";
}
@@ -163,6 +189,8 @@ const char *UserData::GetFlagReadableName(uint64_t flag) {
return "Verified Bot";
case EarlyVerifiedBotDeveloper:
return "Early Verified Bot Developer";
case CertifiedModerator:
return "Discord Certified Moderator";
default:
return "";
}

View File

@@ -24,8 +24,10 @@ struct UserData {
BugHunterLevel2 = 1 << 14,
VerifiedBot = 1 << 16,
EarlyVerifiedBotDeveloper = 1 << 17,
CertifiedModerator = 1 << 18,
MaxFlag = EarlyVerifiedBotDeveloper,
MaxFlag_PlusOne,
MaxFlag = MaxFlag_PlusOne - 1,
};
static const char *GetFlagName(uint64_t flag);
@@ -50,6 +52,9 @@ struct UserData {
std::optional<bool> IsMobile;
std::optional<bool> IsNSFWAllowed; // null
std::optional<std::string> Phone; // null?
// for now (unserialized)
std::optional<std::string> BannerHash; // null
std::optional<std::string> Bio; // null
friend void from_json(const nlohmann::json &j, UserData &m);
friend void to_json(nlohmann::json &j, const UserData &m);
@@ -58,7 +63,10 @@ struct UserData {
bool IsDeleted() const;
bool HasAvatar() const;
bool HasAnimatedAvatar() const;
std::string GetAvatarURL(Snowflake guild_id, std::string ext = "png", std::string size = "32") const;
std::string GetAvatarURL(const std::optional<Snowflake> &guild_id, std::string ext = "png", std::string size = "32") const;
std::string GetAvatarURL(std::string ext = "png", std::string size = "32") const;
std::string GetDefaultAvatarURL() const;
Snowflake GetHoistedRole(Snowflake guild_id, bool with_color = false) const;
std::string GetMention() const;
std::string GetEscapedName() const;

View File

@@ -125,6 +125,10 @@ void FileCacheWorkerThread::stop() {
}
void FileCacheWorkerThread::loop() {
timeval timeout;
timeout.tv_sec = 1;
timeout.tv_usec = 0;
while (!m_stop) {
if (m_handles.size() == 0) {
std::unique_lock<std::mutex> lock(m_queue_mutex);
@@ -133,7 +137,8 @@ void FileCacheWorkerThread::loop() {
m_cv.wait(lock);
}
if (m_handles.size() < Abaddon::Get().GetSettings().GetCacheHTTPConcurrency()) {
static const auto concurrency = Abaddon::Get().GetSettings().GetCacheHTTPConcurrency();
if (m_handles.size() < concurrency) {
std::optional<QueueEntry> entry;
m_queue_mutex.lock();
if (m_queue.size() > 0) {
@@ -171,8 +176,20 @@ void FileCacheWorkerThread::loop() {
}
}
//int fds;
//curl_multi_wait(m_multi_handle, nullptr, 0, 10, &fds);
fd_set fdread;
fd_set fdwrite;
fd_set fdexcep;
int maxfd = -1;
FD_ZERO(&fdread);
FD_ZERO(&fdwrite);
FD_ZERO(&fdexcep);
curl_multi_fdset(m_multi_handle, &fdread, &fdwrite, &fdexcep, &maxfd);
if (maxfd == -1) {
std::this_thread::sleep_for(std::chrono::milliseconds(100));
} else {
select(maxfd + 1, &fdread, &fdwrite, &fdexcep, &timeout);
}
curl_multi_perform(m_multi_handle, &m_running_handles);
int num_msgs;

BIN
fonts/TwitterColorEmoji.ttf Normal file

Binary file not shown.

View File

@@ -0,0 +1,14 @@
<?xml version="1.0"?>
<!DOCTYPE fontconfig SYSTEM "fonts.dtd">
<fontconfig>
<!-- Use the Autohinter -->
<match target="pattern">
<!--
This configuration is available on the major desktop environments.
We shouldn't overwrite it with "assign" unconditionally.
Most clients may picks up the first value only. so using "append"
may simply works to avoid it.
-->
<edit name="autohint" mode="append"><bool>true</bool></edit>
</match>
</fontconfig>

View File

@@ -0,0 +1,13 @@
<?xml version="1.0"?>
<!DOCTYPE fontconfig SYSTEM "fonts.dtd">
<fontconfig>
<match target="pattern">
<!--
This configuration is available on the major desktop environments.
We shouldn't overwrite it with "assign" unconditionally.
Most clients may picks up the first value only. so using "append"
may simply works to avoid it.
-->
<edit name="hintstyle" mode="append"><const>hintfull</const></edit>
</match>
</fontconfig>

View File

@@ -0,0 +1,13 @@
<?xml version="1.0"?>
<!DOCTYPE fontconfig SYSTEM "fonts.dtd">
<fontconfig>
<match target="pattern">
<!--
This configuration is available on the major desktop environments.
We shouldn't overwrite it with "assign" unconditionally.
Most clients may picks up the first value only. so using "append"
may simply works to avoid it.
-->
<edit name="hintstyle" mode="append"><const>hintmedium</const></edit>
</match>
</fontconfig>

View File

@@ -0,0 +1,13 @@
<?xml version="1.0"?>
<!DOCTYPE fontconfig SYSTEM "fonts.dtd">
<fontconfig>
<match target="pattern">
<!--
This configuration is available on the major desktop environments.
We shouldn't overwrite it with "assign" unconditionally.
Most clients may picks up the first value only. so using "append"
may simply works to avoid it.
-->
<edit name="hintstyle" mode="append"><const>hintnone</const></edit>
</match>
</fontconfig>

View File

@@ -0,0 +1,13 @@
<?xml version="1.0"?>
<!DOCTYPE fontconfig SYSTEM "fonts.dtd">
<fontconfig>
<match target="pattern">
<!--
This configuration is available on the major desktop environments.
We shouldn't overwrite it with "assign" unconditionally.
Most clients may picks up the first value only. so using "append"
may simply works to avoid it.
-->
<edit name="hintstyle" mode="append"><const>hintslight</const></edit>
</match>
</fontconfig>

View File

@@ -0,0 +1,14 @@
<?xml version="1.0"?>
<!DOCTYPE fontconfig SYSTEM "fonts.dtd">
<fontconfig>
<!-- Disable sub-pixel rendering -->
<match target="pattern">
<!--
This configuration is available on the major desktop environments.
We shouldn't overwrite it with "assign" unconditionally.
Most clients may picks up the first value only. so using "append"
may simply works to avoid it.
-->
<edit name="rgba" mode="append"><const>none</const></edit>
</match>
</fontconfig>

View File

@@ -0,0 +1,80 @@
<?xml version="1.0"?>
<!DOCTYPE fontconfig SYSTEM "fonts.dtd">
<fontconfig>
<!--
If font is bitmap, calculate scale factor.
Note that color bitmap fonts have scalable=true, while
non-color ones have scalable=false. Both groups have outline=false.
-->
<match target="font">
<test name="outline" compare="eq">
<bool>false</bool>
</test>
<edit name="pixelsizefixupfactor" mode="assign">
<double>0.15</double>
</edit>
</match>
<!--
For non-scalable bitmap fonts (ie. non-color), skip
minor scaling if hinting is enabled.
-->
<match target="font">
<test name="outline" compare="eq">
<bool>false</bool>
</test>
<test name="scalable" compare="eq">
<bool>false</bool>
</test>
<test name="hinting" compare="eq">
<bool>true</bool>
</test>
<edit name="scalingnotneeded" mode="assign">
<and>
<less>
<name>pixelsizefixupfactor</name>
<double>1.2</double>
</less>
<more>
<name>pixelsizefixupfactor</name>
<double>0.8</double>
</more>
</and>
</edit>
</match>
<match target="font">
<test name="scalingnotneeded" compare="eq">
<bool>true</bool>
</test>
<edit name="pixelsizefixupfactor" mode="assign">
<double>1.0</double>
</edit>
</match>
<!--
If we *are* going to scale, go ahead and do it.
-->
<match target="font">
<test name="outline" compare="eq">
<bool>false</bool>
</test>
<test name="pixelsizefixupfactor" compare="not_eq">
<double>1.0</double>
</test>
<edit name="matrix" mode="assign">
<times>
<name>matrix</name>
<matrix>
<name>pixelsizefixupfactor</name> <double>0</double>
<double>0</double> <name>pixelsizefixupfactor</name>
</matrix>
</times>
</edit>
<edit name="size" mode="assign">
<divide>
<name>size</name>
<name>pixelsizefixupfactor</name>
</divide>
</edit>
</match>
</fontconfig>

View File

@@ -0,0 +1,14 @@
<?xml version="1.0"?>
<!DOCTYPE fontconfig SYSTEM "fonts.dtd">
<fontconfig>
<!-- Enable sub-pixel rendering -->
<match target="pattern">
<!--
This configuration is available on the major desktop environments.
We shouldn't overwrite it with "assign" unconditionally.
Most clients may picks up the first value only. so using "append"
may simply works to avoid it.
-->
<edit name="rgba" mode="append"><const>bgr</const></edit>
</match>
</fontconfig>

View File

@@ -0,0 +1,14 @@
<?xml version="1.0"?>
<!DOCTYPE fontconfig SYSTEM "fonts.dtd">
<fontconfig>
<!-- Enable sub-pixel rendering -->
<match target="pattern">
<!--
This configuration is available on the major desktop environments.
We shouldn't overwrite it with "assign" unconditionally.
Most clients may picks up the first value only. so using "append"
may simply works to avoid it.
-->
<edit name="rgba" mode="append"><const>rgb</const></edit>
</match>
</fontconfig>

View File

@@ -0,0 +1,14 @@
<?xml version="1.0"?>
<!DOCTYPE fontconfig SYSTEM "fonts.dtd">
<fontconfig>
<!-- Enable sub-pixel rendering -->
<match target="pattern">
<!--
This configuration is available on the major desktop environments.
We shouldn't overwrite it with "assign" unconditionally.
Most clients may picks up the first value only. so using "append"
may simply works to avoid it.
-->
<edit name="rgba" mode="append"><const>vbgr</const></edit>
</match>
</fontconfig>

View File

@@ -0,0 +1,14 @@
<?xml version="1.0"?>
<!DOCTYPE fontconfig SYSTEM "fonts.dtd">
<fontconfig>
<!-- Enable sub-pixel rendering -->
<match target="pattern">
<!--
This configuration is available on the major desktop environments.
We shouldn't overwrite it with "assign" unconditionally.
Most clients may picks up the first value only. so using "append"
may simply works to avoid it.
-->
<edit name="rgba" mode="append"><const>vrgb</const></edit>
</match>
</fontconfig>

View File

@@ -0,0 +1,14 @@
<?xml version="1.0"?>
<!DOCTYPE fontconfig SYSTEM "fonts.dtd">
<fontconfig>
<!-- Disable hinting -->
<match target="pattern">
<!--
This configuration is available on the major desktop environments.
We shouldn't overwrite it with "assign" unconditionally.
Most clients may picks up the first value only. so using "append"
may simply works to avoid it.
-->
<edit name="hinting" mode="append"><bool>false</bool></edit>
</match>
</fontconfig>

View File

@@ -0,0 +1,16 @@
<?xml version="1.0"?>
<!DOCTYPE fontconfig SYSTEM "fonts.dtd">
<fontconfig>
<!-- Use lcddefault as default for LCD filter -->
<match target="pattern">
<!--
This configuration is available on the major desktop environments.
We shouldn't overwrite it with "assign" unconditionally.
Most clients may picks up the first value only. so using "append"
may simply works to avoid it.
-->
<edit mode="append" name="lcdfilter">
<const>lcddefault</const>
</edit>
</match>
</fontconfig>

View File

@@ -0,0 +1,16 @@
<?xml version="1.0"?>
<!DOCTYPE fontconfig SYSTEM "fonts.dtd">
<fontconfig>
<!-- Use lcdlegacy as default for LCD filter -->
<match target="pattern">
<!--
This configuration is available on the major desktop environments.
We shouldn't overwrite it with "assign" unconditionally.
Most clients may picks up the first value only. so using "append"
may simply works to avoid it.
-->
<edit mode="append" name="lcdfilter">
<const>lcdlegacy</const>
</edit>
</match>
</fontconfig>

View File

@@ -0,0 +1,16 @@
<?xml version="1.0"?>
<!DOCTYPE fontconfig SYSTEM "fonts.dtd">
<fontconfig>
<!-- Use lcdlight as default for LCD filter -->
<match target="pattern">
<!--
This configuration is available on the major desktop environments.
We shouldn't overwrite it with "assign" unconditionally.
Most clients may picks up the first value only. so using "append"
may simply works to avoid it.
-->
<edit mode="append" name="lcdfilter">
<const>lcdlight</const>
</edit>
</match>
</fontconfig>

View File

@@ -0,0 +1,48 @@
<?xml version="1.0"?>
<!DOCTYPE fontconfig SYSTEM "fonts.dtd">
<fontconfig>
<!--
The Bitstream Vera fonts have GASP entries suggesting that hinting be
disabled below 8 ppem, but FreeType ignores those, preferring to use
the data found in the instructed hints. The initial Vera release
didn't include the right instructions in the 'prep' table. Fix this
by disabling hinting manually at smaller sizes (< 8ppem)
-->
<match target="font">
<test name="family" compare="eq" ignore-blanks="true">
<string>Bitstream Vera Sans</string>
</test>
<test name="pixelsize" compare="less">
<double>7.5</double>
</test>
<edit name="hinting">
<bool>false</bool>
</edit>
</match>
<match target="font">
<test name="family" compare="eq" ignore-blanks="true">
<string>Bitstream Vera Serif</string>
</test>
<test name="pixelsize" compare="less">
<double>7.5</double>
</test>
<edit name="hinting">
<bool>false</bool>
</edit>
</match>
<match target="font">
<test name="family" compare="eq" ignore-blanks="true">
<string>Bitstream Vera Sans Mono</string>
</test>
<test name="pixelsize" compare="less">
<double>7.5</double>
</test>
<edit name="hinting">
<bool>false</bool>
</edit>
</match>
</fontconfig>

View File

@@ -0,0 +1,128 @@
<?xml version="1.0"?>
<!DOCTYPE fontconfig SYSTEM "fonts.dtd">
<fontconfig>
<!-- We can't hint CJK fonts well, so turn off hinting for CJK fonts. -->
<match target="font">
<test name="family" compare="eq" ignore-blanks="true">
<string>Kochi Mincho</string>
</test>
<edit name="hinting" mode="assign">
<bool>false</bool>
</edit>
</match>
<match target="font">
<test name="family" compare="eq" ignore-blanks="true">
<string>Kochi Gothic</string>
</test>
<edit name="hinting" mode="assign">
<bool>false</bool>
</edit>
</match>
<match target="font">
<test name="family" compare="eq" ignore-blanks="true">
<string>Sazanami Mincho</string>
</test>
<edit name="hinting" mode="assign">
<bool>false</bool>
</edit>
</match>
<match target="font">
<test name="family" compare="eq" ignore-blanks="true">
<string>Sazanami Gothic</string>
</test>
<edit name="hinting" mode="assign">
<bool>false</bool>
</edit>
</match>
<match target="font">
<test name="family" compare="eq" ignore-blanks="true">
<string>Baekmuk Batang</string>
</test>
<edit name="hinting" mode="assign">
<bool>false</bool>
</edit>
</match>
<match target="font">
<test name="family" compare="eq" ignore-blanks="true">
<string>Baekmuk Dotum</string>
</test>
<edit name="hinting" mode="assign">
<bool>false</bool>
</edit>
</match>
<match target="font">
<test name="family" compare="eq" ignore-blanks="true">
<string>Baekmuk Gulim</string>
</test>
<edit name="hinting" mode="assign">
<bool>false</bool>
</edit>
</match>
<match target="font">
<test name="family" compare="eq" ignore-blanks="true">
<string>Baekmuk Headline</string>
</test>
<edit name="hinting" mode="assign">
<bool>false</bool>
</edit>
</match>
<match target="font">
<test name="family" compare="eq" ignore-blanks="true">
<string>AR PL Mingti2L Big5</string>
</test>
<edit name="hinting" mode="assign">
<bool>false</bool>
</edit>
</match>
<match target="font">
<test name="family" compare="eq" ignore-blanks="true">
<string>AR PL ShanHeiSun Uni</string>
</test>
<edit name="hinting" mode="assign">
<bool>false</bool>
</edit>
</match>
<match target="font">
<test name="family" compare="eq" ignore-blanks="true">
<string>AR PL KaitiM Big5</string>
</test>
<edit name="hinting" mode="assign">
<bool>false</bool>
</edit>
</match>
<match target="font">
<test name="family" compare="eq" ignore-blanks="true">
<string>AR PL ZenKai Uni</string>
</test>
<edit name="hinting" mode="assign">
<bool>false</bool>
</edit>
</match>
<match target="font">
<test name="family" compare="eq" ignore-blanks="true">
<string>AR PL SungtiL GB</string>
</test>
<edit name="hinting" mode="assign">
<bool>false</bool>
</edit>
</match>
<match target="font">
<test name="family" compare="eq" ignore-blanks="true">
<string>AR PL KaitiM GB</string>
</test>
<edit name="hinting" mode="assign">
<bool>false</bool>
</edit>
</match>
<match target="font">
<test name="family" compare="eq" ignore-blanks="true">
<string>ZYSong18030</string>
</test>
<edit name="hinting" mode="assign">
<bool>false</bool>
</edit>
</match>
</fontconfig>

View File

@@ -0,0 +1,652 @@
<?xml version="1.0"?>
<!DOCTYPE fontconfig SYSTEM "fonts.dtd">
<fontconfig>
<!--
Alias similar/metric-compatible families from various sources:
PostScript fonts: URW fonts: GUST fonts: Windows fonts:
====================== ================== ================= ==================
Helvetica Nimbus Sans TeX Gyre Heros
Helvetica Narrow Nimbus Sans Narrow TeX Gyre Heros Cn
Times Nimbus Roman TeX Gyre Termes
Courier Nimbus Mono PS TeX Gyre Cursor
ITC Avant Garde Gothic URW Gothic TeX Gyre Adventor
ITC Bookman URW Bookman TeX Gyre Bonum Bookman Old Style
ITC Zapf Chancery Z003 TeX Gyre Chorus
Palatino P052 TeX Gyre Pagella Palatino Linotype
New Century Schoolbook C059 TeX Gyre Schola Century Schoolbook
Microsoft fonts: Liberation fonts: Google CrOS core fonts: StarOffice fonts: AMT fonts:
================ ====================== ======================= ================= ==============
Arial Liberation Sans Arimo Albany Albany AMT
Arial Narrow Liberation Sans Narrow
Times New Roman Liberation Serif Tinos Thorndale Thorndale AMT
Courier New Liberation Mono Cousine Cumberland Cumberland AMT
Cambria Caladea
Calibri Carlito
Symbol SymbolNeu
Microsoft fonts: Other fonts:
================ ============
Georgia Gelasio
We want for each of them to fallback to any of these available,
but in an order preferring similar designs first. We do this in three steps:
1) Alias each specific to its generic family.
e.g. Liberation Sans to Arial
2) Weak alias each generic to the other generic of its family.
e.g. Arial to Helvetica
3) Alias each generic to its specifics.
e.g. Arial to Liberation Sans, Arimo, Albany, and Albany AMT
-->
<!-- Map specifics to generics -->
<!-- PostScript -->
<alias binding="same">
<family>Nimbus Sans L</family>
<default>
<family>Helvetica</family>
</default>
</alias>
<alias binding="same">
<family>Nimbus Sans</family>
<default>
<family>Helvetica</family>
</default>
</alias>
<alias binding="same">
<family>TeX Gyre Heros</family>
<default>
<family>Helvetica</family>
</default>
</alias>
<alias binding="same">
<family>Nimbus Sans Narrow</family>
<default>
<family>Helvetica Narrow</family>
</default>
</alias>
<alias binding="same">
<family>TeX Gyre Heros Cn</family>
<default>
<family>Helvetica Narrow</family>
</default>
</alias>
<alias binding="same">
<family>Nimbus Roman No9 L</family>
<default>
<family>Times</family>
</default>
</alias>
<alias binding="same">
<family>Nimbus Roman</family>
<default>
<family>Times</family>
</default>
</alias>
<alias binding="same">
<family>TeX Gyre Termes</family>
<default>
<family>Times</family>
</default>
</alias>
<alias binding="same">
<family>Nimbus Mono L</family>
<default>
<family>Courier</family>
</default>
</alias>
<alias binding="same">
<family>Nimbus Mono</family>
<default>
<family>Courier</family>
</default>
</alias>
<alias binding="same">
<family>Nimbus Mono PS</family>
<default>
<family>Courier</family>
</default>
</alias>
<alias binding="same">
<family>TeX Gyre Cursor</family>
<default>
<family>Courier</family>
</default>
</alias>
<alias binding="same">
<family>Avant Garde</family>
<default>
<family>ITC Avant Garde Gothic</family>
</default>
</alias>
<alias binding="same">
<family>URW Gothic L</family>
<default>
<family>ITC Avant Garde Gothic</family>
</default>
</alias>
<alias binding="same">
<family>URW Gothic</family>
<default>
<family>ITC Avant Garde Gothic</family>
</default>
</alias>
<alias binding="same">
<family>TeX Gyre Adventor</family>
<default>
<family>ITC Avant Garde Gothic</family>
</default>
</alias>
<alias binding="same">
<family>Bookman</family>
<default>
<family>ITC Bookman</family>
</default>
</alias>
<alias binding="same">
<family>URW Bookman L</family>
<default>
<family>ITC Bookman</family>
</default>
</alias>
<alias binding="same">
<family>Bookman URW</family>
<default>
<family>ITC Bookman</family>
</default>
</alias>
<alias binding="same">
<family>URW Bookman</family>
<default>
<family>ITC Bookman</family>
</default>
</alias>
<alias binding="same">
<family>TeX Gyre Bonum</family>
<default>
<family>ITC Bookman</family>
</default>
</alias>
<alias binding="same">
<family>Bookman Old Style</family>
<default>
<family>ITC Bookman</family>
</default>
</alias>
<alias binding="same">
<family>Zapf Chancery</family>
<default>
<family>ITC Zapf Chancery</family>
</default>
</alias>
<alias binding="same">
<family>URW Chancery L</family>
<default>
<family>ITC Zapf Chancery</family>
</default>
</alias>
<alias binding="same">
<family>Chancery URW</family>
<default>
<family>ITC Zapf Chancery</family>
</default>
</alias>
<alias binding="same">
<family>Z003</family>
<default>
<family>ITC Zapf Chancery</family>
</default>
</alias>
<alias binding="same">
<family>TeX Gyre Chorus</family>
<default>
<family>ITC Zapf Chancery</family>
</default>
</alias>
<alias binding="same">
<family>URW Palladio L</family>
<default>
<family>Palatino</family>
</default>
</alias>
<alias binding="same">
<family>Palladio URW</family>
<default>
<family>Palatino</family>
</default>
</alias>
<alias binding="same">
<family>P052</family>
<default>
<family>Palatino</family>
</default>
</alias>
<alias binding="same">
<family>TeX Gyre Pagella</family>
<default>
<family>Palatino</family>
</default>
</alias>
<alias binding="same">
<family>Palatino Linotype</family>
<default>
<family>Palatino</family>
</default>
</alias>
<alias binding="same">
<family>Century Schoolbook L</family>
<default>
<family>New Century Schoolbook</family>
</default>
</alias>
<alias binding="same">
<family>Century SchoolBook URW</family>
<default>
<family>New Century Schoolbook</family>
</default>
</alias>
<alias binding="same">
<family>C059</family>
<default>
<family>New Century Schoolbook</family>
</default>
</alias>
<alias binding="same">
<family>TeX Gyre Schola</family>
<default>
<family>New Century Schoolbook</family>
</default>
</alias>
<alias binding="same">
<family>Century Schoolbook</family>
<default>
<family>New Century Schoolbook</family>
</default>
</alias>
<!-- Microsoft -->
<alias binding="same">
<family>Arimo</family>
<default>
<family>Arial</family>
</default>
</alias>
<alias binding="same">
<family>Liberation Sans</family>
<default>
<family>Arial</family>
</default>
</alias>
<alias binding="same">
<family>Liberation Sans Narrow</family>
<default>
<family>Arial Narrow</family>
</default>
</alias>
<alias binding="same">
<family>Albany</family>
<default>
<family>Arial</family>
</default>
</alias>
<alias binding="same">
<family>Albany AMT</family>
<default>
<family>Arial</family>
</default>
</alias>
<alias binding="same">
<family>Tinos</family>
<default>
<family>Times New Roman</family>
</default>
</alias>
<alias binding="same">
<family>Liberation Serif</family>
<default>
<family>Times New Roman</family>
</default>
</alias>
<alias binding="same">
<family>Thorndale</family>
<default>
<family>Times New Roman</family>
</default>
</alias>
<alias binding="same">
<family>Thorndale AMT</family>
<default>
<family>Times New Roman</family>
</default>
</alias>
<alias binding="same">
<family>Cousine</family>
<default>
<family>Courier New</family>
</default>
</alias>
<alias binding="same">
<family>Liberation Mono</family>
<default>
<family>Courier New</family>
</default>
</alias>
<alias binding="same">
<family>Cumberland</family>
<default>
<family>Courier New</family>
</default>
</alias>
<alias binding="same">
<family>Cumberland AMT</family>
<default>
<family>Courier New</family>
</default>
</alias>
<alias binding="same">
<family>Gelasio</family>
<default>
<family>Georgia</family>
</default>
</alias>
<alias binding="same">
<family>Caladea</family>
<default>
<family>Cambria</family>
</default>
</alias>
<alias binding="same">
<family>Carlito</family>
<default>
<family>Calibri</family>
</default>
</alias>
<alias binding="same">
<family>SymbolNeu</family>
<default>
<family>Symbol</family>
</default>
</alias>
<!-- Accept the other group as fallback -->
<!-- PostScript -->
<alias>
<family>Helvetica</family>
<default>
<family>Arial</family>
</default>
</alias>
<alias>
<family>Helvetica Narrow</family>
<default>
<family>Arial Narrow</family>
</default>
</alias>
<alias>
<family>Times</family>
<default>
<family>Times New Roman</family>
</default>
</alias>
<alias>
<family>Courier</family>
<default>
<family>Courier New</family>
</default>
</alias>
<!-- Microsoft -->
<alias>
<family>Arial</family>
<default>
<family>Helvetica</family>
</default>
</alias>
<alias>
<family>Arial Narrow</family>
<default>
<family>Helvetica Narrow</family>
</default>
</alias>
<alias>
<family>Times New Roman</family>
<default>
<family>Times</family>
</default>
</alias>
<alias>
<family>Courier New</family>
<default>
<family>Courier</family>
</default>
</alias>
<!-- Map generics to specifics -->
<!-- PostScript -->
<alias binding="same">
<family>Helvetica</family>
<accept>
<family>TeX Gyre Heros</family>
<family>Nimbus Sans</family>
<family>Nimbus Sans L</family>
</accept>
</alias>
<alias binding="same">
<family>Helvetica Narrow</family>
<accept>
<family>TeX Gyre Heros Cn</family>
<family>Nimbus Sans Narrow</family>
</accept>
</alias>
<alias binding="same">
<family>Times</family>
<accept>
<family>TeX Gyre Termes</family>
<family>Nimbus Roman</family>
<family>Nimbus Roman No9 L</family>
</accept>
</alias>
<alias binding="same">
<family>Courier</family>
<accept>
<family>TeX Gyre Cursor</family>
<family>Nimbus Mono PS</family>
<family>Nimbus Mono</family>
<family>Nimbus Mono L</family>
</accept>
</alias>
<alias binding="same">
<family>ITC Avant Garde Gothic</family>
<accept>
<family>TeX Gyre Adventor</family>
<family>URW Gothic</family>
<family>URW Gothic L</family>
</accept>
</alias>
<alias binding="same">
<family>ITC Bookman</family>
<accept>
<family>Bookman Old Style</family>
<family>TeX Gyre Bonum</family>
<family>URW Bookman</family>
<family>Bookman URW</family>
<family>URW Bookman L</family>
</accept>
</alias>
<alias binding="same">
<family>ITC Zapf Chancery</family>
<accept>
<family>TeX Gyre Chorus</family>
<family>Z003</family>
<family>Chancery URW</family>
<family>URW Chancery L</family>
</accept>
</alias>
<alias binding="same">
<family>Palatino</family>
<accept>
<family>Palatino Linotype</family>
<family>TeX Gyre Pagella</family>
<family>P052</family>
<family>Palladio URW</family>
<family>URW Palladio L</family>
</accept>
</alias>
<alias binding="same">
<family>New Century Schoolbook</family>
<accept>
<family>Century Schoolbook</family>
<family>TeX Gyre Schola</family>
<family>C059</family>
<family>Century SchoolBook URW</family>
<family>Century Schoolbook L</family>
</accept>
</alias>
<!-- Microsoft -->
<alias binding="same">
<family>Arial</family>
<accept>
<family>Arimo</family>
<family>Liberation Sans</family>
<family>Albany</family>
<family>Albany AMT</family>
</accept>
</alias>
<alias binding="same">
<family>Arial Narrow</family>
<accept>
<family>Liberation Sans Narrow</family>
</accept>
</alias>
<alias binding="same">
<family>Times New Roman</family>
<accept>
<family>Tinos</family>
<family>Liberation Serif</family>
<family>Thorndale</family>
<family>Thorndale AMT</family>
</accept>
</alias>
<alias binding="same">
<family>Courier New</family>
<accept>
<family>Cousine</family>
<family>Liberation Mono</family>
<family>Cumberland</family>
<family>Cumberland AMT</family>
</accept>
</alias>
<alias binding="same">
<family>Georgia</family>
<accept>
<family>Gelasio</family>
</accept>
</alias>
<alias binding="same">
<family>Cambria</family>
<accept>
<family>Caladea</family>
</accept>
</alias>
<alias binding="same">
<family>Calibri</family>
<accept>
<family>Carlito</family>
</accept>
</alias>
<alias binding="same">
<family>Symbol</family>
<accept>
<family>SymbolNeu</family>
</accept>
</alias>
</fontconfig>

View File

@@ -0,0 +1,33 @@
<?xml version="1.0"?>
<!DOCTYPE fontconfig SYSTEM "fonts.dtd">
<fontconfig>
<!--
URW provides metric and shape compatible fonts for some Adobe families.
Most of these are handled in 30-metric-aliases.conf.
-->
<alias binding="same">
<family>Zapf Dingbats</family>
<accept>
<family>D050000L</family>
<family>Dingbats</family>
</accept>
</alias>
<alias binding="same">
<family>ITC Zapf Dingbats</family>
<accept>
<family>D050000L</family>
<family>Dingbats</family>
</accept>
</alias>
<match target="pattern">
<test name="family" compare="eq" ignore-blanks="true">
<string>Symbol</string>
</test>
<edit name="family" mode="append" binding="same">
<string>Standard Symbols PS</string>
</edit>
<edit name="family" mode="append" binding="same">
<string>Standard Symbols L</string>
</edit>
</match>
</fontconfig>

View File

@@ -0,0 +1,231 @@
<?xml version="1.0"?>
<!DOCTYPE fontconfig SYSTEM "fonts.dtd">
<fontconfig>
<!--
Mark common families with their generics so we'll get
something reasonable
-->
<!--
Serif faces
-->
<alias>
<family>Nazli</family>
<default><family>serif</family></default>
</alias>
<alias>
<family>Lotoos</family>
<default><family>serif</family></default>
</alias>
<alias>
<family>Mitra</family>
<default><family>serif</family></default>
</alias>
<alias>
<family>Ferdosi</family>
<default><family>serif</family></default>
</alias>
<alias>
<family>Badr</family>
<default><family>serif</family></default>
</alias>
<alias>
<family>Zar</family>
<default><family>serif</family></default>
</alias>
<alias>
<family>Titr</family>
<default><family>serif</family></default>
</alias>
<alias>
<family>Jadid</family>
<default><family>serif</family></default>
</alias>
<alias>
<family>Kochi Mincho</family>
<default><family>serif</family></default>
</alias>
<alias>
<family>AR PL SungtiL GB</family>
<default><family>serif</family></default>
</alias>
<alias>
<family>AR PL Mingti2L Big5</family>
<default><family>serif</family></default>
</alias>
<alias>
<family> 明朝</family>
<default><family>serif</family></default>
</alias>
<alias>
<family>NanumMyeongjo</family>
<default><family>serif</family></default>
</alias>
<alias>
<family>UnBatang</family>
<default><family>serif</family></default>
</alias>
<alias>
<family>Baekmuk Batang</family>
<default><family>serif</family></default>
</alias>
<alias>
<family>MgOpen Canonica</family>
<default><family>serif</family></default>
</alias>
<alias>
<family>Sazanami Mincho</family>
<default><family>serif</family></default>
</alias>
<alias>
<family>AR PL ZenKai Uni</family>
<default><family>serif</family></default>
</alias>
<alias>
<family>ZYSong18030</family>
<default><family>serif</family></default>
</alias>
<alias>
<family>FreeSerif</family>
<default><family>serif</family></default>
</alias>
<alias>
<family>SimSun</family>
<default><family>serif</family></default>
</alias>
<!--
Sans-serif faces
-->
<alias>
<family>Arshia</family>
<default><family>sans-serif</family></default>
</alias>
<alias>
<family>Elham</family>
<default><family>sans-serif</family></default>
</alias>
<alias>
<family>Farnaz</family>
<default><family>sans-serif</family></default>
</alias>
<alias>
<family>Nasim</family>
<default><family>sans-serif</family></default>
</alias>
<alias>
<family>Sina</family>
<default><family>sans-serif</family></default>
</alias>
<alias>
<family>Roya</family>
<default><family>sans-serif</family></default>
</alias>
<alias>
<family>Koodak</family>
<default><family>sans-serif</family></default>
</alias>
<alias>
<family>Terafik</family>
<default><family>sans-serif</family></default>
</alias>
<alias>
<family>Kochi Gothic</family>
<default><family>sans-serif</family></default>
</alias>
<alias>
<family>AR PL KaitiM GB</family>
<default><family>sans-serif</family></default>
</alias>
<alias>
<family>AR PL KaitiM Big5</family>
<default><family>sans-serif</family></default>
</alias>
<alias>
<family> ゴシック</family>
<default><family>sans-serif</family></default>
</alias>
<alias>
<family>NanumGothic</family>
<default><family>sans-serif</family></default>
</alias>
<alias>
<family>UnDotum</family>
<default><family>sans-serif</family></default>
</alias>
<alias>
<family>Baekmuk Dotum</family>
<default><family>sans-serif</family></default>
</alias>
<alias>
<family>MgOpen Modata</family>
<default><family>sans-serif</family></default>
</alias>
<alias>
<family>Sazanami Gothic</family>
<default><family>sans-serif</family></default>
</alias>
<alias>
<family>AR PL ShanHeiSun Uni</family>
<default><family>sans-serif</family></default>
</alias>
<alias>
<family>ZYSong18030</family>
<default><family>sans-serif</family></default>
</alias>
<alias>
<family>FreeSans</family>
<default><family>sans-serif</family></default>
</alias>
<!--
Monospace faces
-->
<alias>
<family>NSimSun</family>
<default><family>monospace</family></default>
</alias>
<alias>
<family>ZYSong18030</family>
<default><family>monospace</family></default>
</alias>
<alias>
<family>NanumGothicCoding</family>
<default><family>monospace</family></default>
</alias>
<alias>
<family>FreeMono</family>
<default><family>monospace</family></default>
</alias>
<!--
Fantasy faces
-->
<alias>
<family>Homa</family>
<default><family>fantasy</family></default>
</alias>
<alias>
<family>Kamran</family>
<default><family>fantasy</family></default>
</alias>
<alias>
<family>Fantezi</family>
<default><family>fantasy</family></default>
</alias>
<alias>
<family>Tabassom</family>
<default><family>fantasy</family></default>
</alias>
<!--
Cursive faces
-->
<alias>
<family>IranNastaliq</family>
<default><family>cursive</family></default>
</alias>
<alias>
<family>Nafees Nastaleeq</family>
<default><family>cursive</family></default>
</alias>
</fontconfig>

273
fonts/conf.d/45-latin.conf Normal file
View File

@@ -0,0 +1,273 @@
<?xml version="1.0"?>
<!DOCTYPE fontconfig SYSTEM "fonts.dtd">
<fontconfig>
<!--
Mark common families with their generics so we'll get
something reasonable
-->
<!--
Serif faces
-->
<alias>
<family>Bitstream Vera Serif</family>
<default><family>serif</family></default>
</alias>
<alias>
<family>Cambria</family>
<default><family>serif</family></default>
</alias>
<alias>
<family>Constantia</family>
<default><family>serif</family></default>
</alias>
<alias>
<family>DejaVu Serif</family>
<default><family>serif</family></default>
</alias>
<alias>
<family>Elephant</family>
<default><family>serif</family></default>
</alias>
<alias>
<family>Garamond</family>
<default><family>serif</family></default>
</alias>
<alias>
<family>Georgia</family>
<default><family>serif</family></default>
</alias>
<alias>
<family>Liberation Serif</family>
<default><family>serif</family></default>
</alias>
<alias>
<family>Luxi Serif</family>
<default><family>serif</family></default>
</alias>
<alias>
<family>MS Serif</family>
<default><family>serif</family></default>
</alias>
<alias>
<family>Nimbus Roman No9 L</family>
<default><family>serif</family></default>
</alias>
<alias>
<family>Nimbus Roman</family>
<default><family>serif</family></default>
</alias>
<alias>
<family>Palatino Linotype</family>
<default><family>serif</family></default>
</alias>
<alias>
<family>Thorndale AMT</family>
<default><family>serif</family></default>
</alias>
<alias>
<family>Thorndale</family>
<default><family>serif</family></default>
</alias>
<alias>
<family>Times New Roman</family>
<default><family>serif</family></default>
</alias>
<alias>
<family>Times</family>
<default><family>serif</family></default>
</alias>
<!--
Sans-serif faces
-->
<alias>
<family>Albany AMT</family>
<default><family>sans-serif</family></default>
</alias>
<alias>
<family>Albany</family>
<default><family>sans-serif</family></default>
</alias>
<alias>
<family>Arial Unicode MS</family>
<default><family>sans-serif</family></default>
</alias>
<alias>
<family>Arial</family>
<default><family>sans-serif</family></default>
</alias>
<alias>
<family>Bitstream Vera Sans</family>
<default><family>sans-serif</family></default>
</alias>
<alias>
<family>Britannic</family>
<default><family>sans-serif</family></default>
</alias>
<alias>
<family>Calibri</family>
<default><family>sans-serif</family></default>
</alias>
<alias>
<family>Candara</family>
<default><family>sans-serif</family></default>
</alias>
<alias>
<family>Century Gothic</family>
<default><family>sans-serif</family></default>
</alias>
<alias>
<family>Corbel</family>
<default><family>sans-serif</family></default>
</alias>
<alias>
<family>DejaVu Sans</family>
<default><family>sans-serif</family></default>
</alias>
<alias>
<family>Helvetica</family>
<default><family>sans-serif</family></default>
</alias>
<alias>
<family>Haettenschweiler</family>
<default><family>sans-serif</family></default>
</alias>
<alias>
<family>Liberation Sans</family>
<default><family>sans-serif</family></default>
</alias>
<alias>
<family>MS Sans Serif</family>
<default><family>sans-serif</family></default>
</alias>
<alias>
<family>Nimbus Sans L</family>
<default><family>sans-serif</family></default>
</alias>
<alias>
<family>Nimbus Sans</family>
<default><family>sans-serif</family></default>
</alias>
<alias>
<family>Luxi Sans</family>
<default><family>sans-serif</family></default>
</alias>
<alias>
<family>Tahoma</family>
<default><family>sans-serif</family></default>
</alias>
<alias>
<family>Trebuchet MS</family>
<default><family>sans-serif</family></default>
</alias>
<alias>
<family>Twentieth Century</family>
<default><family>sans-serif</family></default>
</alias>
<alias>
<family>Verdana</family>
<default><family>sans-serif</family></default>
</alias>
<!--
Monospace faces
-->
<alias>
<family>Andale Mono</family>
<default><family>monospace</family></default>
</alias>
<alias>
<family>Bitstream Vera Sans Mono</family>
<default><family>monospace</family></default>
</alias>
<alias>
<family>Consolas</family>
<default><family>monospace</family></default>
</alias>
<alias>
<family>Courier New</family>
<default><family>monospace</family></default>
</alias>
<alias>
<family>Courier</family>
<default><family>monospace</family></default>
</alias>
<alias>
<family>Cumberland AMT</family>
<default><family>monospace</family></default>
</alias>
<alias>
<family>Cumberland</family>
<default><family>monospace</family></default>
</alias>
<alias>
<family>DejaVu Sans Mono</family>
<default><family>monospace</family></default>
</alias>
<alias>
<family>Fixedsys</family>
<default><family>monospace</family></default>
</alias>
<alias>
<family>Inconsolata</family>
<default><family>monospace</family></default>
</alias>
<alias>
<family>Liberation Mono</family>
<default><family>monospace</family></default>
</alias>
<alias>
<family>Luxi Mono</family>
<default><family>monospace</family></default>
</alias>
<alias>
<family>Nimbus Mono L</family>
<default><family>monospace</family></default>
</alias>
<alias>
<family>Nimbus Mono</family>
<default><family>monospace</family></default>
</alias>
<alias>
<family>Nimbus Mono PS</family>
<default><family>monospace</family></default>
</alias>
<alias>
<family>Terminal</family>
<default><family>monospace</family></default>
</alias>
<!--
Fantasy faces
-->
<alias>
<family>Bauhaus Std</family>
<default><family>fantasy</family></default>
</alias>
<alias>
<family>Cooper Std</family>
<default><family>fantasy</family></default>
</alias>
<alias>
<family>Copperplate Gothic Std</family>
<default><family>fantasy</family></default>
</alias>
<alias>
<family>Impact</family>
<default><family>fantasy</family></default>
</alias>
<!--
Cursive faces
-->
<alias>
<family>Comic Sans MS</family>
<default><family>cursive</family></default>
</alias>
<alias>
<family>ITC Zapf Chancery Std</family>
<default><family>cursive</family></default>
</alias>
<alias>
<family>Zapfino</family>
<default><family>cursive</family></default>
</alias>
</fontconfig>

View File

@@ -0,0 +1,21 @@
<?xml version="1.0"?>
<!DOCTYPE fontconfig SYSTEM "fonts.dtd">
<fontconfig>
<!--
If the font still has no generic name, add sans-serif
-->
<match target="pattern">
<test qual="all" name="family" compare="not_eq">
<string>sans-serif</string>
</test>
<test qual="all" name="family" compare="not_eq">
<string>serif</string>
</test>
<test qual="all" name="family" compare="not_eq">
<string>monospace</string>
</test>
<edit name="family" mode="append_last">
<string>sans-serif</string>
</edit>
</match>
</fontconfig>

15
fonts/conf.d/50-user.conf Normal file
View File

@@ -0,0 +1,15 @@
<?xml version="1.0"?>
<!DOCTYPE fontconfig SYSTEM "fonts.dtd">
<fontconfig>
<!--
Load per-user customization files where stored on XDG Base Directory
specification compliant places. it should be usually:
$HOME/.config/fontconfig/conf.d
$HOME/.config/fontconfig/fonts.conf
-->
<include ignore_missing="yes" prefix="xdg">fontconfig/conf.d</include>
<include ignore_missing="yes" prefix="xdg">fontconfig/fonts.conf</include>
<!-- the following elements will be removed in the future -->
<include ignore_missing="yes" deprecated="yes">~/.fonts.conf.d</include>
<include ignore_missing="yes" deprecated="yes">~/.fonts.conf</include>
</fontconfig>

View File

@@ -0,0 +1,6 @@
<?xml version="1.0"?>
<!DOCTYPE fontconfig SYSTEM "fonts.dtd">
<fontconfig>
<!-- Load local system customization file -->
<include ignore_missing="yes">local.conf</include>
</fontconfig>

View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE fontconfig SYSTEM "fonts.dtd">
<fontconfig>
<match>
<edit name="family" mode="prepend" binding="weak">
<string>Twitter Color Emoji</string>
</edit>
</match>
</fontconfig>

View File

@@ -0,0 +1,74 @@
<?xml version="1.0"?>
<!DOCTYPE fontconfig SYSTEM "fonts.dtd">
<fontconfig>
<alias>
<family>serif</family>
<prefer>
<family>Bitstream Vera Serif</family>
<family>DejaVu Serif</family>
<family>Times New Roman</family>
<family>Thorndale AMT</family>
<family>Luxi Serif</family>
<family>Nimbus Roman No9 L</family>
<family>Nimbus Roman</family>
<family>Times</family>
</prefer>
</alias>
<alias>
<family>sans-serif</family>
<prefer>
<family>Bitstream Vera Sans</family>
<family>DejaVu Sans</family>
<family>Verdana</family>
<family>Arial</family>
<family>Albany AMT</family>
<family>Luxi Sans</family>
<family>Nimbus Sans L</family>
<family>Nimbus Sans</family>
<family>Helvetica</family>
<family>Lucida Sans Unicode</family>
<family>BPG Glaho International</family> <!-- lat,cyr,arab,geor -->
<family>Tahoma</family> <!-- lat,cyr,greek,heb,arab,thai -->
</prefer>
</alias>
<alias>
<family>monospace</family>
<prefer>
<family>Bitstream Vera Sans Mono</family>
<family>DejaVu Sans Mono</family>
<family>Inconsolata</family>
<family>Andale Mono</family>
<family>Courier New</family>
<family>Cumberland AMT</family>
<family>Luxi Mono</family>
<family>Nimbus Mono L</family>
<family>Nimbus Mono</family>
<family>Nimbus Mono PS</family>
<family>Courier</family>
</prefer>
</alias>
<!--
Fantasy faces
-->
<alias>
<family>fantasy</family>
<prefer>
<family>Impact</family>
<family>Copperplate Gothic Std</family>
<family>Cooper Std</family>
<family>Bauhaus Std</family>
</prefer>
</alias>
<!--
Cursive faces
-->
<alias>
<family>cursive</family>
<prefer>
<family>ITC Zapf Chancery Std</family>
<family>Zapfino</family>
<family>Comic Sans MS</family>
</prefer>
</alias>
</fontconfig>

View File

@@ -0,0 +1,419 @@
<?xml version="1.0"?>
<!DOCTYPE fontconfig SYSTEM "fonts.dtd">
<!--
fonts-persian.conf
To configure Persian fonts from The FarsiWeb Project.
Copyright (C) 2005 Sharif FarsiWeb, Inc. <license@farsiweb.info>
Permission to use, copy, modify, distribute, and sell this software and its
documentation for any purpose is hereby granted without fee, provided that
the above copyright notice appear in all copies and that both that
copyright notice and this permission notice appear in supporting
documentation, and that the name of Sharif FarsiWeb, Inc. not be used in
advertising or publicity pertaining to distribution of the software without
specific, written prior permission. Sharif FarsiWeb, Inc. makes no
representations about the suitability of this software for any purpose. It
is provided "as is" without express or implied warranty.
SHARIF FARSIWEB, INC. DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE,
INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS, IN NO
EVENT SHALL THE AUTHOR(S) BE LIABLE FOR ANY SPECIAL, INDIRECT OR
CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE,
DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER
TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
PERFORMANCE OF THIS SOFTWARE.
ChangeLog:
2005-04-03 Behdad Esfahbod: Initial revision.
2005-10-09 Behdad Esfahbod: Turned off back-slant and Tahoma sections.
2005-11-30 Behdad Esfahbod: Set Titr susbtitution size to 24 points.
2008 Behdad Esfahbod: Cleanup. Add fantasy and cursive.
-->
<fontconfig>
<!-- Deprecated fonts are discouraged -->
<!-- Nesf[2] is officially deprecated and has problematic tables -->
<alias binding="same">
<family>Nesf</family>
<accept><family>Nesf2</family></accept>
</alias>
<alias binding="same">
<family>Nesf2</family>
<accept><family>Persian_sansserif_default</family></accept>
</alias>
<!-- Name changes and spelling variant aliases -->
<alias binding="same">
<family>Nazanin</family>
<accept><family>Nazli</family></accept>
</alias>
<alias binding="same">
<family>Lotus</family>
<accept><family>Lotoos</family></accept>
</alias>
<alias binding="same">
<family>Yaqut</family>
<accept><family>Yaghoot</family></accept>
</alias>
<alias binding="same">
<family>Yaghut</family>
<accept><family>Yaghoot</family></accept>
</alias>
<alias binding="same">
<family>Traffic</family>
<accept><family>Terafik</family></accept>
</alias>
<alias binding="same">
<family>Ferdowsi</family>
<accept><family>Ferdosi</family></accept>
</alias>
<alias binding="same">
<family>Fantezy</family>
<accept><family>Fantezi</family></accept>
</alias>
<!-- Classify fonts. -->
<!-- Persian_title class -->
<alias binding="same">
<family>Jadid</family>
<accept><family>Persian_title</family></accept>
</alias>
<alias binding="same">
<family>Titr</family>
<accept><family>Persian_title</family></accept>
</alias>
<!-- Persian_fantasy class -->
<alias binding="same">
<family>Kamran</family>
<accept>
<family>Persian_fantasy</family>
<family>Homa</family>
</accept>
</alias>
<alias binding="same">
<family>Homa</family>
<accept>
<family>Persian_fantasy</family>
<family>Kamran</family>
</accept>
</alias>
<alias binding="same">
<family>Fantezi</family>
<accept><family>Persian_fantasy</family></accept>
</alias>
<alias binding="same">
<family>Tabassom</family>
<accept><family>Persian_fantasy</family></accept>
</alias>
<!-- Persian_square class -->
<alias binding="same">
<family>Arshia</family>
<accept><family>Persian_square</family></accept>
</alias>
<alias binding="same">
<family>Nasim</family>
<accept><family>Persian_square</family></accept>
</alias>
<alias binding="same">
<family>Elham</family>
<accept>
<family>Persian_square</family>
<family>Farnaz</family>
</accept>
</alias>
<alias binding="same">
<family>Farnaz</family>
<accept>
<family>Persian_square</family>
<family>Elham</family>
</accept>
</alias>
<alias binding="same">
<family>Sina</family>
<accept><family>Persian_square</family></accept>
</alias>
<!-- Font ordering per class -->
<!-- Persian_title class -->
<alias binding="same">
<family>Persian_title</family>
<accept>
<family>Titr</family>
<family>Jadid</family>
<family>Persian_serif</family>
</accept>
</alias>
<!-- Persian_fantasy class -->
<alias binding="same">
<family>Persian_fantasy</family>
<accept>
<family>Homa</family>
<family>Kamran</family>
<family>Fantezi</family>
<family>Tabassom</family>
<family>Persian_square</family>
</accept>
</alias>
<!-- Persian_square class -->
<alias binding="same">
<family>Persian_square</family>
<accept>
<family>Arshia</family>
<family>Elham</family>
<family>Farnaz</family>
<family>Nasim</family>
<family>Sina</family>
<family>Persian_serif</family>
</accept>
</alias>
<!-- Register the fonts that we actually do have -->
<match target="scan">
<test name="family" compare="eq" ignore-blanks="true">
<string>Elham</string>
</test>
<edit name="foundry">
<string>farsiweb</string>
</edit>
</match>
<match target="scan">
<test name="family" compare="eq" ignore-blanks="true">
<string>Homa</string>
</test>
<edit name="foundry">
<string>farsiweb</string>
</edit>
</match>
<match target="scan">
<test name="family" compare="eq" ignore-blanks="true">
<string>Koodak</string>
</test>
<edit name="foundry">
<string>farsiweb</string>
</edit>
</match>
<match target="scan">
<test name="family" compare="eq" ignore-blanks="true">
<string>Nazli</string>
</test>
<edit name="foundry">
<string>farsiweb</string>
</edit>
</match>
<match target="scan">
<test name="family" compare="eq" ignore-blanks="true">
<string>Roya</string>
</test>
<edit name="foundry">
<string>farsiweb</string>
</edit>
</match>
<match target="scan">
<test name="family" compare="eq" ignore-blanks="true">
<string>Terafik</string>
</test>
<edit name="foundry">
<string>farsiweb</string>
</edit>
</match>
<match target="scan">
<test name="family" compare="eq" ignore-blanks="true">
<string>Titr</string>
</test>
<edit name="foundry">
<string>farsiweb</string>
</edit>
</match>
<!-- Our fonts should oblique to the other side (TURNED-OFF) -->
<match target="font">
<test name="foundry">
<!--string>farsiweb</string-->
<string>TURNED-OFF</string>
</test>
<test name="foundry">
<string>farsiweb</string>
</test>
<!-- check to see if the font is roman -->
<test name="slant">
<const>roman</const>
</test>
<!-- check to see if the pattern requested non-roman -->
<test target="pattern" name="slant" compare="not_eq">
<const>roman</const>
</test>
<!-- multiply the matrix to slant the font -->
<edit name="matrix" mode="assign">
<times>
<name>matrix</name>
<matrix><double>1</double><double>-0.2</double>
<double>0</double><double>1</double>
</matrix>
</times>
</edit>
<!-- pretend the font is oblique now -->
<edit name="slant" mode="assign">
<const>oblique</const>
</edit>
</match>
<!--
We can't hint our fonts well, so turn off hinting.
Moreover, the bitmaps we have designed (well, they
have designed), suck, so disable them too.
-->
<match target="font">
<test name="foundry">
<string>farsiweb</string>
</test>
<edit name="autohint">
<bool>false</bool>
</edit>
<edit name="hinting">
<bool>false</bool>
</edit>
<edit name="embeddedbitmap">
<bool>false</bool>
</edit>
</match>
<!-- Alias our fonts to common families -->
<!-- Persian serif fonts -->
<alias>
<family>serif</family>
<accept>
<family>Nazli</family>
<family>Lotoos</family>
<family>Mitra</family>
<family>Ferdosi</family>
<family>Badr</family>
<family>Zar</family>
</accept>
</alias>
<!-- Persian sans-serif fonts -->
<alias>
<family>sans-serif</family>
<accept>
<family>Roya</family>
<family>Koodak</family>
<family>Terafik</family>
</accept>
</alias>
<!-- Persian monospace fonts -->
<alias>
<family>monospace</family>
<accept>
<!-- Not really monospace -->
<family>Terafik</family>
</accept>
</alias>
<!-- Persian fantasy fonts -->
<alias>
<family>fantasy</family>
<accept>
<family>Homa</family>
<family>Kamran</family>
<family>Fantezi</family>
<family>Tabassom</family>
</accept>
</alias>
<!-- Persian (and Urdu) Nastaliq/cursive fonts -->
<alias>
<family>cursive</family>
<accept>
<family>IranNastaliq</family>
<family>Nafees Nastaleeq</family>
</accept>
</alias>
<!-- Use Titr in titles -->
<!-- Both serif... -->
<match>
<test name="family">
<string>serif</string>
</test>
<test name="weight" compare="more_eq">
<int>200</int>
</test>
<test name="size" compare="more_eq">
<double>24</double>
</test>
<edit name="family" mode="prepend">
<string>Titr</string>
</edit>
</match>
<!-- and sans-serif. -->
<match>
<test name="family">
<string>sans-serif</string>
</test>
<test name="weight" compare="more_eq">
<int>200</int>
</test>
<test name="size" compare="more_eq">
<double>24</double>
</test>
<edit name="family" mode="prepend">
<string>Titr</string>
</edit>
</match>
<!-- and more. -->
<match>
<test name="family">
<string>Persian_sansserif_default</string>
</test>
<test name="weight" compare="more_eq">
<int>200</int>
</test>
<test name="size" compare="more_eq">
<double>24</double>
</test>
<edit name="family" mode="prepend" binding="same">
<string>Titr</string>
</edit>
</match>
<!-- Default substituted for deprecated sans-serif fonts -->
<match>
<test name="family">
<string>Persian_sansserif_default</string>
</test>
<edit name="family" mode="assign" binding="same">
<string>Roya</string>
</edit>
</match>
</fontconfig>

View File

@@ -0,0 +1,16 @@
<?xml version="1.0"?>
<!DOCTYPE fontconfig SYSTEM "fonts.dtd">
<fontconfig>
<alias>
<family>serif</family>
<prefer>
<family>Khmer OS"</family>
</prefer>
</alias>
<alias>
<family>sans-serif</family>
<prefer>
<family>Khmer OS"</family>
</prefer>
</alias>
</fontconfig>

View File

@@ -0,0 +1,196 @@
<?xml version="1.0"?>
<!DOCTYPE fontconfig SYSTEM "fonts.dtd">
<fontconfig>
<alias>
<family>serif</family>
<prefer>
<family>Artsounk</family> <!-- armenian -->
<family>BPG UTF8 M</family> <!-- georgian -->
<family>Kinnari</family> <!-- thai -->
<family>Norasi</family> <!-- thai -->
<family>Frank Ruehl</family> <!-- hebrew -->
<family>Dror</family> <!-- hebrew -->
<family>JG LaoTimes</family> <!-- lao -->
<family>Saysettha Unicode</family> <!-- lao -->
<family>Pigiarniq</family> <!-- canadian syllabics -->
<family>B Davat</family> <!-- arabic (fa) -->
<family>B Compset</family> <!-- arabic (fa) -->
<family>Kacst-Qr</family> <!-- arabic (ar) -->
<family>Urdu Nastaliq Unicode</family> <!-- arabic (ur) -->
<family>Raghindi</family> <!-- devanagari -->
<family>Mukti Narrow</family> <!-- bengali -->
<family>malayalam</family> <!-- malayalam -->
<family>Sampige</family> <!-- kannada -->
<family>padmaa</family> <!-- gujarati -->
<family>Hapax Berbère</family> <!-- tifinagh -->
<family>MS Mincho</family> <!-- han (ja) -->
<family>SimSun</family> <!-- han (zh-cn,zh-tw) -->
<family>PMingLiu</family> <!-- han (zh-tw) -->
<family>WenQuanYi Zen Hei</family> <!-- han (zh-cn,zh-tw) -->
<family>WenQuanYi Bitmap Song</family> <!-- han (zh-cn,zh-tw) -->
<family>AR PL ShanHeiSun Uni</family> <!-- han (ja,zh-cn,zh-tw) -->
<family>AR PL New Sung</family> <!-- han (zh-cn,zh-tw) -->
<family>ZYSong18030</family> <!-- han (zh-cn,zh-tw) -->
<family>HanyiSong</family> <!-- han (zh-cn,zh-tw) -->
<family>MgOpen Canonica</family>
<family>Sazanami Mincho</family>
<family>IPAMonaMincho</family>
<family>IPAMincho</family>
<family>Kochi Mincho</family>
<family>AR PL SungtiL GB</family>
<family>AR PL Mingti2L Big5</family>
<family>AR PL Zenkai Uni</family>
<family> 明朝</family>
<family>ZYSong18030</family>
<family>NanumMyeongjo</family> <!-- hangul (ko) -->
<family>UnBatang</family> <!-- hangul (ko) -->
<family>Baekmuk Batang</family> <!-- hangul (ko) -->
<family>KacstQura</family>
<family>Frank Ruehl CLM</family>
<family>Lohit Bengali</family>
<family>Lohit Gujarati</family>
<family>Lohit Hindi</family>
<family>Lohit Marathi</family>
<family>Lohit Maithili</family>
<family>Lohit Kashmiri</family>
<family>Lohit Konkani</family>
<family>Lohit Nepali</family>
<family>Lohit Sindhi</family>
<family>Lohit Punjabi</family>
<family>Lohit Tamil</family>
<family>Meera</family>
<family>Lohit Malayalam</family>
<family>Lohit Kannada</family>
<family>Lohit Telugu</family>
<family>Lohit Oriya</family>
<family>LKLUG</family>
</prefer>
</alias>
<alias>
<family>sans-serif</family>
<prefer>
<family>Nachlieli</family> <!-- hebrew -->
<family>Lucida Sans Unicode</family>
<family>Yudit Unicode</family>
<family>Kerkis</family> <!-- greek -->
<family>ArmNet Helvetica</family> <!-- armenian -->
<family>Artsounk</family> <!-- armenian -->
<family>BPG UTF8 M</family> <!-- georgian -->
<family>Waree</family> <!-- thai -->
<family>Loma</family> <!-- thai -->
<family>Garuda</family> <!-- thai -->
<family>Umpush</family> <!-- thai -->
<family>Saysettha Unicode</family> <!-- lao? -->
<family>JG Lao Old Arial</family> <!-- lao -->
<family>GF Zemen Unicode</family> <!-- ethiopic -->
<family>Pigiarniq</family> <!-- canadian syllabics -->
<family>B Davat</family> <!-- arabic (fa) -->
<family>B Compset</family> <!-- arabic (fa) -->
<family>Kacst-Qr</family> <!-- arabic (ar) -->
<family>Urdu Nastaliq Unicode</family> <!-- arabic (ur) -->
<family>Raghindi</family> <!-- devanagari -->
<family>Mukti Narrow</family> <!-- bengali -->
<family>malayalam</family> <!-- malayalam -->
<family>Sampige</family> <!-- kannada -->
<family>padmaa</family> <!-- gujarati -->
<family>Hapax Berbère</family> <!-- tifinagh -->
<family>MS Gothic</family> <!-- han (ja) -->
<family>UmePlus P Gothic</family> <!-- han (ja) -->
<!-- chinese fonts are actually serifed -->
<family>SimSun</family> <!-- han (zh-cn,zh-tw) -->
<family>PMingLiu</family> <!-- han (zh-tw) -->
<family>WenQuanYi Zen Hei</family> <!-- han (zh-cn,zh-tw) -->
<family>WenQuanYi Bitmap Song</family> <!-- han (zh-cn,zh-tw) -->
<family>AR PL ShanHeiSun Uni</family> <!--han (ja,zh-cn,zh-tw) -->
<family>AR PL New Sung</family> <!-- han (zh-cn,zh-tw) -->
<family>MgOpen Modata</family>
<family>VL Gothic</family>
<family>IPAMonaGothic</family>
<family>IPAGothic</family>
<family>Sazanami Gothic</family>
<family>Kochi Gothic</family>
<family>AR PL KaitiM GB</family>
<family>AR PL KaitiM Big5</family>
<family>AR PL ShanHeiSun Uni</family>
<family>AR PL SungtiL GB</family>
<family>AR PL Mingti2L Big5</family>
<family> ゴシック</family>
<family>ZYSong18030</family> <!-- han (zh-cn,zh-tw) -->
<family>TSCu_Paranar</family> <!-- tamil -->
<family>NanumGothic</family> <!-- hangul (ko) -->
<family>UnDotum</family> <!-- hangul (ko) -->
<family>Baekmuk Dotum</family> <!-- hangul (ko) -->
<family>Baekmuk Gulim</family> <!-- hangul (ko) -->
<family>KacstQura</family>
<family>Lohit Bengali</family>
<family>Lohit Gujarati</family>
<family>Lohit Hindi</family>
<family>Lohit Marathi</family>
<family>Lohit Maithili</family>
<family>Lohit Kashmiri</family>
<family>Lohit Konkani</family>
<family>Lohit Nepali</family>
<family>Lohit Sindhi</family>
<family>Lohit Punjabi</family>
<family>Lohit Tamil</family>
<family>Meera</family>
<family>Lohit Malayalam</family>
<family>Lohit Kannada</family>
<family>Lohit Telugu</family>
<family>Lohit Oriya</family>
<family>LKLUG</family>
</prefer>
</alias>
<alias>
<family>monospace</family>
<prefer>
<family>Miriam Mono</family> <!-- hebrew -->
<family>VL Gothic</family>
<family>IPAMonaGothic</family>
<family>IPAGothic</family>
<family>Sazanami Gothic</family>
<family>Kochi Gothic</family>
<family>AR PL KaitiM GB</family>
<family>MS Gothic</family> <!-- han (ja) -->
<family>UmePlus Gothic</family> <!-- han (ja) -->
<family>NSimSun</family> <!-- han (zh-cn,zh-tw) -->
<family>MingLiu</family> <!-- han (zh-tw) -->
<family>AR PL ShanHeiSun Uni</family> <!-- han (ja,zh-cn,zh-tw) -->
<family>AR PL New Sung Mono</family> <!-- han (zh-cn,zh-tw) -->
<family>HanyiSong</family> <!-- han (zh-cn) -->
<family>AR PL SungtiL GB</family>
<family>AR PL Mingti2L Big5</family>
<family>ZYSong18030</family> <!-- han (zh-cn,zh-tw) -->
<family>NanumGothicCoding</family> <!-- hangul (ko) -->
<family>NanumGothic</family> <!-- hangul (ko) -->
<family>UnDotum</family> <!-- hangul (ko) -->
<family>Baekmuk Dotum</family> <!-- hangul (ko) -->
<family>Baekmuk Gulim</family> <!-- hangul (ko) -->
<family>TlwgTypo</family> <!-- thai -->
<family>TlwgTypist</family> <!-- thai -->
<family>TlwgTypewriter</family> <!-- thai -->
<family>TlwgMono</family> <!-- thai -->
<family>Hasida</family> <!-- hebrew -->
<family>Mitra Mono</family> <!-- bengali -->
<family>GF Zemen Unicode</family> <!-- ethiopic -->
<family>Hapax Berbère</family> <!-- tifinagh -->
<family>Lohit Bengali</family>
<family>Lohit Gujarati</family>
<family>Lohit Hindi</family>
<family>Lohit Marathi</family>
<family>Lohit Maithili</family>
<family>Lohit Kashmiri</family>
<family>Lohit Konkani</family>
<family>Lohit Nepali</family>
<family>Lohit Sindhi</family>
<family>Lohit Punjabi</family>
<family>Lohit Tamil</family>
<family>Meera</family>
<family>Lohit Malayalam</family>
<family>Lohit Kannada</family>
<family>Lohit Telugu</family>
<family>Lohit Oriya</family>
<family>LKLUG</family>
</prefer>
</alias>
</fontconfig>

View File

@@ -0,0 +1,28 @@
<?xml version="1.0"?>
<!DOCTYPE fontconfig SYSTEM "fonts.dtd">
<fontconfig>
<alias>
<family>serif</family>
<prefer>
<family>FreeSerif</family>
<family>Code2000</family>
<family>Code2001</family> <!-- plane1 and beyond -->
</prefer>
</alias>
<alias>
<family>sans-serif</family>
<prefer>
<family>FreeSans</family>
<family>Arial Unicode MS</family>
<family>Arial Unicode</family>
<family>Code2000</family> <!-- almost everything; serif actually -->
<family>Code2001</family> <!-- plane1 and beyond -->
</prefer>
</alias>
<alias>
<family>monospace</family>
<prefer>
<family>FreeMono</family>
</prefer>
</alias>
</fontconfig>

View File

@@ -0,0 +1,12 @@
<?xml version="1.0"?>
<!DOCTYPE fontconfig SYSTEM "fonts.dtd">
<fontconfig>
<!-- Accept bitmap fonts -->
<selectfont>
<acceptfont>
<pattern>
<patelt name="scalable"><bool>false</bool></patelt>
</pattern>
</acceptfont>
</selectfont>
</fontconfig>

View File

@@ -0,0 +1,19 @@
<?xml version="1.0"?>
<!DOCTYPE fontconfig SYSTEM "fonts.dtd">
<fontconfig>
<!-- Fix-ups for Delicious family -->
<!-- Delicious 'heavy' variant says its Medium weight -->
<match target="scan">
<test name="family" compare="eq" ignore-blanks="true">
<string>Delicious</string>
</test>
<test name="style">
<string>Heavy</string>
</test>
<edit name="weight">
<const>heavy</const>
</edit>
</match>
</fontconfig>

View File

@@ -0,0 +1,64 @@
<?xml version="1.0"?>
<!DOCTYPE fontconfig SYSTEM "fonts.dtd">
<fontconfig>
<!--
Artificial oblique for fonts without an italic or oblique version
-->
<match target="font">
<!-- check to see if the font is roman -->
<test name="slant">
<const>roman</const>
</test>
<!-- check to see if the pattern requested non-roman -->
<test target="pattern" name="slant" compare="not_eq">
<const>roman</const>
</test>
<!-- multiply the matrix to slant the font -->
<edit name="matrix" mode="assign">
<times>
<name>matrix</name>
<matrix><double>1</double><double>0.2</double>
<double>0</double><double>1</double>
</matrix>
</times>
</edit>
<!-- pretend the font is oblique now -->
<edit name="slant" mode="assign">
<const>oblique</const>
</edit>
<!-- and disable embedded bitmaps for artificial oblique -->
<edit name="embeddedbitmap" mode="assign">
<bool>false</bool>
</edit>
</match>
<!--
Synthetic emboldening for fonts that do not have bold face available
-->
<match target="font">
<!-- check to see if the font is just regular -->
<test name="weight" compare="less_eq">
<const>medium</const>
</test>
<!-- check to see if the pattern requests bold -->
<test target="pattern" name="weight" compare="more">
<const>medium</const>
</test>
<!--
set the embolden flag
needed for applications using cairo, e.g. gucharmap, gedit, ...
-->
<edit name="embolden" mode="assign">
<bool>true</bool>
</edit>
<!--
set weight to bold
needed for applications using Xft directly, e.g. Firefox, ...
-->
<edit name="weight" mode="assign">
<const>bold</const>
</edit>
</match>
</fontconfig>

89
fonts/fonts.template.conf Normal file
View File

@@ -0,0 +1,89 @@
<?xml version="1.0"?>
<!DOCTYPE fontconfig SYSTEM "fonts.dtd">
<!-- /etc/fonts/fonts.conf file to configure system font access -->
<fontconfig>
<!--
DO NOT EDIT THIS FILE.
IT WILL BE REPLACED WHEN FONTCONFIG IS UPDATED.
LOCAL CHANGES BELONG IN 'local.conf'.
The intent of this standard configuration file is to be adequate for
most environments. If you have a reasonably normal environment and
have found problems with this configuration, they are probably
things that others will also want fixed. Please submit any
problems to the fontconfig bugzilla system located at fontconfig.org
Note that the normal 'make install' procedure for fontconfig is to
replace any existing fonts.conf file with the new version. Place
any local customizations in local.conf which this file references.
Keith Packard
-->
<!-- Font directory list -->
<dir>WINDOWSFONTDIR</dir>
<dir prefix="xdg">fonts</dir>
<!-- the following element will be removed in the future -->
<dir>~/.fonts</dir>
<!--
Accept deprecated 'mono' alias, replacing it with 'monospace'
-->
<match target="pattern">
<test qual="any" name="family">
<string>mono</string>
</test>
<edit name="family" mode="assign" binding="same">
<string>monospace</string>
</edit>
</match>
<!--
Accept alternate 'sans serif' spelling, replacing it with 'sans-serif'
-->
<match target="pattern">
<test qual="any" name="family">
<string>sans serif</string>
</test>
<edit name="family" mode="assign" binding="same">
<string>sans-serif</string>
</edit>
</match>
<!--
Accept deprecated 'sans' alias, replacing it with 'sans-serif'
-->
<match target="pattern">
<test qual="any" name="family">
<string>sans</string>
</test>
<edit name="family" mode="assign" binding="same">
<string>sans-serif</string>
</edit>
</match>
<!--
Load local system customization file
-->
<!--(CONFD)-->
<!-- Font cache directory list -->
<cachedir>LOCAL_APPDATA_FONTCONFIG_CACHE</cachedir>
<cachedir prefix="xdg">fontconfig</cachedir>
<!-- the following element will be removed in the future -->
<cachedir>~/.fontconfig</cachedir>
<config>
<!--
Rescan configuration every 30 seconds when FcFontSetList is called
-->
<rescan>
<int>30</int>
</rescan>
</config>
</fontconfig>

View File

@@ -64,12 +64,14 @@ response request::execute() {
detail::check_init();
std::string str;
curl_easy_setopt(m_curl, CURLOPT_NOSIGNAL, 1L);
curl_easy_setopt(m_curl, CURLOPT_CUSTOMREQUEST, m_method);
curl_easy_setopt(m_curl, CURLOPT_URL, m_url.c_str());
curl_easy_setopt(m_curl, CURLOPT_FOLLOWLOCATION, 1);
curl_easy_setopt(m_curl, CURLOPT_WRITEFUNCTION, detail::curl_write_data_callback);
curl_easy_setopt(m_curl, CURLOPT_WRITEDATA, &str);
curl_easy_setopt(m_curl, CURLOPT_ERRORBUFFER, m_error_buf);
m_error_buf[0] = '\0';
if (m_header_list != nullptr)
curl_easy_setopt(m_curl, CURLOPT_HTTPHEADER, m_header_list);
@@ -81,7 +83,7 @@ response request::execute() {
return response;
}
int response_code = 0;
long response_code = 0;
curl_easy_getinfo(m_curl, CURLINFO_RESPONSE_CODE, &response_code);
auto response = detail::make_response(m_url, response_code);
@@ -95,10 +97,10 @@ void request::prepare() {
}
namespace detail {
size_t curl_write_data_callback(void *ptr, size_t size, size_t nmemb, std::string *outstr) {
auto new_length = size * nmemb;
outstr->append(reinterpret_cast<char *>(ptr), new_length);
return new_length;
size_t curl_write_data_callback(void *ptr, size_t size, size_t nmemb, void *userdata) {
const size_t n = size * nmemb;
static_cast<std::string*>(userdata)->append(static_cast<char*>(ptr), n);
return n;
}
response make_response(const std::string &url, int code) {

View File

@@ -121,7 +121,7 @@ private:
using response_type = response;
namespace detail {
size_t curl_write_data_callback(void *ptr, size_t size, size_t nmemb, std::string *outstr);
size_t curl_write_data_callback(void *ptr, size_t size, size_t nmemb, void *userdata);
response make_response(const std::string &url, int code);
void check_init();

View File

@@ -1,5 +1,6 @@
#include "imgmanager.hpp"
#include "util.hpp"
#include "abaddon.hpp"
ImageManager::ImageManager() {
m_cb_dispatcher.connect(sigc::mem_fun(*this, &ImageManager::RunCallbacks));
@@ -112,7 +113,7 @@ Glib::RefPtr<Gdk::Pixbuf> ImageManager::GetPlaceholder(int size) {
return m_pixs.at(name);
try {
auto buf = Gdk::Pixbuf::create_from_file("res/decamarks.png", size, size);
auto buf = Gdk::Pixbuf::create_from_file(Abaddon::Get().GetResPath() + "/decamarks.png", size, size);
m_pixs[name] = buf;
return buf;
} catch (std::exception &e) {

140
platform.cpp Normal file
View File

@@ -0,0 +1,140 @@
#include "platform.hpp"
#include <string>
#include <fstream>
#include <filesystem>
#include <config.h>
using namespace std::literals::string_literals;
bool IsFolder(std::string_view path) {
std::error_code ec;
const auto status = std::filesystem::status(path, ec);
if (ec) return false;
return status.type() == std::filesystem::file_type::directory;
}
bool IsFile(std::string_view path) {
std::error_code ec;
const auto status = std::filesystem::status(path, ec);
if (ec) return false;
return status.type() == std::filesystem::file_type::regular;
}
#if defined(_WIN32) && defined(_MSC_VER)
#include <Windows.h>
#include <Shlwapi.h>
#include <ShlObj_core.h>
#include <pango/pangocairo.h>
#include <pango/pangofc-fontmap.h>
#pragma comment(lib, "Shlwapi.lib")
bool Platform::SetupFonts() {
using namespace std::string_literals;
char buf[MAX_PATH] { 0 };
GetCurrentDirectoryA(MAX_PATH, buf);
{
// thanks @WorkingRobot for da help :^))
std::ifstream template_stream(buf + "\\fonts\\fonts.template.conf"s);
std::ofstream conf_stream(buf + "\\fonts\\fonts.conf"s);
if (!template_stream.good()) {
printf("can't open fonts/fonts.template.conf\n");
return false;
}
if (!conf_stream.good()) {
printf("can't open write to fonts.conf\n");
return false;
}
std::string line;
while (std::getline(template_stream, line)) {
if (line == "<!--(CONFD)-->")
conf_stream << "<include ignore_missing=\"no\">" << (buf + "\\fonts\\conf.d"s) << "</include>";
else
conf_stream << line;
conf_stream << '\n';
}
}
auto fc = FcConfigCreate();
FcConfigSetCurrent(fc);
FcConfigParseAndLoad(fc, const_cast<FcChar8 *>(reinterpret_cast<const FcChar8 *>((buf + "\\fonts\\fonts.conf"s).c_str())), true);
FcConfigAppFontAddDir(fc, const_cast<FcChar8 *>(reinterpret_cast<const FcChar8 *>((buf + "\\fonts"s).c_str())));
char fonts_path[MAX_PATH];
if (SHGetFolderPathA(NULL, CSIDL_FONTS, NULL, SHGFP_TYPE_CURRENT, fonts_path) == S_OK) {
FcConfigAppFontAddDir(fc, reinterpret_cast<FcChar8 *>(fonts_path));
}
auto map = pango_cairo_font_map_new_for_font_type(CAIRO_FONT_TYPE_FT);
pango_fc_font_map_set_config(reinterpret_cast<PangoFcFontMap *>(map), fc);
pango_cairo_font_map_set_default(reinterpret_cast<PangoCairoFontMap *>(map));
return true;
}
#else
bool Platform::SetupFonts() {
return true;
}
#endif
#if defined(_WIN32)
std::string Platform::FindResourceFolder() {
return ".";
}
std::string Platform::FindConfigFile() {
const auto x = std::getenv("ABADDON_CONFIG");
if (x != nullptr)
return x;
return "./abaddon.ini";
}
#elif defined(__linux__)
std::string Platform::FindResourceFolder() {
static std::string found_path;
static bool found = false;
if (found) return found_path;
const static std::string home_path = std::getenv("HOME") + "/.local/share/abaddon"s;
for (const auto &path : { "."s, home_path, std::string(ABADDON_DEFAULT_RESOURCE_DIR) }) {
if (IsFolder(path + "/res") && IsFolder(path + "/css")) {
found_path = path;
found = true;
return found_path;
}
}
puts("cant find a resources folder, will try to load from cwd");
found_path = ".";
found = true;
return found_path;
}
std::string Platform::FindConfigFile() {
const auto x = std::getenv("ABADDON_CONFIG");
if (x != nullptr)
return x;
const auto home_path = std::string(std::getenv("HOME")) + "/.config/abaddon/abaddon.ini";
for (const auto path : { "./abaddon.ini"s, home_path }) {
if (IsFile(path)) return path;
}
puts("can't find configuration file!");
return "./abaddon.ini";
}
#else
std::string Platform::FindResourceFolder() {
puts("unknown OS, trying to load resources from cwd");
return ".";
}
std::string Platform::FindConfigFile() {
const auto x = std::getenv("ABADDON_CONFIG");
if (x != nullptr)
return x;
puts("unknown OS, trying to load config from cwd");
return "./abaddon.ini";
}
#endif

8
platform.hpp Normal file
View File

@@ -0,0 +1,8 @@
#pragma once
#include <string>
namespace Platform {
bool SetupFonts();
std::string FindResourceFolder();
std::string FindConfigFile();
}

BIN
res/certifiedmoderator.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

View File

@@ -54,12 +54,24 @@ bool SettingsManager::GetShowMemberListDiscriminators() const {
return GetSettingBool("gui", "member_list_discriminator", true);
}
bool SettingsManager::GetShowEmojis() const {
return GetSettingBool("gui", "emojis", true);
bool SettingsManager::GetShowStockEmojis() const {
return GetSettingBool("gui", "stock_emojis", true);
}
bool SettingsManager::GetShowCustomEmojis() const {
return GetSettingBool("gui", "custom_emojis", true);
}
std::string SettingsManager::GetLinkColor() const {
return GetSettingString("misc", "linkcolor", "rgba(40, 200, 180, 255)");
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 {
@@ -71,7 +83,7 @@ bool SettingsManager::GetPrefetch() const {
}
std::string SettingsManager::GetMainCSS() const {
return GetSettingString("gui", "css", "./css/main.css");
return GetSettingString("gui", "css", "main.css");
}
bool SettingsManager::GetShowAnimations() const {
@@ -81,3 +93,15 @@ bool SettingsManager::GetShowAnimations() const {
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);
}

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