forked from OpenGamers/abaddon
Compare commits
108 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cd900cdfee | ||
|
|
1767575728 | ||
|
|
0a34c04b44 | ||
|
|
7e85168576 | ||
|
|
3027e00905 | ||
|
|
2ecbacc924 | ||
|
|
84eb56d6b1 | ||
|
|
f3e5dcbe65 | ||
|
|
a78fdd386f | ||
|
|
348c1cb965 | ||
|
|
32fc7def7c | ||
|
|
14602a7384 | ||
|
|
c683ef9ad9 | ||
|
|
039cc7458d | ||
|
|
d99f16f82d | ||
|
|
243e48e609 | ||
|
|
fac9f1ba58 | ||
|
|
6a5ecb4d95 | ||
|
|
dc28eae95a | ||
|
|
baf96da80c | ||
|
|
31ca6d9fd2 | ||
|
|
04befeb180 | ||
|
|
a4c8a2290d | ||
|
|
96ec5bb665 | ||
|
|
f60cea2216 | ||
|
|
02741f2c1b | ||
|
|
955b9239b9 | ||
|
|
53ac853367 | ||
|
|
1c38671356 | ||
|
|
91527fbd0d | ||
|
|
537d4163c2 | ||
|
|
c0e4a3a988 | ||
|
|
860049fad5 | ||
|
|
344f269414 | ||
|
|
3487353fc7 | ||
|
|
86fc8f4186 | ||
|
|
acb80da387 | ||
|
|
d99d8443ee | ||
|
|
319f9c392c | ||
|
|
a61a630ee6 | ||
|
|
e8260c164f | ||
|
|
4e4986f670 | ||
|
|
3610a2508b | ||
|
|
8396d07d9e | ||
|
|
59acd0f82f | ||
|
|
544ae6f915 | ||
|
|
111399cf4a | ||
|
|
fba5cee519 | ||
|
|
f95d79129e | ||
|
|
02ce353c6d | ||
|
|
241d9a2140 | ||
|
|
849ebf17f1 | ||
|
|
41776fbd02 | ||
|
|
5c7631e713 | ||
|
|
41e2478a6f | ||
|
|
a9d35dcccd | ||
|
|
e87766f106 | ||
|
|
a038f47a25 | ||
|
|
d841a2c862 | ||
|
|
4ee7025ab0 | ||
|
|
d0fa308f6e | ||
|
|
4456c8771d | ||
|
|
caa551a469 | ||
|
|
ccf5afbba9 | ||
|
|
2474ffc2ba | ||
|
|
5cf4d8e160 | ||
|
|
49ff9a249e | ||
|
|
abc448eca0 | ||
|
|
d7177cac97 | ||
|
|
c6182e8923 | ||
|
|
270730d9b3 | ||
|
|
4ec5c1dfcc | ||
|
|
da27c67e6b | ||
|
|
de3b53c676 | ||
|
|
3ac993bae4 | ||
|
|
756de57919 | ||
|
|
d9cf989813 | ||
|
|
ffc69576f2 | ||
|
|
2bed7f161b | ||
|
|
607607ef0a | ||
|
|
b2ba7709df | ||
|
|
8b488a5ca9 | ||
|
|
1d8ef79da6 | ||
|
|
bbf32730cd | ||
|
|
f58ca39e8c | ||
|
|
3b5f4ded31 | ||
|
|
f6fdfeb95f | ||
|
|
2c25319fb8 | ||
|
|
7daa0a250c | ||
|
|
121c2585c8 | ||
|
|
b18b94818a | ||
|
|
63db16a711 | ||
|
|
c30ab91738 | ||
|
|
e8f16292d1 | ||
|
|
db28abaa44 | ||
|
|
bfb2490938 | ||
|
|
b4ab88f708 | ||
|
|
2dab595476 | ||
|
|
a98967fccc | ||
|
|
32e4540464 | ||
|
|
02dc28e89c | ||
|
|
47545d9d32 | ||
|
|
5670dfc1d5 | ||
|
|
34f8af599d | ||
|
|
d36fe4d0e9 | ||
|
|
44317e2d34 | ||
|
|
5b806a2589 | ||
|
|
5a13c7fef7 |
60
.github/workflows/ci.yml
vendored
60
.github/workflows/ci.yml
vendored
@@ -9,6 +9,10 @@ jobs:
|
||||
strategy:
|
||||
matrix:
|
||||
buildtype: [Debug, RelWithDebInfo, MinSizeRel]
|
||||
mindeps: [false]
|
||||
include:
|
||||
- buildtype: RelWithDebInfo
|
||||
mindeps: true
|
||||
defaults:
|
||||
run:
|
||||
shell: msys2 {0}
|
||||
@@ -17,12 +21,12 @@ jobs:
|
||||
with:
|
||||
submodules: true
|
||||
|
||||
- name: Setup MSYS2
|
||||
uses: msys2/setup-msys2@v2
|
||||
- name: Setup MSYS2 (1)
|
||||
uses: haya14busa/action-cond@v1
|
||||
id: setupmsys
|
||||
with:
|
||||
msystem: mingw64
|
||||
update: true
|
||||
install: >-
|
||||
cond: ${{ matrix.mindeps == true }}
|
||||
if_true: >-
|
||||
git
|
||||
make
|
||||
mingw-w64-x86_64-toolchain
|
||||
@@ -33,6 +37,25 @@ jobs:
|
||||
mingw-w64-x86_64-curl
|
||||
mingw-w64-x86_64-zlib
|
||||
mingw-w64-x86_64-gtkmm3
|
||||
if_false: >-
|
||||
git
|
||||
make
|
||||
mingw-w64-x86_64-toolchain
|
||||
mingw-w64-x86_64-cmake
|
||||
mingw-w64-x86_64-ninja
|
||||
mingw-w64-x86_64-sqlite3
|
||||
mingw-w64-x86_64-nlohmann-json
|
||||
mingw-w64-x86_64-curl
|
||||
mingw-w64-x86_64-zlib
|
||||
mingw-w64-x86_64-gtkmm3
|
||||
mingw-w64-x86_64-libhandy
|
||||
|
||||
- name: Setup MSYS2 (2)
|
||||
uses: msys2/setup-msys2@v2
|
||||
with:
|
||||
msystem: mingw64
|
||||
update: true
|
||||
install: ${{ steps.setupmsys.outputs.value }}
|
||||
|
||||
- name: Build
|
||||
run: |
|
||||
@@ -49,12 +72,32 @@ jobs:
|
||||
cp -r /mingw64/lib/gdk-pixbuf-2.0 build/artifactdir/lib
|
||||
cp -r res/css res/res res/fonts build/artifactdir/bin
|
||||
cp /mingw64/share/glib-2.0/schemas/gschemas.compiled build/artifactdir/share/glib-2.0/schemas
|
||||
cat "ci/msys-deps.txt" | sed 's/\r$//' | xargs -I % cp /mingw64% build/artifactdir/bin
|
||||
cat "ci/msys-deps.txt" | sed 's/\r$//' | xargs -I % cp /mingw64% build/artifactdir/bin || :
|
||||
cp /usr/bin/msys-ffi-8.dll build/artifactdir/bin/libffi-8.dll
|
||||
mkdir -p build/artifactdir/share/icons/Adwaita
|
||||
cd build/artifactdir/share/icons/Adwaita
|
||||
mkdir -p 16x16/actions 24x24/actions 32x32/actions 48x48/actions 64x64/actions 96x96/actions scalable/actions
|
||||
cd ../../../../../
|
||||
cat "ci/used-icons.txt" | sed 's/\r$//' | xargs -I % cp ci/gtk-for-windows/gtk-nsis-pack/share/icons/Adwaita/16x16/actions/%.symbolic.png build/artifactdir/share/icons/Adwaita/16x16/actions || :
|
||||
cat "ci/used-icons.txt" | sed 's/\r$//' | xargs -I % cp ci/gtk-for-windows/gtk-nsis-pack/share/icons/Adwaita/24x24/actions/%.symbolic.png build/artifactdir/share/icons/Adwaita/24x24/actions || :
|
||||
cat "ci/used-icons.txt" | sed 's/\r$//' | xargs -I % cp ci/gtk-for-windows/gtk-nsis-pack/share/icons/Adwaita/32x32/actions/%.symbolic.png build/artifactdir/share/icons/Adwaita/32x32/actions || :
|
||||
cat "ci/used-icons.txt" | sed 's/\r$//' | xargs -I % cp ci/gtk-for-windows/gtk-nsis-pack/share/icons/Adwaita/48x48/actions/%.symbolic.png build/artifactdir/share/icons/Adwaita/48x48/actions || :
|
||||
cat "ci/used-icons.txt" | sed 's/\r$//' | xargs -I % cp ci/gtk-for-windows/gtk-nsis-pack/share/icons/Adwaita/64x64/actions/%.symbolic.png build/artifactdir/share/icons/Adwaita/64x64/actions || :
|
||||
cat "ci/used-icons.txt" | sed 's/\r$//' | xargs -I % cp ci/gtk-for-windows/gtk-nsis-pack/share/icons/Adwaita/96x96/actions/%.symbolic.png build/artifactdir/share/icons/Adwaita/96x96/actions || :
|
||||
cat "ci/used-icons.txt" | sed 's/\r$//' | xargs -I % cp ci/gtk-for-windows/gtk-nsis-pack/share/icons/Adwaita/scalable/actions/%.svg build/artifactdir/share/icons/Adwaita/scalable/actions || :
|
||||
|
||||
- name: Upload build
|
||||
- name: Upload build (1)
|
||||
uses: haya14busa/action-cond@v1
|
||||
id: buildname
|
||||
with:
|
||||
cond: ${{ matrix.mindeps == true }}
|
||||
if_true: "${{ matrix.buildtype }}-mindeps"
|
||||
if_false: "${{ matrix.buildtype }}"
|
||||
|
||||
- name: Upload build (2)
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: build-windows-msys2-${{ matrix.buildtype }}
|
||||
name: build-windows-msys2-${{ steps.buildname.outputs.value }}
|
||||
path: build/artifactdir
|
||||
|
||||
mac:
|
||||
@@ -75,6 +118,7 @@ jobs:
|
||||
run: |
|
||||
brew install gtkmm3
|
||||
brew install nlohmann-json
|
||||
brew install jpeg
|
||||
|
||||
- name: Build
|
||||
uses: lukka/run-cmake@v3
|
||||
|
||||
6
.gitignore
vendored
6
.gitignore
vendored
@@ -356,3 +356,9 @@ build/
|
||||
out/
|
||||
|
||||
fonts/fonts.conf
|
||||
|
||||
# To make sure no zipped resources are added to the repo
|
||||
*.7z
|
||||
*.zip
|
||||
*.tar.*
|
||||
*.rar
|
||||
|
||||
100
CMakeLists.txt
100
CMakeLists.txt
@@ -7,6 +7,8 @@ set(ABADDON_RESOURCE_DIR "/usr/share/abaddon" CACHE PATH "Fallback directory for
|
||||
set(CMAKE_CXX_STANDARD 17)
|
||||
set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${CMAKE_SOURCE_DIR}/cmake/")
|
||||
|
||||
option(USE_LIBHANDY "Enable features that require libhandy (default)" ON)
|
||||
|
||||
find_package(nlohmann_json REQUIRED)
|
||||
find_package(CURL)
|
||||
find_package(ZLIB REQUIRED)
|
||||
@@ -17,30 +19,33 @@ set(USE_TLS TRUE)
|
||||
set(USE_OPEN_SSL TRUE)
|
||||
find_package(IXWebSocket QUIET)
|
||||
if (NOT IXWebSocket_FOUND)
|
||||
message("ixwebsocket was not found and will be included as a submodule")
|
||||
add_subdirectory(subprojects/ixwebsocket)
|
||||
include_directories(IXWEBSOCKET_INCLUDE_DIRS)
|
||||
endif()
|
||||
message("ixwebsocket was not found and will be included as a submodule")
|
||||
add_subdirectory(subprojects/ixwebsocket)
|
||||
include_directories(IXWEBSOCKET_INCLUDE_DIRS)
|
||||
endif ()
|
||||
|
||||
if(MINGW OR WIN32)
|
||||
link_libraries(ws2_32)
|
||||
endif()
|
||||
if (MINGW OR WIN32)
|
||||
link_libraries(ws2_32)
|
||||
endif ()
|
||||
|
||||
if(WIN32)
|
||||
add_compile_definitions(_CRT_SECURE_NO_WARNINGS)
|
||||
add_compile_definitions(NOMINMAX)
|
||||
if (WIN32)
|
||||
add_compile_definitions(_CRT_SECURE_NO_WARNINGS)
|
||||
add_compile_definitions(NOMINMAX)
|
||||
endif ()
|
||||
|
||||
find_package(Fontconfig REQUIRED)
|
||||
link_libraries(${Fontconfig_LIBRARIES})
|
||||
endif()
|
||||
include(TestBigEndian)
|
||||
test_big_endian(IS_BIG_ENDIAN)
|
||||
if (IS_BIG_ENDIAN)
|
||||
add_compile_definitions(ABADDON_IS_BIG_ENDIAN)
|
||||
endif ()
|
||||
|
||||
configure_file(${PROJECT_SOURCE_DIR}/src/config.h.in ${PROJECT_BINARY_DIR}/config.h)
|
||||
|
||||
file(GLOB_RECURSE ABADDON_SOURCES
|
||||
"src/*.h"
|
||||
"src/*.hpp"
|
||||
"src/*.cpp"
|
||||
)
|
||||
"src/*.h"
|
||||
"src/*.hpp"
|
||||
"src/*.cpp"
|
||||
)
|
||||
|
||||
add_executable(abaddon ${ABADDON_SOURCES})
|
||||
target_include_directories(abaddon PUBLIC ${PROJECT_SOURCE_DIR}/src)
|
||||
@@ -51,36 +56,53 @@ target_include_directories(abaddon PUBLIC ${SQLite3_INCLUDE_DIRS})
|
||||
target_include_directories(abaddon PUBLIC ${NLOHMANN_JSON_INCLUDE_DIRS})
|
||||
|
||||
if ((CMAKE_CXX_COMPILER_ID STREQUAL "GNU") OR
|
||||
(CMAKE_CXX_COMPILER_ID STREQUAL "Clang" AND
|
||||
((CMAKE_SYSTEM_NAME STREQUAL "Linux") OR (CMAKE_CXX_COMPILER_VERSION LESS 9))))
|
||||
target_link_libraries(abaddon stdc++fs)
|
||||
endif()
|
||||
(CMAKE_CXX_COMPILER_ID STREQUAL "Clang" AND
|
||||
((CMAKE_SYSTEM_NAME STREQUAL "Linux") OR (CMAKE_CXX_COMPILER_VERSION LESS 9))))
|
||||
target_link_libraries(abaddon stdc++fs)
|
||||
endif ()
|
||||
|
||||
if (IXWebSocket_LIBRARIES)
|
||||
target_link_libraries(abaddon ${IXWebSocket_LIBRARIES})
|
||||
find_library(MBEDTLS_X509_LIBRARY mbedx509)
|
||||
find_library(MBEDTLS_TLS_LIBRARY mbedtls)
|
||||
find_library(MBEDTLS_CRYPTO_LIBRARY mbedcrypto)
|
||||
if (MBEDTLS_TLS_LIBRARY)
|
||||
target_link_libraries(abaddon ${MBEDTLS_TLS_LIBRARY})
|
||||
endif()
|
||||
if (MBEDTLS_X509_LIBRARY)
|
||||
target_link_libraries(abaddon ${MBEDTLS_X509_LIBRARY})
|
||||
endif()
|
||||
if (MBEDTLS_CRYPTO_LIBRARY)
|
||||
target_link_libraries(abaddon ${MBEDTLS_CRYPTO_LIBRARY})
|
||||
endif()
|
||||
else()
|
||||
target_link_libraries(abaddon $<BUILD_INTERFACE:ixwebsocket>)
|
||||
endif()
|
||||
target_link_libraries(abaddon ${IXWebSocket_LIBRARIES})
|
||||
find_library(MBEDTLS_X509_LIBRARY mbedx509)
|
||||
find_library(MBEDTLS_TLS_LIBRARY mbedtls)
|
||||
find_library(MBEDTLS_CRYPTO_LIBRARY mbedcrypto)
|
||||
if (MBEDTLS_TLS_LIBRARY)
|
||||
target_link_libraries(abaddon ${MBEDTLS_TLS_LIBRARY})
|
||||
endif ()
|
||||
if (MBEDTLS_X509_LIBRARY)
|
||||
target_link_libraries(abaddon ${MBEDTLS_X509_LIBRARY})
|
||||
endif ()
|
||||
if (MBEDTLS_CRYPTO_LIBRARY)
|
||||
target_link_libraries(abaddon ${MBEDTLS_CRYPTO_LIBRARY})
|
||||
endif ()
|
||||
else ()
|
||||
target_link_libraries(abaddon $<BUILD_INTERFACE:ixwebsocket>)
|
||||
endif ()
|
||||
|
||||
find_package(Threads)
|
||||
if (Threads_FOUND)
|
||||
target_link_libraries(abaddon Threads::Threads)
|
||||
endif()
|
||||
target_link_libraries(abaddon Threads::Threads)
|
||||
endif ()
|
||||
|
||||
find_package(Fontconfig QUIET)
|
||||
if (Fontconfig_FOUND)
|
||||
target_link_libraries(abaddon Fontconfig::Fontconfig)
|
||||
endif ()
|
||||
|
||||
target_link_libraries(abaddon ${SQLite3_LIBRARIES})
|
||||
target_link_libraries(abaddon ${GTKMM_LIBRARIES})
|
||||
target_link_libraries(abaddon ${CURL_LIBRARIES})
|
||||
target_link_libraries(abaddon ${ZLIB_LIBRARY})
|
||||
target_link_libraries(abaddon ${NLOHMANN_JSON_LIBRARIES})
|
||||
|
||||
if (USE_LIBHANDY)
|
||||
find_package(libhandy)
|
||||
if (NOT libhandy_FOUND)
|
||||
message("libhandy could not be found. features requiring it have been disabled")
|
||||
set(USE_LIBHANDY OFF)
|
||||
else ()
|
||||
target_include_directories(abaddon PUBLIC ${libhandy_INCLUDE_DIRS})
|
||||
target_link_libraries(abaddon ${libhandy_LIBRARIES})
|
||||
target_compile_definitions(abaddon PRIVATE WITH_LIBHANDY)
|
||||
endif ()
|
||||
endif ()
|
||||
|
||||
35
README.md
35
README.md
@@ -25,13 +25,13 @@ Current features:
|
||||
* Thread support<sup>3</sup>
|
||||
* Animated avatars, server icons, emojis (can be turned off)
|
||||
|
||||
1 - Abaddon tries its best to make Discord think it's a legitimate web client. Some of the things done to do this
|
||||
1 - Abaddon tries its best (though is not perfect) to make Discord think it's a legitimate web client. Some of the things done to do this
|
||||
include: using a browser user agent, sending the same IDENTIFY message that the official web client does, using API v9
|
||||
endpoints in all cases, and not using endpoints the web client does not normally use. There are still a few smaller
|
||||
inconsistencies, however. For example the web client sends lots of telemetry via the `/science` endpoint (uBlock origin
|
||||
stops this) as well as in the headers of all requests. **In any case,** you should use an official client for joining
|
||||
servers, sending new DMs, or managing your friends list if you are afraid of being caught in Discord's spam filters
|
||||
(unlikely).
|
||||
stops this) as well as in the headers of all requests.<br>
|
||||
|
||||
**See [here](#the-spam-filter)** for things you might want to avoid if you are worried about being caught in the spam filter.
|
||||
|
||||
2 - Unicode emojis are substituted manually as opposed to rendered by GTK on non-Windows platforms. This can be changed
|
||||
with the `stock_emojis` setting as shown at the bottom of this README. A CBDT-based font using Twemoji is provided to
|
||||
@@ -52,6 +52,7 @@ the result of fundamental issues with Discord's thread implementation.
|
||||
* mingw-w64-x86_64-curl
|
||||
* mingw-w64-x86_64-zlib
|
||||
* mingw-w64-x86_64-gtkmm3
|
||||
* mingw-w64-x86_64-libhandy
|
||||
2. `git clone --recurse-submodules="subprojects" https://github.com/uowuo/abaddon && cd abaddon`
|
||||
3. `mkdir build && cd build`
|
||||
4. `cmake -GNinja -DCMAKE_BUILD_TYPE=RelWithDebInfo ..`
|
||||
@@ -59,18 +60,17 @@ the result of fundamental issues with Discord's thread implementation.
|
||||
|
||||
#### Mac:
|
||||
|
||||
1. `git clone https://github.com/uowuo/abaddon && cd abaddon`
|
||||
2. `brew install gtkmm3 nlohmann-json`
|
||||
3. `git submodule update --init subprojects`
|
||||
4. `mkdir build && cd build`
|
||||
5. `cmake ..`
|
||||
6. `make`
|
||||
1. `git clone https://github.com/uowuo/abaddon --recurse-submodules="subprojects" && cd abaddon`
|
||||
2. `brew install gtkmm3 nlohmann-json libhandy`
|
||||
3. `mkdir build && cd build`
|
||||
4. `cmake ..`
|
||||
5. `make`
|
||||
|
||||
#### Linux:
|
||||
|
||||
1. Install dependencies: `libgtkmm-3.0-dev`, `libcurl4-gnutls-dev`,
|
||||
and [nlohmann-json](https://github.com/nlohmann/json)
|
||||
2. `git clone https://github.com/uowuo/abaddon && cd abaddon`
|
||||
2. `git clone https://github.com/uowuo/abaddon --recurse-submodules="subprojects" && cd abaddon`
|
||||
3. `mkdir build && cd build`
|
||||
4. `cmake ..`
|
||||
5. `make`
|
||||
@@ -81,7 +81,7 @@ Latest release version: https://github.com/uowuo/abaddon/releases/latest
|
||||
|
||||
**CI:**
|
||||
|
||||
- Windows: [here](https://nightly.link/uowuo/abaddon/workflows/ci/master/build-windows-RelWithDebInfo.zip)
|
||||
- Windows: [here](https://nightly.link/uowuo/abaddon/workflows/ci/master/build-windows-msys2-MinSizeRel.zip)
|
||||
- MacOS: [here](https://nightly.link/uowuo/abaddon/workflows/ci/master/build-macos-RelWithDebInfo.zip) unsigned,
|
||||
unpackaged, requires gtkmm3 (e.g. from homebrew)
|
||||
- Linux: [here](https://nightly.link/uowuo/abaddon/workflows/ci/master/build-linux-MinSizeRel.zip) unpackaged (for now),
|
||||
@@ -94,6 +94,16 @@ On Linux, `css` and `res` can also be loaded from `~/.local/share/abaddon` or `/
|
||||
`abaddon.ini` will also be automatically used if located at `~/.config/abaddon/abaddon.ini` and there is
|
||||
no `abaddon.ini` in the working directory
|
||||
|
||||
#### The Spam Filter
|
||||
|
||||
Discord likes disabling accounts/forcing them to reset their passwords if they think the user is a spam bot or potentially had their account compromised. While the official client still often gets users caught in the spam filter, third party clients tend to upset the spam filter more often. If you get caught by it, you can usually [appeal](https://support.discord.com/hc/en-us/requests/new?ticket_form_id=360000029731) it and get it restored. Here are some things you might want to do with the official client instead if you are particularly afraid of evoking the spam filter's wrath:
|
||||
|
||||
* Joining or leaving servers (usually main cause of getting caught)
|
||||
* Frequently disconnecting and reconnecting
|
||||
* Starting new DMs with people
|
||||
* Managing your friends list
|
||||
* Managing your user profile while connected to a third party client
|
||||
|
||||
#### Dependencies:
|
||||
|
||||
* [gtkmm](https://www.gtkmm.org/en/)
|
||||
@@ -102,6 +112,7 @@ no `abaddon.ini` in the working directory
|
||||
* [libcurl](https://curl.se/)
|
||||
* [zlib](https://zlib.net/)
|
||||
* [SQLite3](https://www.sqlite.org/index.html)
|
||||
* [libhandy](https://gnome.pages.gitlab.gnome.org/libhandy/) (optional)
|
||||
|
||||
### TODO:
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
/bin/libdeflate.dll
|
||||
/bin/libepoxy-0.dll
|
||||
/bin/libexpat-1.dll
|
||||
/bin/libffi-7.dll
|
||||
/bin/libffi-8.dll
|
||||
/bin/libfontconfig-1.dll
|
||||
/bin/libfreetype-6.dll
|
||||
/bin/libfribidi-0.dll
|
||||
@@ -31,6 +31,7 @@
|
||||
/bin/libgraphite2.dll
|
||||
/bin/libgtk-3-0.dll
|
||||
/bin/libgtkmm-3.0-1.dll
|
||||
/bin/libhandy-1-0.dll
|
||||
/bin/libharfbuzz-0.dll
|
||||
/bin/libiconv-2.dll
|
||||
/bin/libidn2-0.dll
|
||||
@@ -41,7 +42,7 @@
|
||||
/bin/libpangoft2-1.0-0.dll
|
||||
/bin/libpangomm-1.4-1.dll
|
||||
/bin/libpangowin32-1.0-0.dll
|
||||
/bin/libpcre-1.dll
|
||||
/bin/libpcre2-8-0.dll
|
||||
/bin/libpixman-1-0.dll
|
||||
/bin/libpng16-16.dll
|
||||
/bin/libpsl-5.dll
|
||||
@@ -55,3 +56,4 @@
|
||||
/bin/libwinpthread-1.dll
|
||||
/bin/libzstd.dll
|
||||
/bin/zlib1.dll
|
||||
/../usr/bin/msys-2.0.dll
|
||||
|
||||
1
ci/used-icons.txt
Normal file
1
ci/used-icons.txt
Normal file
@@ -0,0 +1 @@
|
||||
document-send-symbolic
|
||||
37
cmake/Findgdk.cmake
Normal file
37
cmake/Findgdk.cmake
Normal file
@@ -0,0 +1,37 @@
|
||||
find_package(PkgConfig)
|
||||
if (PKG_CONFIG_FOUND)
|
||||
pkg_check_modules(PC_gdk QUIET gdk-3.0)
|
||||
set(gdk_DEFINITIONS ${PC_gdk_CFLAGS_OTHER})
|
||||
endif ()
|
||||
|
||||
set(gdk_INCLUDE_HINTS ${PC_gdk_INCLUDEDIR} ${PC_gdk_INCLUDE_DIRS})
|
||||
set(gdk_LIBRARY_HINTS ${PC_gdk_LIBDIR} ${PC_gdk_LIBRARY_DIRS})
|
||||
|
||||
find_path(gdk_INCLUDE_DIR
|
||||
NAMES gdk/gdk.h
|
||||
HINTS ${gdk_INCLUDE_HINTS}
|
||||
/usr/include
|
||||
/usr/local/include
|
||||
/opt/local/include
|
||||
PATH_SUFFIXES gdk-3.0)
|
||||
|
||||
find_library(gdk_LIBRARY
|
||||
NAMES gdk-3.0
|
||||
gdk-3
|
||||
gdk
|
||||
HINTS ${gdk_LIBRARY_HINTS}
|
||||
/usr/lib
|
||||
/usr/local/lib
|
||||
/opt/local/lib)
|
||||
|
||||
set(gdk_LIBRARIES ${gdk_LIBRARY})
|
||||
set(gdk_INCLUDE_DIRS ${gdk_INCLUDE_DIR})
|
||||
|
||||
include(FindPackageHandleStandardArgs)
|
||||
find_package_handle_standard_args(gdk
|
||||
REQUIRED_VARS
|
||||
gdk_LIBRARY
|
||||
gdk_INCLUDE_DIR
|
||||
VERSION_VAR gdk_VERSION)
|
||||
|
||||
mark_as_advanced(gdk_INCLUDE_DIR gdk_LIBRARY)
|
||||
@@ -1,48 +1,50 @@
|
||||
set(GDKMM_LIBRARY_NAME gdkmm-3.0)
|
||||
set(gdkmm_LIBRARY_NAME gdkmm-3.0)
|
||||
|
||||
find_package(PkgConfig)
|
||||
if (PKG_CONFIG_FOUND)
|
||||
pkg_check_modules(PKGCONFIG_GDKMM QUIET ${GDKMM_LIBRARY_NAME})
|
||||
set(GDKMM_DEFINITIONS ${PKGCONFIG_GDKMM_CFLAGS_OTHER})
|
||||
pkg_check_modules(PKGCONFIG_gdkmm QUIET ${gdkmm_LIBRARY_NAME})
|
||||
set(gdkmm_DEFINITIONS ${PKGCONFIG_gdkmm_CFLAGS_OTHER})
|
||||
endif (PKG_CONFIG_FOUND)
|
||||
|
||||
set(GDKMM_INCLUDE_HINTS ${PKGCONFIG_GDKMM_INCLUDEDIR} ${PKGCONFIG_GDKMM_INCLUDE_DIRS})
|
||||
set(GDKMM_LIBRARY_HINTS ${PKGCONFIG_GDKMM_LIBDIR} ${PKGCONFIG_GDKMM_LIBRARY_DIRS})
|
||||
set(gdkmm_INCLUDE_HINTS ${PKGCONFIG_gdkmm_INCLUDEDIR} ${PKGCONFIG_gdkmm_INCLUDE_DIRS})
|
||||
set(gdkmm_LIBRARY_HINTS ${PKGCONFIG_gdkmm_LIBDIR} ${PKGCONFIG_gdkmm_LIBRARY_DIRS})
|
||||
|
||||
find_path(GDKMM_INCLUDE_DIR
|
||||
find_path(gdkmm_INCLUDE_DIR
|
||||
NAMES gdkmm.h
|
||||
HINTS ${GDKMM_INCLUDE_HINTS}
|
||||
HINTS ${gdkmm_INCLUDE_HINTS}
|
||||
/usr/include
|
||||
/usr/local/include
|
||||
/opt/local/include
|
||||
PATH_SUFFIXES ${GDKMM_LIBRARY_NAME})
|
||||
PATH_SUFFIXES ${gdkmm_LIBRARY_NAME})
|
||||
|
||||
find_path(GDKMM_CONFIG_INCLUDE_DIR
|
||||
find_path(gdkmm_CONFIG_INCLUDE_DIR
|
||||
NAMES gdkmmconfig.h
|
||||
HINTS ${GDKMM_LIBRARY_HINTS}
|
||||
HINTS ${gdkmm_LIBRARY_HINTS}
|
||||
/usr/lib
|
||||
/usr/local/lib
|
||||
/opt/local/lib
|
||||
PATH_SUFFIXES ${GDKMM_LIBRARY_NAME}/include)
|
||||
PATH_SUFFIXES ${gdkmm_LIBRARY_NAME}/include)
|
||||
|
||||
find_library(GDKMM_LIBRARY
|
||||
NAMES ${GDKMM_LIBRARY_NAME}
|
||||
gdkmm
|
||||
HINTS ${GDKMM_LIBRARY_HINTS}
|
||||
find_library(gdkmm_LIBRARY
|
||||
NAMES ${gdkmm_LIBRARY_NAME}
|
||||
gdkmm
|
||||
HINTS ${gdkmm_LIBRARY_HINTS}
|
||||
/usr/lib
|
||||
/usr/local/lib
|
||||
/opt/local/lib
|
||||
PATH_SUFFIXES ${GDKMM_LIBRARY_NAME}
|
||||
${GDKMM_LIBRARY_NAME}/include)
|
||||
PATH_SUFFIXES ${gdkmm_LIBRARY_NAME}
|
||||
${gdkmm_LIBRARY_NAME}/include)
|
||||
|
||||
set(GDKMM_LIBRARIES ${GDKMM_LIBRARY})
|
||||
set(GDKMM_INCLUDE_DIRS ${GDKMM_INCLUDE_DIR};${GDKMM_CONFIG_INCLUDE_DIRS};${GDKMM_CONFIG_INCLUDE_DIR})
|
||||
find_package(gdk)
|
||||
|
||||
set(gdkmm_LIBRARIES ${gdkmm_LIBRARY};${gdk_LIBRARIES})
|
||||
set(gdkmm_INCLUDE_DIRS ${gdkmm_INCLUDE_DIR};${gdkmm_CONFIG_INCLUDE_DIRS};${gdkmm_CONFIG_INCLUDE_DIR};${gdk_INCLUDE_DIRS})
|
||||
|
||||
include(FindPackageHandleStandardArgs)
|
||||
find_package_handle_standard_args(gdkmm
|
||||
REQUIRED_VARS
|
||||
GDKMM_LIBRARY
|
||||
GDKMM_INCLUDE_DIRS
|
||||
VERSION_VAR GDKMM_VERSION)
|
||||
gdkmm_LIBRARY
|
||||
gdkmm_INCLUDE_DIRS
|
||||
VERSION_VAR gdkmm_VERSION)
|
||||
|
||||
mark_as_advanced(GDKMM_INCLUDE_DIR GDKMM_LIBRARY)
|
||||
mark_as_advanced(gdkmm_INCLUDE_DIR gdkmm_LIBRARY)
|
||||
|
||||
@@ -2,56 +2,70 @@ find_package(PkgConfig)
|
||||
pkg_check_modules(PC_GLIB2 QUIET glib-2.0)
|
||||
|
||||
find_path(GLIB_INCLUDE_DIR
|
||||
NAMES glib.h
|
||||
HINTS ${PC_GLIB2_INCLUDEDIR}
|
||||
${PC_GLIB2_INCLUDE_DIRS}
|
||||
$ENV{GLIB2_HOME}/include
|
||||
$ENV{GLIB2_ROOT}/include
|
||||
/usr/local/include
|
||||
/usr/include
|
||||
/glib2/include
|
||||
/glib-2.0/include
|
||||
PATH_SUFFIXES glib2 glib-2.0 glib-2.0/include
|
||||
)
|
||||
NAMES glib.h
|
||||
HINTS ${PC_GLIB2_INCLUDEDIR}
|
||||
${PC_GLIB2_INCLUDE_DIRS}
|
||||
$ENV{GLIB2_HOME}/include
|
||||
$ENV{GLIB2_ROOT}/include
|
||||
/usr/local/include
|
||||
/usr/include
|
||||
/glib2/include
|
||||
/glib-2.0/include
|
||||
PATH_SUFFIXES glib2 glib-2.0 glib-2.0/include
|
||||
)
|
||||
set(GLIB_INCLUDE_DIRS ${GLIB_INCLUDE_DIR})
|
||||
|
||||
find_library(GLIB_LIBRARIES
|
||||
NAMES glib2
|
||||
glib-2.0
|
||||
HINTS ${PC_GLIB2_LIBDIR}
|
||||
${PC_GLIB2_LIBRARY_DIRS}
|
||||
$ENV{GLIB2_HOME}/lib
|
||||
$ENV{GLIB2_ROOT}/lib
|
||||
/usr/local/lib
|
||||
/usr/lib
|
||||
/lib
|
||||
/glib-2.0/lib
|
||||
PATH_SUFFIXES glib2 glib-2.0
|
||||
)
|
||||
NAMES glib2
|
||||
glib-2.0
|
||||
HINTS ${PC_GLIB2_LIBDIR}
|
||||
${PC_GLIB2_LIBRARY_DIRS}
|
||||
$ENV{GLIB2_HOME}/lib
|
||||
$ENV{GLIB2_ROOT}/lib
|
||||
/usr/local/lib
|
||||
/usr/lib
|
||||
/lib
|
||||
/glib-2.0/lib
|
||||
PATH_SUFFIXES glib2 glib-2.0
|
||||
)
|
||||
|
||||
find_library(glib_GOBJECT_LIBRARIES
|
||||
NAMES gobject-2.0
|
||||
HINTS ${PC_GLIB2_LIBDIR}
|
||||
${PC_GLIB2_LIBRARY_DIRS}
|
||||
)
|
||||
|
||||
find_library(glib_GIO_LIBRARIES
|
||||
NAMES gio-2.0
|
||||
HINTS ${PC_GLIB2_LIBDIR}
|
||||
${PC_GLIB2_LIBRARY_DIRS}
|
||||
)
|
||||
|
||||
get_filename_component(_GLIB2_LIB_DIR "${GLIB_LIBRARIES}" PATH)
|
||||
find_path(GLIB_CONFIG_INCLUDE_DIR
|
||||
NAMES glibconfig.h
|
||||
HINTS ${PC_GLIB2_INCLUDEDIR}
|
||||
${PC_GLIB2_INCLUDE_DIRS}
|
||||
$ENV{GLIB2_HOME}/include
|
||||
$ENV{GLIB2_ROOT}/include
|
||||
/usr/local/include
|
||||
/usr/include
|
||||
/glib2/include
|
||||
/glib-2.0/include
|
||||
${_GLIB2_LIB_DIR}
|
||||
${CMAKE_SYSTEM_LIBRARY_PATH}
|
||||
PATH_SUFFIXES glib2 glib-2.0 glib-2.0/include
|
||||
)
|
||||
NAMES glibconfig.h
|
||||
HINTS ${PC_GLIB2_INCLUDEDIR}
|
||||
${PC_GLIB2_INCLUDE_DIRS}
|
||||
$ENV{GLIB2_HOME}/include
|
||||
$ENV{GLIB2_ROOT}/include
|
||||
/usr/local/include
|
||||
/usr/include
|
||||
/glib2/include
|
||||
/glib-2.0/include
|
||||
${_GLIB2_LIB_DIR}
|
||||
${CMAKE_SYSTEM_LIBRARY_PATH}
|
||||
PATH_SUFFIXES glib2 glib-2.0 glib-2.0/include
|
||||
)
|
||||
if (GLIB_CONFIG_INCLUDE_DIR)
|
||||
set(GLIB_INCLUDE_DIRS ${GLIB_INCLUDE_DIRS} ${GLIB_CONFIG_INCLUDE_DIR})
|
||||
endif()
|
||||
endif ()
|
||||
|
||||
set(GLIB_LIBRARIES ${GLIB_LIBRARIES} ${glib_GOBJECT_LIBRARIES} ${glib_GIO_LIBRARIES})
|
||||
|
||||
include(FindPackageHandleStandardArgs)
|
||||
find_package_handle_standard_args(glib
|
||||
REQUIRED_VARS
|
||||
GLIB_LIBRARIES
|
||||
GLIB_INCLUDE_DIRS
|
||||
VERSION_VAR GLIB_VERSION)
|
||||
mark_as_advanced(GLIB_INCLUDE_DIR GLIB_CONFIG_INCLUDE_DIR)
|
||||
REQUIRED_VARS
|
||||
GLIB_LIBRARIES
|
||||
GLIB_INCLUDE_DIRS
|
||||
VERSION_VAR GLIB_VERSION)
|
||||
mark_as_advanced(GLIB_INCLUDE_DIR GLIB_CONFIG_INCLUDE_DIR glib_GOBJECT_LIBRARIES)
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
set(GTKMM_LIBRARY_NAME gtkmm-3.0)
|
||||
set(GDKMM_LIBRARY_NAME gdkmm-3.0)
|
||||
set(GTKMM_LIBRARY_NAME gtkmm-3.0)
|
||||
set(GDKMM_LIBRARY_NAME gdkmm-3.0)
|
||||
|
||||
find_package(PkgConfig)
|
||||
if(PKG_CONFIG_FOUND)
|
||||
if (PKG_CONFIG_FOUND)
|
||||
pkg_check_modules(PC_GTKMM QUIET ${GTKMM_LIBRARY_NAME})
|
||||
pkg_check_modules(PC_GDKMM QUIET ${GDKMM_LIBRARY_NAME})
|
||||
pkg_check_modules(PC_PANGOMM QUIET ${PANGOMM_LIBRARY_NAME})
|
||||
set(GTKMM_DEFINITIONS ${PC_GTKMM_CFLAGS_OTHER})
|
||||
endif()
|
||||
set(GTKMM_DEFINITIONS ${PC_GTKMM_CFLAGS_OTHER})
|
||||
endif ()
|
||||
|
||||
find_package(gtk)
|
||||
find_package(glibmm)
|
||||
@@ -46,14 +46,14 @@ find_path(GDKMM_CONFIG_INCLUDE_DIR
|
||||
HINTS ${GDKMM_INCLUDE_HINTS}
|
||||
PATH_SUFFIXES ${GDKMM_LIBRARY_NAME}/include)
|
||||
|
||||
set(GTKMM_LIBRARIES ${GTKMM_LIB};${GDKMM_LIBRARY};${GTK_LIBRARIES};${GLIBMM_LIBRARIES};${PANGOMM_LIBRARIES};${CAIROMM_LIBRARIES};${ATKMM_LIBRARIES};${SIGC++_LIBRARIES})
|
||||
set(GTKMM_INCLUDE_DIRS ${GTKMM_INCLUDE_DIR};${GTKMM_CONFIG_INCLUDE_DIR};${GDKMM_INCLUDE_DIR};${GDKMM_CONFIG_INCLUDE_DIR};${GTK_INCLUDE_DIRS};${GLIBMM_INCLUDE_DIRS};${PANGOMM_INCLUDE_DIRS};${CAIROMM_INCLUDE_DIRS};${ATKMM_INCLUDE_DIRS};${SIGC++_INCLUDE_DIRS})
|
||||
set(GTKMM_LIBRARIES ${GTKMM_LIB};${gdkmm_LIBRARIES};${GTK_LIBRARIES};${GLIBMM_LIBRARIES};${PANGOMM_LIBRARIES};${CAIROMM_LIBRARIES};${ATKMM_LIBRARIES};${SIGC++_LIBRARIES})
|
||||
set(GTKMM_INCLUDE_DIRS ${GTKMM_INCLUDE_DIR};${GTKMM_CONFIG_INCLUDE_DIR};${gdkmm_INCLUDE_DIRS};${gdkmm_CONFIG_INCLUDE_DIR};${GTK_INCLUDE_DIRS};${GLIBMM_INCLUDE_DIRS};${PANGOMM_INCLUDE_DIRS};${CAIROMM_INCLUDE_DIRS};${ATKMM_INCLUDE_DIRS};${SIGC++_INCLUDE_DIRS})
|
||||
|
||||
include(FindPackageHandleStandardArgs)
|
||||
find_package_handle_standard_args(gtkmm
|
||||
REQUIRED_VARS
|
||||
GTKMM_LIB
|
||||
GTKMM_INCLUDE_DIRS
|
||||
GTKMM_LIB
|
||||
GTKMM_INCLUDE_DIRS
|
||||
VERSION_VAR GTKMM_VERSION)
|
||||
|
||||
mark_as_advanced(GTKMM_INCLUDE_DIR GTKMM_LIBRARY)
|
||||
|
||||
39
cmake/Findlibhandy.cmake
Normal file
39
cmake/Findlibhandy.cmake
Normal file
@@ -0,0 +1,39 @@
|
||||
set(libhandy_LIBRARY_NAME libhandy-1)
|
||||
|
||||
find_package(PkgConfig)
|
||||
if (PKG_CONFIG_FOUND)
|
||||
pkg_check_modules(PC_libhandy QUIET ${libhandy_LIBRARY_NAME})
|
||||
set(libhandy_DEfINITIONS ${PC_libhandy_CFLAGS_OTHER})
|
||||
endif (PKG_CONFIG_FOUND)
|
||||
|
||||
set(libhandy_INCLUDE_HINTS ${PC_libhandy_INCLUDEDIR} ${PC_libhandy_INCLUDE_DIRS})
|
||||
set(libhandy_LIBRARY_HINTS ${PC_libhandy_LIBDIR} ${PC_libhandy_LIBRARY_DIRS})
|
||||
|
||||
find_path(libhandy_INCLUDE_DIR
|
||||
NAMES handy.h
|
||||
HINTS ${libhandy_INCLUDE_HINTS}
|
||||
/usr/include
|
||||
/usr/local/include
|
||||
/opt/local/include
|
||||
PATH_SUFFIXES ${libhandy_LIBRARY_NAME})
|
||||
|
||||
find_library(libhandy_LIBRARY
|
||||
NAMES ${libhandy_LIBRARY_NAME} handy-1
|
||||
HINTS ${libhandy_LIBRARY_HINTS}
|
||||
/usr/lib
|
||||
/usr/local/lib
|
||||
/opt/local/lib
|
||||
PATH_SUFFIXES ${libhandy_LIBRARY_NAME}
|
||||
${libhandy_LIBRARY_NAME}/include)
|
||||
|
||||
set(libhandy_LIBRARIES ${libhandy_LIBRARY})
|
||||
set(libhandy_INCLUDE_DIRS ${libhandy_INCLUDE_DIR};${libhandy_CONFIG_INCLUDE_DIRS})
|
||||
|
||||
include(FindPackageHandleStandardArgs)
|
||||
find_package_handle_standard_args(libhandy
|
||||
REQUIRED_VARS
|
||||
libhandy_LIBRARY
|
||||
libhandy_INCLUDE_DIR
|
||||
VERSION_VAR libhandy_VERSION)
|
||||
|
||||
mark_as_advanced(libhandy_INCLUDE_DIR libhandy_LIBRARY)
|
||||
@@ -101,17 +101,38 @@
|
||||
.message-input, .message-input textview, .message-input textview text {
|
||||
background-color: #242424;
|
||||
color: #adadad;
|
||||
border-radius: 15px;
|
||||
border-radius: 3px;
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
|
||||
.message-input {
|
||||
border: 1px solid #444444;
|
||||
margin-right: 15px;
|
||||
}
|
||||
|
||||
.message-input.replying {
|
||||
border: 1px solid #026FB9;
|
||||
}
|
||||
|
||||
.message-input {
|
||||
.message-input.bad-input {
|
||||
border: 1px solid #dd3300;
|
||||
}
|
||||
|
||||
.message-input-browse-icon {
|
||||
color: #b9bbbe;
|
||||
margin-left: 5px;
|
||||
margin-top: 11px;
|
||||
}
|
||||
|
||||
/* i dont think theres a way to circumvent having to do this to adjust around the browse icon */
|
||||
.message-input:not(.with-browser-icon) {
|
||||
padding: 0px 0px 0px 5px;
|
||||
}
|
||||
|
||||
.message-input.with-browse-icon {
|
||||
padding: 0px 0px 0px 30px;
|
||||
}
|
||||
|
||||
.members {
|
||||
background-color: @background_color;
|
||||
}
|
||||
@@ -282,3 +303,60 @@
|
||||
.friends-list-row-bot {
|
||||
color: #ff0000;
|
||||
}
|
||||
|
||||
.channel-tab-switcher .box {
|
||||
margin: -7px -1px -7px -1px;
|
||||
background: #2a2a2a;
|
||||
border: 1px solid black;
|
||||
}
|
||||
|
||||
.channel-tab-switcher tab:hover {
|
||||
box-shadow: inset 0 -6px #17633e;
|
||||
}
|
||||
|
||||
.channel-tab-switcher tab:checked {
|
||||
box-shadow: inset 0 -6px #2feb90;
|
||||
}
|
||||
|
||||
.channel-tab-switcher tab {
|
||||
background: #1A1A1A;
|
||||
border: 1px solid #808080;
|
||||
min-height: 35px;
|
||||
}
|
||||
|
||||
.channel-tab-switcher tab.needs-attention:not(:checked) {
|
||||
font-weight: bold;
|
||||
animation: 150ms ease-in;
|
||||
/* background-image: radial-gradient(ellipse at bottom, #FF5370, #1A1A1A 30%); */
|
||||
box-shadow: inset 0 -6px red;
|
||||
}
|
||||
|
||||
.channel-tab-switcher tab > button {
|
||||
border: none;
|
||||
padding: 0;
|
||||
margin: 5px;
|
||||
min-width: 16px;
|
||||
min-height: 16px;
|
||||
color: #FF5370;
|
||||
background-color: rgba(0.21, 0.21, 0.21, 0.5);
|
||||
}
|
||||
|
||||
.channel-tab-switcher tab > button:hover {
|
||||
background-color: alpha(#ff0000, 0.5);
|
||||
}
|
||||
|
||||
.message-progress {
|
||||
border: none;
|
||||
margin-bottom: -8px;
|
||||
}
|
||||
|
||||
.message-progress trough {
|
||||
border: none;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.message-progress progress {
|
||||
border: none;
|
||||
background-color: #dd3300;
|
||||
margin-left: 1px;
|
||||
}
|
||||
|
||||
271
src/abaddon.cpp
271
src/abaddon.cpp
@@ -6,16 +6,21 @@
|
||||
#include "discord/discord.hpp"
|
||||
#include "dialogs/token.hpp"
|
||||
#include "dialogs/editmessage.hpp"
|
||||
#include "dialogs/joinguild.hpp"
|
||||
#include "dialogs/confirm.hpp"
|
||||
#include "dialogs/setstatus.hpp"
|
||||
#include "dialogs/friendpicker.hpp"
|
||||
#include "dialogs/verificationgate.hpp"
|
||||
#include "dialogs/textinput.hpp"
|
||||
#include "abaddon.hpp"
|
||||
#include "windows/guildsettingswindow.hpp"
|
||||
#include "windows/profilewindow.hpp"
|
||||
#include "windows/pinnedwindow.hpp"
|
||||
#include "windows/threadswindow.hpp"
|
||||
#include "startup.hpp"
|
||||
|
||||
#ifdef WITH_LIBHANDY
|
||||
#include <handy.h>
|
||||
#endif
|
||||
|
||||
#ifdef _WIN32
|
||||
#pragma comment(lib, "crypt32.lib")
|
||||
@@ -43,6 +48,10 @@ Abaddon::Abaddon()
|
||||
m_discord.signal_thread_update().connect(sigc::mem_fun(*this, &Abaddon::DiscordOnThreadUpdate));
|
||||
m_discord.signal_message_sent().connect(sigc::mem_fun(*this, &Abaddon::DiscordOnMessageSent));
|
||||
m_discord.signal_disconnected().connect(sigc::mem_fun(*this, &Abaddon::DiscordOnDisconnect));
|
||||
m_discord.signal_channel_accessibility_changed().connect([this](Snowflake id, bool accessible) {
|
||||
if (!accessible)
|
||||
m_channels_requested.erase(id);
|
||||
});
|
||||
if (GetSettings().Prefetch)
|
||||
m_discord.signal_message_create().connect([this](const Message &message) {
|
||||
if (message.Author.HasAvatar())
|
||||
@@ -59,9 +68,92 @@ Abaddon &Abaddon::Get() {
|
||||
return instance;
|
||||
}
|
||||
|
||||
#ifdef WITH_LIBHANDY
|
||||
#ifdef _WIN32
|
||||
constexpr static guint BUTTON_BACK = 4;
|
||||
constexpr static guint BUTTON_FORWARD = 5;
|
||||
#else
|
||||
constexpr static guint BUTTON_BACK = 8;
|
||||
constexpr static guint BUTTON_FORWARD = 9;
|
||||
#endif
|
||||
|
||||
static bool HandleButtonEvents(GdkEvent *event, MainWindow *main_window) {
|
||||
if (event->type != GDK_BUTTON_PRESS) return false;
|
||||
|
||||
auto *widget = gtk_get_event_widget(event);
|
||||
if (widget == nullptr) return false;
|
||||
auto *window = gtk_widget_get_toplevel(widget);
|
||||
if (static_cast<void *>(window) != static_cast<void *>(main_window->gobj())) return false; // is this the right way???
|
||||
|
||||
switch (event->button.button) {
|
||||
case BUTTON_BACK:
|
||||
main_window->GoBack();
|
||||
break;
|
||||
case BUTTON_FORWARD:
|
||||
main_window->GoForward();
|
||||
break;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool HandleKeyEvents(GdkEvent *event, MainWindow *main_window) {
|
||||
if (event->type != GDK_KEY_PRESS) return false;
|
||||
|
||||
auto *widget = gtk_get_event_widget(event);
|
||||
if (widget == nullptr) return false;
|
||||
auto *window = gtk_widget_get_toplevel(widget);
|
||||
if (static_cast<void *>(window) != static_cast<void *>(main_window->gobj())) return false;
|
||||
|
||||
const bool ctrl = (event->key.state & GDK_CONTROL_MASK) == GDK_CONTROL_MASK;
|
||||
const bool shft = (event->key.state & GDK_SHIFT_MASK) == GDK_SHIFT_MASK;
|
||||
|
||||
if (ctrl) {
|
||||
switch (event->key.keyval) {
|
||||
case GDK_KEY_Tab:
|
||||
case GDK_KEY_KP_Tab:
|
||||
case GDK_KEY_ISO_Left_Tab:
|
||||
if (shft)
|
||||
main_window->GoToPreviousTab();
|
||||
else
|
||||
main_window->GoToNextTab();
|
||||
return true;
|
||||
case GDK_KEY_1:
|
||||
case GDK_KEY_2:
|
||||
case GDK_KEY_3:
|
||||
case GDK_KEY_4:
|
||||
case GDK_KEY_5:
|
||||
case GDK_KEY_6:
|
||||
case GDK_KEY_7:
|
||||
case GDK_KEY_8:
|
||||
case GDK_KEY_9:
|
||||
main_window->GoToTab(event->key.keyval - GDK_KEY_1);
|
||||
return true;
|
||||
case GDK_KEY_0:
|
||||
main_window->GoToTab(9);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
static void MainEventHandler(GdkEvent *event, void *main_window) {
|
||||
if (HandleButtonEvents(event, static_cast<MainWindow *>(main_window))) return;
|
||||
if (HandleKeyEvents(event, static_cast<MainWindow *>(main_window))) return;
|
||||
gtk_main_do_event(event);
|
||||
}
|
||||
#endif
|
||||
|
||||
int Abaddon::StartGTK() {
|
||||
m_gtk_app = Gtk::Application::create("com.github.uowuo.abaddon");
|
||||
|
||||
#ifdef WITH_LIBHANDY
|
||||
m_gtk_app->signal_activate().connect([] {
|
||||
hdy_init();
|
||||
});
|
||||
#endif
|
||||
|
||||
m_css_provider = Gtk::CssProvider::create();
|
||||
m_css_provider->signal_parsing_error().connect([](const Glib::RefPtr<const Gtk::CssSection> §ion, const Glib::Error &error) {
|
||||
Gtk::MessageDialog dlg("css failed parsing (" + error.what() + ")", false, Gtk::MESSAGE_ERROR, Gtk::BUTTONS_OK, true);
|
||||
@@ -103,6 +195,10 @@ int Abaddon::StartGTK() {
|
||||
m_main_window->set_title(APP_TITLE);
|
||||
m_main_window->set_position(Gtk::WIN_POS_CENTER);
|
||||
|
||||
#ifdef WITH_LIBHANDY
|
||||
gdk_event_handler_set(&MainEventHandler, m_main_window.get(), nullptr);
|
||||
#endif
|
||||
|
||||
if (!m_settings.IsValid()) {
|
||||
Gtk::MessageDialog dlg(*m_main_window, "The settings file could not be opened!", false, Gtk::MESSAGE_ERROR, Gtk::BUTTONS_OK, true);
|
||||
dlg.set_position(Gtk::WIN_POS_CENTER);
|
||||
@@ -132,13 +228,12 @@ int Abaddon::StartGTK() {
|
||||
m_main_window->signal_action_disconnect().connect(sigc::mem_fun(*this, &Abaddon::ActionDisconnect));
|
||||
m_main_window->signal_action_set_token().connect(sigc::mem_fun(*this, &Abaddon::ActionSetToken));
|
||||
m_main_window->signal_action_reload_css().connect(sigc::mem_fun(*this, &Abaddon::ActionReloadCSS));
|
||||
m_main_window->signal_action_join_guild().connect(sigc::mem_fun(*this, &Abaddon::ActionJoinGuildDialog));
|
||||
m_main_window->signal_action_set_status().connect(sigc::mem_fun(*this, &Abaddon::ActionSetStatus));
|
||||
m_main_window->signal_action_add_recipient().connect(sigc::mem_fun(*this, &Abaddon::ActionAddRecipient));
|
||||
m_main_window->signal_action_view_pins().connect(sigc::mem_fun(*this, &Abaddon::ActionViewPins));
|
||||
m_main_window->signal_action_view_threads().connect(sigc::mem_fun(*this, &Abaddon::ActionViewThreads));
|
||||
|
||||
m_main_window->GetChannelList()->signal_action_channel_item_select().connect(sigc::mem_fun(*this, &Abaddon::ActionChannelOpened));
|
||||
m_main_window->GetChannelList()->signal_action_channel_item_select().connect(sigc::bind(sigc::mem_fun(*this, &Abaddon::ActionChannelOpened), true));
|
||||
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));
|
||||
|
||||
@@ -151,10 +246,29 @@ int Abaddon::StartGTK() {
|
||||
m_main_window->GetChatWindow()->signal_action_reaction_remove().connect(sigc::mem_fun(*this, &Abaddon::ActionReactionRemove));
|
||||
|
||||
ActionReloadCSS();
|
||||
if (m_settings.GetSettings().HideToTray) {
|
||||
m_tray = Gtk::StatusIcon::create("discord");
|
||||
m_tray->signal_activate().connect(sigc::mem_fun(*this, &Abaddon::on_tray_click));
|
||||
m_tray->signal_popup_menu().connect(sigc::mem_fun(*this, &Abaddon::on_tray_popup_menu));
|
||||
}
|
||||
m_tray_menu = Gtk::make_managed<Gtk::Menu>();
|
||||
m_tray_exit = Gtk::make_managed<Gtk::MenuItem>("Quit", false);
|
||||
|
||||
m_tray_exit->signal_activate().connect(sigc::mem_fun(*this, &Abaddon::on_tray_menu_click));
|
||||
|
||||
m_tray_menu->append(*m_tray_exit);
|
||||
m_tray_menu->show_all();
|
||||
|
||||
m_main_window->signal_hide().connect(sigc::mem_fun(*this, &Abaddon::on_window_hide));
|
||||
m_gtk_app->signal_shutdown().connect(sigc::mem_fun(*this, &Abaddon::OnShutdown), false);
|
||||
|
||||
m_main_window->UpdateMenus();
|
||||
|
||||
m_gtk_app->hold();
|
||||
m_main_window->show();
|
||||
|
||||
RunFirstTimeDiscordStartup();
|
||||
|
||||
return m_gtk_app->run(*m_main_window);
|
||||
}
|
||||
|
||||
@@ -173,11 +287,13 @@ void Abaddon::LoadFromSettings() {
|
||||
|
||||
void Abaddon::StartDiscord() {
|
||||
m_discord.Start();
|
||||
m_main_window->UpdateMenus();
|
||||
}
|
||||
|
||||
void Abaddon::StopDiscord() {
|
||||
m_discord.Stop();
|
||||
SaveState();
|
||||
if (m_discord.Stop())
|
||||
SaveState();
|
||||
m_main_window->UpdateMenus();
|
||||
}
|
||||
|
||||
bool Abaddon::IsDiscordActive() const {
|
||||
@@ -290,6 +406,7 @@ void Abaddon::ShowUserMenu(const GdkEvent *event, Snowflake id, Snowflake guild_
|
||||
|
||||
for (const auto child : m_user_menu_roles_submenu->get_children())
|
||||
delete child;
|
||||
|
||||
if (guild.has_value() && user.has_value()) {
|
||||
const auto roles = user->GetSortedRoles();
|
||||
m_user_menu_roles->set_visible(!roles.empty());
|
||||
@@ -312,7 +429,7 @@ void Abaddon::ShowUserMenu(const GdkEvent *event, Snowflake id, Snowflake guild_
|
||||
if (me == id) {
|
||||
m_user_menu_ban->set_visible(false);
|
||||
m_user_menu_kick->set_visible(false);
|
||||
m_user_menu_open_dm->set_visible(false);
|
||||
m_user_menu_open_dm->set_sensitive(false);
|
||||
} else {
|
||||
const bool has_kick = m_discord.HasGuildPermission(me, guild_id, Permission::KICK_MEMBERS);
|
||||
const bool has_ban = m_discord.HasGuildPermission(me, guild_id, Permission::BAN_MEMBERS);
|
||||
@@ -320,7 +437,7 @@ void Abaddon::ShowUserMenu(const GdkEvent *event, Snowflake id, Snowflake guild_
|
||||
|
||||
m_user_menu_kick->set_visible(has_kick && can_manage);
|
||||
m_user_menu_ban->set_visible(has_ban && can_manage);
|
||||
m_user_menu_open_dm->set_visible(true);
|
||||
m_user_menu_open_dm->set_sensitive(m_discord.FindDM(id).has_value());
|
||||
}
|
||||
|
||||
m_user_menu_remove_recipient->hide();
|
||||
@@ -334,6 +451,43 @@ void Abaddon::ShowUserMenu(const GdkEvent *event, Snowflake id, Snowflake guild_
|
||||
m_user_menu->popup_at_pointer(event);
|
||||
}
|
||||
|
||||
void Abaddon::RunFirstTimeDiscordStartup() {
|
||||
DiscordStartupDialog dlg(*m_main_window);
|
||||
dlg.set_position(Gtk::WIN_POS_CENTER);
|
||||
|
||||
std::optional<std::string> cookie;
|
||||
std::optional<uint32_t> build_number;
|
||||
|
||||
dlg.signal_response().connect([&](int response) {
|
||||
if (response == Gtk::RESPONSE_OK) {
|
||||
cookie = dlg.GetCookie();
|
||||
build_number = dlg.GetBuildNumber();
|
||||
}
|
||||
});
|
||||
|
||||
dlg.run();
|
||||
|
||||
Glib::signal_idle().connect_once([this, cookie, build_number]() {
|
||||
if (cookie.has_value()) {
|
||||
m_discord.SetCookie(*cookie);
|
||||
} else {
|
||||
ConfirmDialog confirm(*m_main_window);
|
||||
confirm.SetConfirmText("Cookies could not be fetched. This may increase your chances of being flagged by Discord's anti-spam");
|
||||
confirm.SetAcceptOnly(true);
|
||||
confirm.run();
|
||||
}
|
||||
|
||||
if (build_number.has_value()) {
|
||||
m_discord.SetBuildNumber(*build_number);
|
||||
} else {
|
||||
ConfirmDialog confirm(*m_main_window);
|
||||
confirm.SetConfirmText("Build number could not be fetched. This may increase your chances of being flagged by Discord's anti-spam");
|
||||
confirm.SetAcceptOnly(true);
|
||||
confirm.run();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void Abaddon::ShowGuildVerificationGateDialog(Snowflake guild_id) {
|
||||
VerificationGateDialog dlg(*m_main_window, guild_id);
|
||||
if (dlg.run() == Gtk::RESPONSE_OK) {
|
||||
@@ -368,7 +522,7 @@ void Abaddon::SetupUserMenu() {
|
||||
m_user_menu_ban = Gtk::manage(new Gtk::MenuItem("Ban"));
|
||||
m_user_menu_kick = Gtk::manage(new Gtk::MenuItem("Kick"));
|
||||
m_user_menu_copy_id = Gtk::manage(new Gtk::MenuItem("Copy ID"));
|
||||
m_user_menu_open_dm = Gtk::manage(new Gtk::MenuItem("Open DM"));
|
||||
m_user_menu_open_dm = Gtk::manage(new Gtk::MenuItem("Go to DM"));
|
||||
m_user_menu_roles = Gtk::manage(new Gtk::MenuItem("Roles"));
|
||||
m_user_menu_info = Gtk::manage(new Gtk::MenuItem("View Profile"));
|
||||
m_user_menu_remove_recipient = Gtk::manage(new Gtk::MenuItem("Remove From Group"));
|
||||
@@ -410,6 +564,9 @@ void Abaddon::SaveState() {
|
||||
AbaddonApplicationState state;
|
||||
state.ActiveChannel = m_main_window->GetChatActiveChannel();
|
||||
state.Expansion = m_main_window->GetChannelList()->GetExpansionState();
|
||||
#ifdef WITH_LIBHANDY
|
||||
state.Tabs = m_main_window->GetChatWindow()->GetTabsState();
|
||||
#endif
|
||||
|
||||
const auto path = GetStateCachePath();
|
||||
if (!util::IsFolder(path)) {
|
||||
@@ -417,7 +574,8 @@ void Abaddon::SaveState() {
|
||||
std::filesystem::create_directories(path, ec);
|
||||
}
|
||||
|
||||
auto *fp = std::fopen(GetStateCachePath("/state.json").c_str(), "wb");
|
||||
auto file_name = "/" + std::to_string(m_discord.GetUserData().ID) + ".json";
|
||||
auto *fp = std::fopen(GetStateCachePath(file_name).c_str(), "wb");
|
||||
if (fp == nullptr) return;
|
||||
const auto s = nlohmann::json(state).dump(4);
|
||||
std::fwrite(s.c_str(), 1, s.size(), fp);
|
||||
@@ -431,12 +589,16 @@ void Abaddon::LoadState() {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto data = ReadWholeFile(GetStateCachePath("/state.json"));
|
||||
auto file_name = "/" + std::to_string(m_discord.GetUserData().ID) + ".json";
|
||||
const auto data = ReadWholeFile(GetStateCachePath(file_name));
|
||||
if (data.empty()) return;
|
||||
try {
|
||||
AbaddonApplicationState state = nlohmann::json::parse(data.begin(), data.end());
|
||||
m_main_window->GetChannelList()->UseExpansionState(state.Expansion);
|
||||
ActionChannelOpened(state.ActiveChannel);
|
||||
#ifdef WITH_LIBHANDY
|
||||
m_main_window->GetChatWindow()->UseTabsState(state.Tabs);
|
||||
#endif
|
||||
ActionChannelOpened(state.ActiveChannel, false);
|
||||
} catch (const std::exception &e) {
|
||||
printf("failed to load application state: %s\n", e.what());
|
||||
}
|
||||
@@ -471,18 +633,9 @@ void Abaddon::on_user_menu_copy_id() {
|
||||
|
||||
void Abaddon::on_user_menu_open_dm() {
|
||||
const auto existing = m_discord.FindDM(m_shown_user_menu_id);
|
||||
if (existing.has_value())
|
||||
if (existing.has_value()) {
|
||||
ActionChannelOpened(*existing);
|
||||
else
|
||||
m_discord.CreateDM(m_shown_user_menu_id, [this](DiscordError code, Snowflake channel_id) {
|
||||
if (code == DiscordError::NONE) {
|
||||
// give the gateway a little window to send CHANNEL_CREATE
|
||||
auto cb = [this, channel_id] {
|
||||
ActionChannelOpened(channel_id);
|
||||
};
|
||||
Glib::signal_timeout().connect_once(sigc::track_obj(cb, *this), 200);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void Abaddon::on_user_menu_remove_recipient() {
|
||||
@@ -535,24 +688,25 @@ void Abaddon::ActionSetToken() {
|
||||
m_main_window->UpdateComponents();
|
||||
GetSettings().DiscordToken = m_discord_token;
|
||||
}
|
||||
m_main_window->UpdateMenus();
|
||||
}
|
||||
|
||||
void Abaddon::ActionJoinGuildDialog() {
|
||||
JoinGuildDialog dlg(*m_main_window);
|
||||
auto response = dlg.run();
|
||||
if (response == Gtk::RESPONSE_OK) {
|
||||
auto code = dlg.GetCode();
|
||||
m_discord.JoinGuild(code);
|
||||
void Abaddon::ActionChannelOpened(Snowflake id, bool expand_to) {
|
||||
if (!id.IsValid()) {
|
||||
m_discord.SetReferringChannel(Snowflake::Invalid);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void Abaddon::ActionChannelOpened(Snowflake id) {
|
||||
if (!id.IsValid() || id == m_main_window->GetChatActiveChannel()) return;
|
||||
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.has_value()) {
|
||||
m_discord.SetReferringChannel(Snowflake::Invalid);
|
||||
m_main_window->UpdateChatActiveChannel(Snowflake::Invalid, false);
|
||||
m_main_window->UpdateChatWindowContents();
|
||||
return;
|
||||
}
|
||||
|
||||
const bool can_access = channel->IsDM() || m_discord.HasChannelPermission(m_discord.GetUserData().ID, id, Permission::VIEW_CHANNEL);
|
||||
|
||||
@@ -569,7 +723,7 @@ void Abaddon::ActionChannelOpened(Snowflake id) {
|
||||
display = "Empty group";
|
||||
m_main_window->set_title(std::string(APP_TITLE) + " - " + display);
|
||||
}
|
||||
m_main_window->UpdateChatActiveChannel(id);
|
||||
m_main_window->UpdateChatActiveChannel(id, expand_to);
|
||||
if (m_channels_requested.find(id) == m_channels_requested.end()) {
|
||||
// dont fire requests we know will fail
|
||||
if (can_access) {
|
||||
@@ -595,6 +749,9 @@ void Abaddon::ActionChannelOpened(Snowflake id) {
|
||||
ShowGuildVerificationGateDialog(*channel->GuildID);
|
||||
}
|
||||
}
|
||||
|
||||
m_main_window->UpdateMenus();
|
||||
m_discord.SetReferringChannel(id);
|
||||
}
|
||||
|
||||
void Abaddon::ActionChatLoadHistory(Snowflake id) {
|
||||
@@ -629,13 +786,13 @@ void Abaddon::ActionChatLoadHistory(Snowflake id) {
|
||||
});
|
||||
}
|
||||
|
||||
void Abaddon::ActionChatInputSubmit(std::string msg, Snowflake channel, Snowflake referenced_message) {
|
||||
if (msg.substr(0, 7) == "/shrug " || msg == "/shrug")
|
||||
msg = msg.substr(6) + "\xC2\xAF\x5C\x5F\x28\xE3\x83\x84\x29\x5F\x2F\xC2\xAF"; // this is important
|
||||
if (referenced_message.IsValid())
|
||||
m_discord.SendChatMessage(msg, channel, referenced_message);
|
||||
else
|
||||
m_discord.SendChatMessage(msg, channel);
|
||||
void Abaddon::ActionChatInputSubmit(ChatSubmitParams data) {
|
||||
if (data.Message.substr(0, 7) == "/shrug " || data.Message == "/shrug")
|
||||
data.Message = data.Message.substr(6) + "\xC2\xAF\x5C\x5F\x28\xE3\x83\x84\x29\x5F\x2F\xC2\xAF"; // this is important
|
||||
|
||||
if (!m_discord.HasChannelPermission(m_discord.GetUserData().ID, data.ChannelID, Permission::VIEW_CHANNEL)) return;
|
||||
|
||||
m_discord.SendChatMessage(data, NOOP_CALLBACK);
|
||||
}
|
||||
|
||||
void Abaddon::ActionChatEditMessage(Snowflake channel_id, Snowflake id) {
|
||||
@@ -744,6 +901,15 @@ void Abaddon::ActionViewThreads(Snowflake channel_id) {
|
||||
window->show();
|
||||
}
|
||||
|
||||
std::optional<Glib::ustring> Abaddon::ShowTextPrompt(const Glib::ustring &prompt, const Glib::ustring &title, const Glib::ustring &placeholder, Gtk::Window *window) {
|
||||
TextInputDialog dlg(prompt, title, placeholder, window != nullptr ? *window : *m_main_window);
|
||||
const auto code = dlg.run();
|
||||
if (code == Gtk::RESPONSE_OK)
|
||||
return dlg.GetInput();
|
||||
else
|
||||
return {};
|
||||
}
|
||||
|
||||
bool Abaddon::ShowConfirm(const Glib::ustring &prompt, Gtk::Window *window) {
|
||||
ConfirmDialog dlg(window != nullptr ? *window : *m_main_window);
|
||||
dlg.SetConfirmText(prompt);
|
||||
@@ -774,17 +940,36 @@ EmojiResource &Abaddon::GetEmojis() {
|
||||
return m_emojis;
|
||||
}
|
||||
|
||||
void Abaddon::on_tray_click() {
|
||||
m_main_window->set_visible(!m_main_window->is_visible());
|
||||
}
|
||||
void Abaddon::on_tray_menu_click() {
|
||||
m_gtk_app->quit();
|
||||
}
|
||||
void Abaddon::on_tray_popup_menu(int button, int activate_time) {
|
||||
m_tray->popup_menu_at_position(*m_tray_menu, button, activate_time);
|
||||
}
|
||||
void Abaddon::on_window_hide() {
|
||||
if (!m_settings.GetSettings().HideToTray) {
|
||||
m_gtk_app->quit();
|
||||
}
|
||||
}
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
if (std::getenv("ABADDON_NO_FC") == nullptr)
|
||||
Platform::SetupFonts();
|
||||
|
||||
char *systemLocale = std::setlocale(LC_ALL, "");
|
||||
try {
|
||||
std::locale::global(std::locale(systemLocale));
|
||||
if (systemLocale != nullptr) {
|
||||
std::locale::global(std::locale(systemLocale));
|
||||
}
|
||||
} catch (...) {
|
||||
try {
|
||||
std::locale::global(std::locale::classic());
|
||||
std::setlocale(LC_ALL, systemLocale);
|
||||
if (systemLocale != nullptr) {
|
||||
std::setlocale(LC_ALL, systemLocale);
|
||||
}
|
||||
} catch (...) {}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
#pragma once
|
||||
#include <gtkmm.h>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
@@ -35,8 +36,8 @@ public:
|
||||
void ActionDisconnect();
|
||||
void ActionSetToken();
|
||||
void ActionJoinGuildDialog();
|
||||
void ActionChannelOpened(Snowflake id);
|
||||
void ActionChatInputSubmit(std::string msg, Snowflake channel, Snowflake referenced_message);
|
||||
void ActionChannelOpened(Snowflake id, bool expand_to = true);
|
||||
void ActionChatInputSubmit(ChatSubmitParams data);
|
||||
void ActionChatLoadHistory(Snowflake id);
|
||||
void ActionChatEditMessage(Snowflake channel_id, Snowflake id);
|
||||
void ActionInsertMention(Snowflake id);
|
||||
@@ -51,6 +52,7 @@ public:
|
||||
void ActionViewPins(Snowflake channel_id);
|
||||
void ActionViewThreads(Snowflake channel_id);
|
||||
|
||||
std::optional<Glib::ustring> ShowTextPrompt(const Glib::ustring &prompt, const Glib::ustring &title, const Glib::ustring &placeholder = "", Gtk::Window *window = nullptr);
|
||||
bool ShowConfirm(const Glib::ustring &prompt, Gtk::Window *window = nullptr);
|
||||
|
||||
void ActionReloadCSS();
|
||||
@@ -92,6 +94,8 @@ public:
|
||||
static std::string GetStateCachePath(const std::string &path);
|
||||
|
||||
protected:
|
||||
void RunFirstTimeDiscordStartup();
|
||||
|
||||
void ShowGuildVerificationGateDialog(Snowflake guild_id);
|
||||
|
||||
void CheckMessagesForMembers(const ChannelData &chan, const std::vector<Message> &msgs);
|
||||
@@ -113,6 +117,8 @@ protected:
|
||||
Gtk::MenuItem *m_user_menu_roles;
|
||||
Gtk::MenuItem *m_user_menu_remove_recipient;
|
||||
Gtk::Menu *m_user_menu_roles_submenu;
|
||||
Gtk::Menu *m_tray_menu;
|
||||
Gtk::MenuItem *m_tray_exit;
|
||||
|
||||
void on_user_menu_insert_mention();
|
||||
void on_user_menu_ban();
|
||||
@@ -120,6 +126,10 @@ protected:
|
||||
void on_user_menu_copy_id();
|
||||
void on_user_menu_open_dm();
|
||||
void on_user_menu_remove_recipient();
|
||||
void on_tray_click();
|
||||
void on_tray_popup_menu(int button, int activate_time);
|
||||
void on_tray_menu_click();
|
||||
void on_window_hide();
|
||||
|
||||
private:
|
||||
SettingsManager m_settings;
|
||||
@@ -138,5 +148,6 @@ private:
|
||||
Glib::RefPtr<Gtk::Application> m_gtk_app;
|
||||
Glib::RefPtr<Gtk::CssProvider> m_css_provider;
|
||||
Glib::RefPtr<Gtk::CssProvider> m_css_low_provider; // registered with a lower priority to allow better customization
|
||||
std::unique_ptr<MainWindow> m_main_window; // wah wah cant create a gtkstylecontext fuck you
|
||||
Glib::RefPtr<Gtk::StatusIcon> m_tray;
|
||||
std::unique_ptr<MainWindow> m_main_window; // wah wah cant create a gtkstylecontext fuck you
|
||||
};
|
||||
|
||||
@@ -17,6 +17,10 @@ ChannelList::ChannelList()
|
||||
, m_menu_category_copy_id("_Copy ID", true)
|
||||
, m_menu_channel_copy_id("_Copy ID", true)
|
||||
, m_menu_channel_mark_as_read("Mark as _Read", true)
|
||||
#ifdef WITH_LIBHANDY
|
||||
, m_menu_channel_open_tab("Open in New _Tab", true)
|
||||
, m_menu_dm_open_tab("Open in New _Tab", true)
|
||||
#endif
|
||||
, m_menu_dm_copy_id("_Copy ID", true)
|
||||
, m_menu_dm_close("") // changes depending on if group or not
|
||||
, m_menu_thread_copy_id("_Copy ID", true)
|
||||
@@ -143,6 +147,15 @@ ChannelList::ChannelList()
|
||||
else
|
||||
discord.MuteChannel(id, NOOP_CALLBACK);
|
||||
});
|
||||
|
||||
#ifdef WITH_LIBHANDY
|
||||
m_menu_channel_open_tab.signal_activate().connect([this] {
|
||||
const auto id = static_cast<Snowflake>((*m_model->get_iter(m_path_for_menu))[m_columns.m_id]);
|
||||
m_signal_action_open_new_tab.emit(id);
|
||||
});
|
||||
m_menu_channel.append(m_menu_channel_open_tab);
|
||||
#endif
|
||||
|
||||
m_menu_channel.append(m_menu_channel_mark_as_read);
|
||||
m_menu_channel.append(m_menu_channel_toggle_mute);
|
||||
m_menu_channel.append(m_menu_channel_copy_id);
|
||||
@@ -170,6 +183,13 @@ ChannelList::ChannelList()
|
||||
else
|
||||
discord.MuteChannel(id, NOOP_CALLBACK);
|
||||
});
|
||||
#ifdef WITH_LIBHANDY
|
||||
m_menu_dm_open_tab.signal_activate().connect([this] {
|
||||
const auto id = static_cast<Snowflake>((*m_model->get_iter(m_path_for_menu))[m_columns.m_id]);
|
||||
m_signal_action_open_new_tab.emit(id);
|
||||
});
|
||||
m_menu_dm.append(m_menu_dm_open_tab);
|
||||
#endif
|
||||
m_menu_dm.append(m_menu_dm_toggle_mute);
|
||||
m_menu_dm.append(m_menu_dm_close);
|
||||
m_menu_dm.append(m_menu_dm_copy_id);
|
||||
@@ -442,7 +462,7 @@ void ChannelList::OnGuildUnmute(Snowflake id) {
|
||||
|
||||
// create a temporary channel row for non-joined threads
|
||||
// and delete them when the active channel switches off of them if still not joined
|
||||
void ChannelList::SetActiveChannel(Snowflake id) {
|
||||
void ChannelList::SetActiveChannel(Snowflake id, bool expand_to) {
|
||||
// mark channel as read when switching off
|
||||
if (m_active_channel.IsValid())
|
||||
Abaddon::Get().GetDiscordClient().MarkChannelAsRead(m_active_channel, [](...) {});
|
||||
@@ -459,11 +479,12 @@ void ChannelList::SetActiveChannel(Snowflake id) {
|
||||
|
||||
const auto channel_iter = GetIteratorForChannelFromID(id);
|
||||
if (channel_iter) {
|
||||
m_view.expand_to_path(m_model->get_path(channel_iter));
|
||||
if (expand_to) {
|
||||
m_view.expand_to_path(m_model->get_path(channel_iter));
|
||||
}
|
||||
m_view.get_selection()->select(channel_iter);
|
||||
} else {
|
||||
m_view.get_selection()->unselect_all();
|
||||
// SetActiveChannel should probably just take the channel object
|
||||
const auto channel = Abaddon::Get().GetDiscordClient().GetChannel(id);
|
||||
if (!channel.has_value() || !channel->IsThread()) return;
|
||||
auto parent_iter = GetIteratorForChannelFromID(*channel->ParentID);
|
||||
@@ -890,10 +911,15 @@ void ChannelList::OnGuildSubmenuPopup() {
|
||||
const auto iter = m_model->get_iter(m_path_for_menu);
|
||||
if (!iter) return;
|
||||
const auto id = static_cast<Snowflake>((*iter)[m_columns.m_id]);
|
||||
if (Abaddon::Get().GetDiscordClient().IsGuildMuted(id))
|
||||
auto &discord = Abaddon::Get().GetDiscordClient();
|
||||
if (discord.IsGuildMuted(id))
|
||||
m_menu_guild_toggle_mute.set_label("Unmute");
|
||||
else
|
||||
m_menu_guild_toggle_mute.set_label("Mute");
|
||||
|
||||
const auto guild = discord.GetGuild(id);
|
||||
const auto self_id = discord.GetUserData().ID;
|
||||
m_menu_guild_leave.set_sensitive(!(guild.has_value() && guild->OwnerID == self_id));
|
||||
}
|
||||
|
||||
void ChannelList::OnCategorySubmenuPopup() {
|
||||
@@ -910,7 +936,12 @@ void ChannelList::OnChannelSubmenuPopup() {
|
||||
const auto iter = m_model->get_iter(m_path_for_menu);
|
||||
if (!iter) return;
|
||||
const auto id = static_cast<Snowflake>((*iter)[m_columns.m_id]);
|
||||
if (Abaddon::Get().GetDiscordClient().IsChannelMuted(id))
|
||||
auto &discord = Abaddon::Get().GetDiscordClient();
|
||||
#ifdef WITH_LIBHANDY
|
||||
const auto perms = discord.HasChannelPermission(discord.GetUserData().ID, id, Permission::VIEW_CHANNEL);
|
||||
m_menu_channel_open_tab.set_sensitive(perms);
|
||||
#endif
|
||||
if (discord.IsChannelMuted(id))
|
||||
m_menu_channel_toggle_mute.set_label("Unmute");
|
||||
else
|
||||
m_menu_channel_toggle_mute.set_label("Mute");
|
||||
@@ -960,6 +991,12 @@ ChannelList::type_signal_action_guild_settings ChannelList::signal_action_guild_
|
||||
return m_signal_action_guild_settings;
|
||||
}
|
||||
|
||||
#ifdef WITH_LIBHANDY
|
||||
ChannelList::type_signal_action_open_new_tab ChannelList::signal_action_open_new_tab() {
|
||||
return m_signal_action_open_new_tab;
|
||||
}
|
||||
#endif
|
||||
|
||||
ChannelList::ModelColumns::ModelColumns() {
|
||||
add(m_type);
|
||||
add(m_id);
|
||||
|
||||
@@ -19,7 +19,7 @@ public:
|
||||
ChannelList();
|
||||
|
||||
void UpdateListing();
|
||||
void SetActiveChannel(Snowflake id);
|
||||
void SetActiveChannel(Snowflake id, bool expand_to);
|
||||
|
||||
// channel list should be populated when this is called
|
||||
void UseExpansionState(const ExpansionStateRoot &state);
|
||||
@@ -121,11 +121,19 @@ protected:
|
||||
Gtk::MenuItem m_menu_channel_mark_as_read;
|
||||
Gtk::MenuItem m_menu_channel_toggle_mute;
|
||||
|
||||
#ifdef WITH_LIBHANDY
|
||||
Gtk::MenuItem m_menu_channel_open_tab;
|
||||
#endif
|
||||
|
||||
Gtk::Menu m_menu_dm;
|
||||
Gtk::MenuItem m_menu_dm_copy_id;
|
||||
Gtk::MenuItem m_menu_dm_close;
|
||||
Gtk::MenuItem m_menu_dm_toggle_mute;
|
||||
|
||||
#ifdef WITH_LIBHANDY
|
||||
Gtk::MenuItem m_menu_dm_open_tab;
|
||||
#endif
|
||||
|
||||
Gtk::Menu m_menu_thread;
|
||||
Gtk::MenuItem m_menu_thread_copy_id;
|
||||
Gtk::MenuItem m_menu_thread_leave;
|
||||
@@ -149,16 +157,25 @@ protected:
|
||||
std::unordered_map<Snowflake, Gtk::TreeModel::iterator> m_tmp_channel_map;
|
||||
|
||||
public:
|
||||
typedef sigc::signal<void, Snowflake> type_signal_action_channel_item_select;
|
||||
typedef sigc::signal<void, Snowflake> type_signal_action_guild_leave;
|
||||
typedef sigc::signal<void, Snowflake> type_signal_action_guild_settings;
|
||||
using type_signal_action_channel_item_select = sigc::signal<void, Snowflake>;
|
||||
using type_signal_action_guild_leave = sigc::signal<void, Snowflake>;
|
||||
using type_signal_action_guild_settings = sigc::signal<void, Snowflake>;
|
||||
|
||||
#ifdef WITH_LIBHANDY
|
||||
using type_signal_action_open_new_tab = sigc::signal<void, Snowflake>;
|
||||
type_signal_action_open_new_tab signal_action_open_new_tab();
|
||||
#endif
|
||||
|
||||
type_signal_action_channel_item_select signal_action_channel_item_select();
|
||||
type_signal_action_guild_leave signal_action_guild_leave();
|
||||
type_signal_action_guild_settings signal_action_guild_settings();
|
||||
|
||||
protected:
|
||||
private:
|
||||
type_signal_action_channel_item_select m_signal_action_channel_item_select;
|
||||
type_signal_action_guild_leave m_signal_action_guild_leave;
|
||||
type_signal_action_guild_settings m_signal_action_guild_settings;
|
||||
|
||||
#ifdef WITH_LIBHANDY
|
||||
type_signal_action_open_new_tab m_signal_action_open_new_tab;
|
||||
#endif
|
||||
};
|
||||
|
||||
246
src/components/channeltabswitcherhandy.cpp
Normal file
246
src/components/channeltabswitcherhandy.cpp
Normal file
@@ -0,0 +1,246 @@
|
||||
#ifdef WITH_LIBHANDY
|
||||
|
||||
#include "channeltabswitcherhandy.hpp"
|
||||
#include "abaddon.hpp"
|
||||
|
||||
void selected_page_notify_cb(HdyTabView *view, GParamSpec *pspec, ChannelTabSwitcherHandy *switcher) {
|
||||
auto *page = hdy_tab_view_get_selected_page(view);
|
||||
if (auto it = switcher->m_pages_rev.find(page); it != switcher->m_pages_rev.end()) {
|
||||
switcher->m_signal_channel_switched_to.emit(it->second);
|
||||
}
|
||||
}
|
||||
|
||||
gboolean close_page_cb(HdyTabView *view, HdyTabPage *page, ChannelTabSwitcherHandy *switcher) {
|
||||
switcher->ClearPage(page);
|
||||
hdy_tab_view_close_page_finish(view, page, true);
|
||||
return GDK_EVENT_STOP;
|
||||
}
|
||||
|
||||
ChannelTabSwitcherHandy::ChannelTabSwitcherHandy() {
|
||||
m_tab_bar = hdy_tab_bar_new();
|
||||
m_tab_bar_wrapped = Glib::wrap(GTK_WIDGET(m_tab_bar));
|
||||
m_tab_view = hdy_tab_view_new();
|
||||
m_tab_view_wrapped = Glib::wrap(GTK_WIDGET(m_tab_view));
|
||||
|
||||
m_tab_bar_wrapped->get_style_context()->add_class("channel-tab-switcher");
|
||||
|
||||
g_signal_connect(m_tab_view, "notify::selected-page", G_CALLBACK(selected_page_notify_cb), this);
|
||||
g_signal_connect(m_tab_view, "close-page", G_CALLBACK(close_page_cb), this);
|
||||
|
||||
hdy_tab_bar_set_view(m_tab_bar, m_tab_view);
|
||||
add(*m_tab_bar_wrapped);
|
||||
m_tab_bar_wrapped->show();
|
||||
|
||||
auto &discord = Abaddon::Get().GetDiscordClient();
|
||||
discord.signal_message_create().connect([this](const Message &data) {
|
||||
CheckUnread(data.ChannelID);
|
||||
});
|
||||
|
||||
discord.signal_message_ack().connect([this](const MessageAckData &data) {
|
||||
CheckUnread(data.ChannelID);
|
||||
});
|
||||
|
||||
discord.signal_channel_accessibility_changed().connect(sigc::mem_fun(*this, &ChannelTabSwitcherHandy::OnChannelAccessibilityChanged));
|
||||
}
|
||||
|
||||
void ChannelTabSwitcherHandy::AddChannelTab(Snowflake id) {
|
||||
if (m_pages.find(id) != m_pages.end()) return;
|
||||
|
||||
auto &discord = Abaddon::Get().GetDiscordClient();
|
||||
const auto channel = discord.GetChannel(id);
|
||||
if (!channel.has_value()) return;
|
||||
|
||||
auto *dummy = Gtk::make_managed<Gtk::Box>(); // minimal
|
||||
auto *page = hdy_tab_view_append(m_tab_view, GTK_WIDGET(dummy->gobj()));
|
||||
|
||||
hdy_tab_page_set_title(page, channel->GetDisplayName().c_str());
|
||||
hdy_tab_page_set_tooltip(page, nullptr);
|
||||
|
||||
m_pages[id] = page;
|
||||
m_pages_rev[page] = id;
|
||||
|
||||
CheckUnread(id);
|
||||
CheckPageIcon(page, *channel);
|
||||
AppendPageHistory(page, id);
|
||||
}
|
||||
|
||||
void ChannelTabSwitcherHandy::ReplaceActiveTab(Snowflake id) {
|
||||
auto *page = hdy_tab_view_get_selected_page(m_tab_view);
|
||||
if (page == nullptr) {
|
||||
AddChannelTab(id);
|
||||
} else if (auto it = m_pages.find(id); it != m_pages.end()) {
|
||||
hdy_tab_view_set_selected_page(m_tab_view, it->second);
|
||||
} else {
|
||||
auto &discord = Abaddon::Get().GetDiscordClient();
|
||||
const auto channel = discord.GetChannel(id);
|
||||
if (!channel.has_value()) return;
|
||||
|
||||
hdy_tab_page_set_title(page, channel->GetDisplayName().c_str());
|
||||
|
||||
ClearPage(page);
|
||||
m_pages[id] = page;
|
||||
m_pages_rev[page] = id;
|
||||
|
||||
CheckUnread(id);
|
||||
CheckPageIcon(page, *channel);
|
||||
AppendPageHistory(page, id);
|
||||
}
|
||||
}
|
||||
|
||||
TabsState ChannelTabSwitcherHandy::GetTabsState() {
|
||||
TabsState state;
|
||||
|
||||
const gint num_pages = hdy_tab_view_get_n_pages(m_tab_view);
|
||||
for (gint i = 0; i < num_pages; i++) {
|
||||
auto *page = hdy_tab_view_get_nth_page(m_tab_view, i);
|
||||
if (page != nullptr) {
|
||||
if (const auto it = m_pages_rev.find(page); it != m_pages_rev.end()) {
|
||||
state.Channels.push_back(it->second);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
void ChannelTabSwitcherHandy::UseTabsState(const TabsState &state) {
|
||||
for (auto id : state.Channels) {
|
||||
AddChannelTab(id);
|
||||
}
|
||||
}
|
||||
|
||||
void ChannelTabSwitcherHandy::GoBackOnCurrent() {
|
||||
AdvanceOnCurrent(-1);
|
||||
}
|
||||
|
||||
void ChannelTabSwitcherHandy::GoForwardOnCurrent() {
|
||||
AdvanceOnCurrent(1);
|
||||
}
|
||||
|
||||
void ChannelTabSwitcherHandy::GoToPreviousTab() {
|
||||
if (!hdy_tab_view_select_previous_page(m_tab_view)) {
|
||||
if (const auto num_pages = hdy_tab_view_get_n_pages(m_tab_view); num_pages > 1) {
|
||||
hdy_tab_view_set_selected_page(m_tab_view, hdy_tab_view_get_nth_page(m_tab_view, num_pages - 1));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ChannelTabSwitcherHandy::GoToNextTab() {
|
||||
if (!hdy_tab_view_select_next_page(m_tab_view)) {
|
||||
if (hdy_tab_view_get_n_pages(m_tab_view) > 1) {
|
||||
hdy_tab_view_set_selected_page(m_tab_view, hdy_tab_view_get_nth_page(m_tab_view, 0));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ChannelTabSwitcherHandy::GoToTab(int idx) {
|
||||
if (hdy_tab_view_get_n_pages(m_tab_view) >= idx + 1)
|
||||
hdy_tab_view_set_selected_page(m_tab_view, hdy_tab_view_get_nth_page(m_tab_view, idx));
|
||||
}
|
||||
|
||||
int ChannelTabSwitcherHandy::GetNumberOfTabs() const {
|
||||
return hdy_tab_view_get_n_pages(m_tab_view);
|
||||
}
|
||||
|
||||
void ChannelTabSwitcherHandy::CheckUnread(Snowflake id) {
|
||||
if (auto it = m_pages.find(id); it != m_pages.end()) {
|
||||
auto &discord = Abaddon::Get().GetDiscordClient();
|
||||
const bool has_unreads = discord.GetUnreadStateForChannel(id) > -1;
|
||||
const bool show_indicator = has_unreads && !discord.IsChannelMuted(id);
|
||||
hdy_tab_page_set_needs_attention(it->second, show_indicator);
|
||||
}
|
||||
}
|
||||
|
||||
void ChannelTabSwitcherHandy::ClearPage(HdyTabPage *page) {
|
||||
if (auto it = m_pages_rev.find(page); it != m_pages_rev.end()) {
|
||||
m_pages.erase(it->second);
|
||||
}
|
||||
m_pages_rev.erase(page);
|
||||
m_page_icons.erase(page);
|
||||
}
|
||||
|
||||
void ChannelTabSwitcherHandy::OnPageIconLoad(HdyTabPage *page, const Glib::RefPtr<Gdk::Pixbuf> &pb) {
|
||||
auto new_pb = pb->scale_simple(16, 16, Gdk::INTERP_BILINEAR);
|
||||
m_page_icons[page] = new_pb;
|
||||
hdy_tab_page_set_icon(page, G_ICON(new_pb->gobj()));
|
||||
}
|
||||
|
||||
void ChannelTabSwitcherHandy::CheckPageIcon(HdyTabPage *page, const ChannelData &data) {
|
||||
std::optional<std::string> icon_url;
|
||||
|
||||
if (data.GuildID.has_value()) {
|
||||
if (const auto guild = Abaddon::Get().GetDiscordClient().GetGuild(*data.GuildID); guild.has_value() && guild->HasIcon()) {
|
||||
icon_url = guild->GetIconURL("png", "16");
|
||||
}
|
||||
} else if (data.IsDM()) {
|
||||
icon_url = data.GetIconURL();
|
||||
}
|
||||
|
||||
if (icon_url.has_value()) {
|
||||
auto *child_widget = hdy_tab_page_get_child(page);
|
||||
if (child_widget == nullptr) return; // probably wont happen :---)
|
||||
// i think this works???
|
||||
auto *trackable = Glib::wrap(GTK_WIDGET(child_widget));
|
||||
Abaddon::Get().GetImageManager().LoadFromURL(
|
||||
*icon_url,
|
||||
sigc::track_obj([this, page](const Glib::RefPtr<Gdk::Pixbuf> &pb) { OnPageIconLoad(page, pb); },
|
||||
*trackable));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
hdy_tab_page_set_icon(page, nullptr);
|
||||
}
|
||||
|
||||
void ChannelTabSwitcherHandy::AppendPageHistory(HdyTabPage *page, Snowflake channel) {
|
||||
auto it = m_page_history.find(page);
|
||||
if (it == m_page_history.end()) {
|
||||
m_page_history[page] = PageHistory { { channel }, 0 };
|
||||
return;
|
||||
}
|
||||
|
||||
// drop everything beyond current position
|
||||
it->second.Visited.resize(++it->second.CurrentVisitedIndex);
|
||||
it->second.Visited.push_back(channel);
|
||||
}
|
||||
|
||||
void ChannelTabSwitcherHandy::AdvanceOnCurrent(size_t by) {
|
||||
auto *current = hdy_tab_view_get_selected_page(m_tab_view);
|
||||
if (current == nullptr) return;
|
||||
auto history = m_page_history.find(current);
|
||||
if (history == m_page_history.end()) return;
|
||||
if (by + history->second.CurrentVisitedIndex < 0 || by + history->second.CurrentVisitedIndex >= history->second.Visited.size()) return;
|
||||
|
||||
history->second.CurrentVisitedIndex += by;
|
||||
const auto to_id = history->second.Visited.at(history->second.CurrentVisitedIndex);
|
||||
|
||||
// temporarily point current index to the end so that it doesnt fuck up the history
|
||||
// remove it immediately after cuz the emit will call ReplaceActiveTab
|
||||
const auto real = history->second.CurrentVisitedIndex;
|
||||
history->second.CurrentVisitedIndex = history->second.Visited.size() - 1;
|
||||
m_signal_channel_switched_to.emit(to_id);
|
||||
// iterator might not be valid
|
||||
history = m_page_history.find(current);
|
||||
if (history != m_page_history.end()) {
|
||||
history->second.Visited.pop_back();
|
||||
}
|
||||
history->second.CurrentVisitedIndex = real;
|
||||
}
|
||||
|
||||
void ChannelTabSwitcherHandy::OnChannelAccessibilityChanged(Snowflake id, bool accessibility) {
|
||||
if (accessibility) return;
|
||||
if (auto it = m_pages.find(id); it != m_pages.end()) {
|
||||
if (hdy_tab_page_get_selected(it->second))
|
||||
if (!hdy_tab_view_select_previous_page(m_tab_view))
|
||||
hdy_tab_view_select_next_page(m_tab_view);
|
||||
|
||||
hdy_tab_view_close_page(m_tab_view, it->second);
|
||||
ClearPage(it->second);
|
||||
}
|
||||
}
|
||||
|
||||
ChannelTabSwitcherHandy::type_signal_channel_switched_to ChannelTabSwitcherHandy::signal_channel_switched_to() {
|
||||
return m_signal_channel_switched_to;
|
||||
}
|
||||
|
||||
#endif
|
||||
71
src/components/channeltabswitcherhandy.hpp
Normal file
71
src/components/channeltabswitcherhandy.hpp
Normal file
@@ -0,0 +1,71 @@
|
||||
#pragma once
|
||||
// perhaps this should be conditionally included within cmakelists?
|
||||
#ifdef WITH_LIBHANDY
|
||||
#include <gtkmm/box.h>
|
||||
#include <unordered_map>
|
||||
#include <handy.h>
|
||||
#include "discord/snowflake.hpp"
|
||||
#include "state.hpp"
|
||||
|
||||
class ChannelData;
|
||||
|
||||
// thin wrapper over c api
|
||||
// HdyTabBar + invisible HdyTabView since it needs one
|
||||
class ChannelTabSwitcherHandy : public Gtk::Box {
|
||||
public:
|
||||
ChannelTabSwitcherHandy();
|
||||
|
||||
// no-op if already added
|
||||
void AddChannelTab(Snowflake id);
|
||||
// switches to existing tab if it exists
|
||||
void ReplaceActiveTab(Snowflake id);
|
||||
TabsState GetTabsState();
|
||||
void UseTabsState(const TabsState &state);
|
||||
|
||||
void GoBackOnCurrent();
|
||||
void GoForwardOnCurrent();
|
||||
void GoToPreviousTab();
|
||||
void GoToNextTab();
|
||||
void GoToTab(int idx);
|
||||
|
||||
[[nodiscard]] int GetNumberOfTabs() const;
|
||||
|
||||
private:
|
||||
void CheckUnread(Snowflake id);
|
||||
void ClearPage(HdyTabPage *page);
|
||||
void OnPageIconLoad(HdyTabPage *page, const Glib::RefPtr<Gdk::Pixbuf> &pb);
|
||||
void CheckPageIcon(HdyTabPage *page, const ChannelData &data);
|
||||
void AppendPageHistory(HdyTabPage *page, Snowflake channel);
|
||||
void AdvanceOnCurrent(size_t by);
|
||||
|
||||
void OnChannelAccessibilityChanged(Snowflake id, bool accessibility);
|
||||
|
||||
HdyTabBar *m_tab_bar;
|
||||
Gtk::Widget *m_tab_bar_wrapped;
|
||||
HdyTabView *m_tab_view;
|
||||
Gtk::Widget *m_tab_view_wrapped;
|
||||
|
||||
std::unordered_map<Snowflake, HdyTabPage *> m_pages;
|
||||
std::unordered_map<HdyTabPage *, Snowflake> m_pages_rev;
|
||||
// need to hold a reference to the pixbuf data
|
||||
std::unordered_map<HdyTabPage *, Glib::RefPtr<Gdk::Pixbuf>> m_page_icons;
|
||||
|
||||
struct PageHistory {
|
||||
std::vector<Snowflake> Visited;
|
||||
size_t CurrentVisitedIndex;
|
||||
};
|
||||
|
||||
std::unordered_map<HdyTabPage *, PageHistory> m_page_history;
|
||||
|
||||
friend void selected_page_notify_cb(HdyTabView *, GParamSpec *, ChannelTabSwitcherHandy *);
|
||||
friend gboolean close_page_cb(HdyTabView *, HdyTabPage *, ChannelTabSwitcherHandy *);
|
||||
|
||||
public:
|
||||
using type_signal_channel_switched_to = sigc::signal<void, Snowflake>;
|
||||
|
||||
type_signal_channel_switched_to signal_channel_switched_to();
|
||||
|
||||
private:
|
||||
type_signal_channel_switched_to m_signal_channel_switched_to;
|
||||
};
|
||||
#endif
|
||||
@@ -1,11 +1,14 @@
|
||||
#include "chatinput.hpp"
|
||||
#include "abaddon.hpp"
|
||||
#include "constants.hpp"
|
||||
#include <filesystem>
|
||||
|
||||
ChatInput::ChatInput() {
|
||||
ChatInputText::ChatInputText() {
|
||||
get_style_context()->add_class("message-input");
|
||||
set_propagate_natural_height(true);
|
||||
set_min_content_height(20);
|
||||
set_max_content_height(250);
|
||||
set_policy(Gtk::POLICY_NEVER, Gtk::POLICY_AUTOMATIC);
|
||||
set_policy(Gtk::POLICY_EXTERNAL, Gtk::POLICY_AUTOMATIC);
|
||||
|
||||
// hack
|
||||
auto cb = [this](GdkEventKey *e) -> bool {
|
||||
@@ -20,22 +23,26 @@ ChatInput::ChatInput() {
|
||||
add(m_textview);
|
||||
}
|
||||
|
||||
void ChatInput::InsertText(const Glib::ustring &text) {
|
||||
void ChatInputText::InsertText(const Glib::ustring &text) {
|
||||
GetBuffer()->insert_at_cursor(text);
|
||||
m_textview.grab_focus();
|
||||
}
|
||||
|
||||
Glib::RefPtr<Gtk::TextBuffer> ChatInput::GetBuffer() {
|
||||
Glib::RefPtr<Gtk::TextBuffer> ChatInputText::GetBuffer() {
|
||||
return m_textview.get_buffer();
|
||||
}
|
||||
|
||||
// this isnt connected directly so that the chat window can handle stuff like the completer first
|
||||
bool ChatInput::ProcessKeyPress(GdkEventKey *event) {
|
||||
bool ChatInputText::ProcessKeyPress(GdkEventKey *event) {
|
||||
if (event->keyval == GDK_KEY_Escape) {
|
||||
m_signal_escape.emit();
|
||||
return true;
|
||||
}
|
||||
|
||||
if ((event->state & GDK_CONTROL_MASK) && event->keyval == GDK_KEY_v) {
|
||||
return CheckHandleClipboardPaste();
|
||||
}
|
||||
|
||||
if (event->keyval == GDK_KEY_Return) {
|
||||
if (event->state & GDK_SHIFT_MASK)
|
||||
return false;
|
||||
@@ -53,10 +60,514 @@ bool ChatInput::ProcessKeyPress(GdkEventKey *event) {
|
||||
return false;
|
||||
}
|
||||
|
||||
void ChatInput::on_grab_focus() {
|
||||
void ChatInputText::on_grab_focus() {
|
||||
m_textview.grab_focus();
|
||||
}
|
||||
|
||||
bool ChatInputText::CheckHandleClipboardPaste() {
|
||||
auto clip = Gtk::Clipboard::get();
|
||||
|
||||
if (!clip->wait_is_image_available()) return false;
|
||||
|
||||
const auto pb = clip->wait_for_image();
|
||||
if (pb) {
|
||||
m_signal_image_paste.emit(pb);
|
||||
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
ChatInputText::type_signal_submit ChatInputText::signal_submit() {
|
||||
return m_signal_submit;
|
||||
}
|
||||
|
||||
ChatInputText::type_signal_escape ChatInputText::signal_escape() {
|
||||
return m_signal_escape;
|
||||
}
|
||||
|
||||
ChatInputText::type_signal_image_paste ChatInputText::signal_image_paste() {
|
||||
return m_signal_image_paste;
|
||||
}
|
||||
|
||||
ChatInputTextContainer::ChatInputTextContainer() {
|
||||
// triple hack !!!
|
||||
auto cb = [this](GdkEventKey *e) -> bool {
|
||||
return event(reinterpret_cast<GdkEvent *>(e));
|
||||
};
|
||||
m_input.signal_key_press_event().connect(cb, false);
|
||||
|
||||
m_upload_img.property_icon_name() = "document-send-symbolic";
|
||||
m_upload_img.property_icon_size() = Gtk::ICON_SIZE_LARGE_TOOLBAR;
|
||||
m_upload_img.get_style_context()->add_class("message-input-browse-icon");
|
||||
|
||||
AddPointerCursor(m_upload_ev);
|
||||
|
||||
m_upload_ev.signal_button_press_event().connect([this](GdkEventButton *ev) -> bool {
|
||||
if (ev->button == GDK_BUTTON_PRIMARY) {
|
||||
ShowFileChooser();
|
||||
// return focus
|
||||
m_input.grab_focus();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
m_upload_ev.add(m_upload_img);
|
||||
add_overlay(m_upload_ev);
|
||||
add(m_input);
|
||||
|
||||
show_all_children();
|
||||
|
||||
// stop the overlay from using (start) padding
|
||||
signal_get_child_position().connect(sigc::mem_fun(*this, &ChatInputTextContainer::GetChildPosition), false);
|
||||
}
|
||||
|
||||
void ChatInputTextContainer::ShowFileChooser() {
|
||||
auto dlg = Gtk::FileChooserNative::create("Choose file", Gtk::FILE_CHOOSER_ACTION_OPEN);
|
||||
dlg->set_select_multiple(true);
|
||||
dlg->set_modal(true);
|
||||
|
||||
dlg->signal_response().connect([this, dlg](int response) {
|
||||
if (response == Gtk::RESPONSE_ACCEPT) {
|
||||
for (const auto &file : dlg->get_files()) {
|
||||
m_signal_add_attachment.emit(file);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
auto filter_all = Gtk::FileFilter::create();
|
||||
filter_all->set_name("All files (*.*)");
|
||||
filter_all->add_pattern("*.*");
|
||||
dlg->add_filter(filter_all);
|
||||
|
||||
dlg->run();
|
||||
}
|
||||
|
||||
ChatInputText &ChatInputTextContainer::Get() {
|
||||
return m_input;
|
||||
}
|
||||
|
||||
void ChatInputTextContainer::ShowChooserIcon() {
|
||||
m_upload_ev.show();
|
||||
}
|
||||
|
||||
void ChatInputTextContainer::HideChooserIcon() {
|
||||
m_upload_ev.hide();
|
||||
}
|
||||
|
||||
bool ChatInputTextContainer::GetChildPosition(Gtk::Widget *child, Gdk::Rectangle &pos) {
|
||||
Gtk::Allocation main_alloc;
|
||||
{
|
||||
auto *grandchild = m_input.get_child();
|
||||
int x, y;
|
||||
if (grandchild->translate_coordinates(m_input, 0, 0, x, y)) {
|
||||
main_alloc.set_x(x);
|
||||
main_alloc.set_y(y);
|
||||
} else {
|
||||
main_alloc.set_x(0);
|
||||
main_alloc.set_y(0);
|
||||
}
|
||||
main_alloc.set_width(grandchild->get_allocated_width());
|
||||
main_alloc.set_height(grandchild->get_allocated_height());
|
||||
}
|
||||
|
||||
Gtk::Requisition min, req;
|
||||
child->get_preferred_size(min, req);
|
||||
|
||||
// let css move it around
|
||||
pos.set_x(0);
|
||||
pos.set_y(0);
|
||||
pos.set_width(std::max(min.width, std::min(main_alloc.get_width(), req.width)));
|
||||
pos.set_height(std::max(min.height, std::min(main_alloc.get_height(), req.height)));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
ChatInputTextContainer::type_signal_add_attachment ChatInputTextContainer::signal_add_attachment() {
|
||||
return m_signal_add_attachment;
|
||||
}
|
||||
|
||||
ChatInputAttachmentContainer::ChatInputAttachmentContainer()
|
||||
: m_box(Gtk::ORIENTATION_HORIZONTAL) {
|
||||
get_style_context()->add_class("attachment-container");
|
||||
|
||||
m_box.set_halign(Gtk::ALIGN_START);
|
||||
|
||||
add(m_box);
|
||||
m_box.show();
|
||||
|
||||
set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_NEVER);
|
||||
set_vexpand(true);
|
||||
set_size_request(-1, AttachmentItemSize + 10);
|
||||
}
|
||||
|
||||
void ChatInputAttachmentContainer::Clear() {
|
||||
for (auto *item : m_attachments) {
|
||||
item->RemoveIfTemp();
|
||||
delete item;
|
||||
}
|
||||
m_attachments.clear();
|
||||
}
|
||||
|
||||
void ChatInputAttachmentContainer::ClearNoPurge() {
|
||||
for (auto *item : m_attachments) {
|
||||
delete item;
|
||||
}
|
||||
m_attachments.clear();
|
||||
}
|
||||
|
||||
bool ChatInputAttachmentContainer::AddImage(const Glib::RefPtr<Gdk::Pixbuf> &pb) {
|
||||
if (m_attachments.size() == 10) return false;
|
||||
|
||||
static unsigned go_up = 0;
|
||||
std::string dest_name = "pasted-image-" + std::to_string(go_up++);
|
||||
const auto path = (std::filesystem::temp_directory_path() / "abaddon-cache" / dest_name).string();
|
||||
|
||||
try {
|
||||
pb->save(path, "png");
|
||||
} catch (...) {
|
||||
fprintf(stderr, "pasted image save error\n");
|
||||
return false;
|
||||
}
|
||||
|
||||
auto *item = Gtk::make_managed<ChatInputAttachmentItem>(Gio::File::create_for_path(path), pb);
|
||||
item->set_valign(Gtk::ALIGN_FILL);
|
||||
item->set_vexpand(true);
|
||||
item->set_margin_bottom(5);
|
||||
item->show();
|
||||
m_box.add(*item);
|
||||
|
||||
m_attachments.push_back(item);
|
||||
|
||||
item->signal_item_removed().connect([this, item] {
|
||||
item->RemoveIfTemp();
|
||||
if (auto it = std::find(m_attachments.begin(), m_attachments.end(), item); it != m_attachments.end())
|
||||
m_attachments.erase(it);
|
||||
delete item;
|
||||
if (m_attachments.empty())
|
||||
m_signal_emptied.emit();
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ChatInputAttachmentContainer::AddFile(const Glib::RefPtr<Gio::File> &file, Glib::RefPtr<Gdk::Pixbuf> pb) {
|
||||
if (m_attachments.size() == 10) return false;
|
||||
|
||||
ChatInputAttachmentItem *item;
|
||||
if (pb)
|
||||
item = Gtk::make_managed<ChatInputAttachmentItem>(file, pb, true);
|
||||
else
|
||||
item = Gtk::make_managed<ChatInputAttachmentItem>(file);
|
||||
item->set_valign(Gtk::ALIGN_FILL);
|
||||
item->set_vexpand(true);
|
||||
item->set_margin_bottom(5);
|
||||
item->show();
|
||||
m_box.add(*item);
|
||||
|
||||
m_attachments.push_back(item);
|
||||
|
||||
item->signal_item_removed().connect([this, item] {
|
||||
if (auto it = std::find(m_attachments.begin(), m_attachments.end(), item); it != m_attachments.end())
|
||||
m_attachments.erase(it);
|
||||
delete item;
|
||||
if (m_attachments.empty())
|
||||
m_signal_emptied.emit();
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
std::vector<ChatSubmitParams::Attachment> ChatInputAttachmentContainer::GetAttachments() const {
|
||||
std::vector<ChatSubmitParams::Attachment> ret;
|
||||
for (auto *x : m_attachments) {
|
||||
if (!x->GetFile()->query_exists())
|
||||
puts("bad!");
|
||||
ret.push_back({ x->GetFile(), x->GetType(), x->GetFilename() });
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
ChatInputAttachmentContainer::type_signal_emptied ChatInputAttachmentContainer::signal_emptied() {
|
||||
return m_signal_emptied;
|
||||
}
|
||||
|
||||
ChatInputAttachmentItem::ChatInputAttachmentItem(const Glib::RefPtr<Gio::File> &file)
|
||||
: m_file(file)
|
||||
, m_img(Gtk::make_managed<Gtk::Image>())
|
||||
, m_type(ChatSubmitParams::ExtantFile)
|
||||
, m_box(Gtk::ORIENTATION_VERTICAL) {
|
||||
get_style_context()->add_class("attachment-item");
|
||||
|
||||
set_size_request(AttachmentItemSize, AttachmentItemSize);
|
||||
set_halign(Gtk::ALIGN_START);
|
||||
m_box.set_hexpand(true);
|
||||
m_box.set_vexpand(true);
|
||||
m_box.set_halign(Gtk::ALIGN_FILL);
|
||||
m_box.set_valign(Gtk::ALIGN_FILL);
|
||||
m_box.add(*m_img);
|
||||
m_box.add(m_label);
|
||||
add(m_box);
|
||||
show_all_children();
|
||||
|
||||
m_label.set_valign(Gtk::ALIGN_END);
|
||||
m_label.set_max_width_chars(0); // will constrain to given size
|
||||
m_label.set_ellipsize(Pango::ELLIPSIZE_MIDDLE);
|
||||
m_label.set_margin_start(7);
|
||||
m_label.set_margin_end(7);
|
||||
|
||||
m_img->set_vexpand(true);
|
||||
m_img->property_icon_name() = "document-send-symbolic";
|
||||
m_img->property_icon_size() = Gtk::ICON_SIZE_DIALOG; // todo figure out how to not use this weird property??? i dont know how icons work (screw your theme)
|
||||
|
||||
SetFilenameFromFile();
|
||||
|
||||
SetupMenu();
|
||||
UpdateTooltip();
|
||||
}
|
||||
|
||||
ChatInputAttachmentItem::ChatInputAttachmentItem(const Glib::RefPtr<Gio::File> &file, const Glib::RefPtr<Gdk::Pixbuf> &pb, bool is_extant)
|
||||
: m_file(file)
|
||||
, m_img(Gtk::make_managed<Gtk::Image>())
|
||||
, m_type(is_extant ? ChatSubmitParams::ExtantFile : ChatSubmitParams::PastedImage)
|
||||
, m_filename("unknown.png")
|
||||
, m_label("unknown.png")
|
||||
, m_box(Gtk::ORIENTATION_VERTICAL) {
|
||||
get_style_context()->add_class("attachment-item");
|
||||
|
||||
int outw, outh;
|
||||
GetImageDimensions(pb->get_width(), pb->get_height(), outw, outh, AttachmentItemSize, AttachmentItemSize);
|
||||
m_img->property_pixbuf() = pb->scale_simple(outw, outh, Gdk::INTERP_BILINEAR);
|
||||
|
||||
set_size_request(AttachmentItemSize, AttachmentItemSize);
|
||||
set_halign(Gtk::ALIGN_START);
|
||||
m_box.set_hexpand(true);
|
||||
m_box.set_vexpand(true);
|
||||
m_box.set_halign(Gtk::ALIGN_FILL);
|
||||
m_box.set_valign(Gtk::ALIGN_FILL);
|
||||
m_box.add(*m_img);
|
||||
m_box.add(m_label);
|
||||
add(m_box);
|
||||
show_all_children();
|
||||
|
||||
m_label.set_valign(Gtk::ALIGN_END);
|
||||
m_label.set_max_width_chars(0); // will constrain to given size
|
||||
m_label.set_ellipsize(Pango::ELLIPSIZE_MIDDLE);
|
||||
m_label.set_margin_start(7);
|
||||
m_label.set_margin_end(7);
|
||||
|
||||
m_img->set_vexpand(true);
|
||||
|
||||
if (is_extant)
|
||||
SetFilenameFromFile();
|
||||
|
||||
SetupMenu();
|
||||
UpdateTooltip();
|
||||
}
|
||||
|
||||
Glib::RefPtr<Gio::File> ChatInputAttachmentItem::GetFile() const {
|
||||
return m_file;
|
||||
}
|
||||
|
||||
ChatSubmitParams::AttachmentType ChatInputAttachmentItem::GetType() const {
|
||||
return m_type;
|
||||
}
|
||||
|
||||
std::string ChatInputAttachmentItem::GetFilename() const {
|
||||
return m_filename;
|
||||
}
|
||||
|
||||
bool ChatInputAttachmentItem::IsTemp() const noexcept {
|
||||
return m_type == ChatSubmitParams::PastedImage;
|
||||
}
|
||||
|
||||
void ChatInputAttachmentItem::RemoveIfTemp() {
|
||||
if (IsTemp())
|
||||
m_file->remove();
|
||||
}
|
||||
|
||||
void ChatInputAttachmentItem::SetFilenameFromFile() {
|
||||
auto info = m_file->query_info(G_FILE_ATTRIBUTE_STANDARD_DISPLAY_NAME);
|
||||
m_filename = info->get_attribute_string(G_FILE_ATTRIBUTE_STANDARD_DISPLAY_NAME);
|
||||
m_label.set_text(m_filename);
|
||||
}
|
||||
|
||||
void ChatInputAttachmentItem::SetupMenu() {
|
||||
m_menu_remove.set_label("Remove");
|
||||
m_menu_remove.signal_activate().connect([this] {
|
||||
m_signal_item_removed.emit();
|
||||
});
|
||||
|
||||
m_menu_set_filename.set_label("Change Filename");
|
||||
m_menu_set_filename.signal_activate().connect([this] {
|
||||
const auto name = Abaddon::Get().ShowTextPrompt("Enter new filename for attachment", "Enter filename", m_filename);
|
||||
if (name.has_value()) {
|
||||
m_filename = *name;
|
||||
m_label.set_text(m_filename);
|
||||
UpdateTooltip();
|
||||
}
|
||||
});
|
||||
|
||||
m_menu.add(m_menu_set_filename);
|
||||
m_menu.add(m_menu_remove);
|
||||
m_menu.show_all();
|
||||
|
||||
signal_button_press_event().connect([this](GdkEventButton *ev) -> bool {
|
||||
if (ev->button == GDK_BUTTON_SECONDARY) {
|
||||
m_menu.popup_at_pointer(reinterpret_cast<GdkEvent *>(ev));
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
void ChatInputAttachmentItem::UpdateTooltip() {
|
||||
set_tooltip_text(m_filename);
|
||||
}
|
||||
|
||||
ChatInputAttachmentItem::type_signal_item_removed ChatInputAttachmentItem::signal_item_removed() {
|
||||
return m_signal_item_removed;
|
||||
}
|
||||
|
||||
ChatInput::ChatInput()
|
||||
: Gtk::Box(Gtk::ORIENTATION_VERTICAL) {
|
||||
m_input.signal_add_attachment().connect(sigc::mem_fun(*this, &ChatInput::AddAttachment));
|
||||
|
||||
m_input.Get().signal_escape().connect([this] {
|
||||
m_attachments.Clear();
|
||||
m_attachments_revealer.set_reveal_child(false);
|
||||
m_signal_escape.emit();
|
||||
});
|
||||
|
||||
m_input.Get().signal_submit().connect([this](const Glib::ustring &input) -> bool {
|
||||
ChatSubmitParams data;
|
||||
data.Message = input;
|
||||
data.Attachments = m_attachments.GetAttachments();
|
||||
|
||||
bool b = m_signal_submit.emit(data);
|
||||
if (b) {
|
||||
m_attachments_revealer.set_reveal_child(false);
|
||||
m_attachments.ClearNoPurge();
|
||||
}
|
||||
return b;
|
||||
});
|
||||
|
||||
m_attachments.set_vexpand(false);
|
||||
|
||||
m_attachments_revealer.set_transition_type(Gtk::REVEALER_TRANSITION_TYPE_SLIDE_UP);
|
||||
m_attachments_revealer.add(m_attachments);
|
||||
add(m_attachments_revealer);
|
||||
add(m_input);
|
||||
show_all_children();
|
||||
|
||||
m_input.Get().signal_image_paste().connect([this](const Glib::RefPtr<Gdk::Pixbuf> &pb) {
|
||||
if (CanAttachFiles() && m_attachments.AddImage(pb))
|
||||
m_attachments_revealer.set_reveal_child(true);
|
||||
});
|
||||
|
||||
// double hack !
|
||||
auto cb = [this](GdkEventKey *e) -> bool {
|
||||
return event(reinterpret_cast<GdkEvent *>(e));
|
||||
};
|
||||
m_input.signal_key_press_event().connect(cb, false);
|
||||
|
||||
m_attachments.signal_emptied().connect([this] {
|
||||
m_attachments_revealer.set_reveal_child(false);
|
||||
});
|
||||
|
||||
SetActiveChannel(Snowflake::Invalid);
|
||||
}
|
||||
|
||||
void ChatInput::InsertText(const Glib::ustring &text) {
|
||||
m_input.Get().InsertText(text);
|
||||
}
|
||||
|
||||
Glib::RefPtr<Gtk::TextBuffer> ChatInput::GetBuffer() {
|
||||
return m_input.Get().GetBuffer();
|
||||
}
|
||||
|
||||
bool ChatInput::ProcessKeyPress(GdkEventKey *event) {
|
||||
return m_input.Get().ProcessKeyPress(event);
|
||||
}
|
||||
|
||||
void ChatInput::AddAttachment(const Glib::RefPtr<Gio::File> &file) {
|
||||
if (!CanAttachFiles()) return;
|
||||
|
||||
std::string content_type;
|
||||
|
||||
try {
|
||||
const auto info = file->query_info(G_FILE_ATTRIBUTE_STANDARD_CONTENT_TYPE);
|
||||
content_type = info->get_attribute_string(G_FILE_ATTRIBUTE_STANDARD_CONTENT_TYPE);
|
||||
} catch (const Gio::Error &err) {
|
||||
printf("io error: %s\n", err.what().c_str());
|
||||
return;
|
||||
} catch (...) {
|
||||
puts("attachment query exception");
|
||||
return;
|
||||
}
|
||||
|
||||
static const std::unordered_set<std::string> image_exts {
|
||||
".png",
|
||||
".jpg",
|
||||
};
|
||||
|
||||
if (image_exts.find(content_type) != image_exts.end()) {
|
||||
if (AddFileAsImageAttachment(file)) {
|
||||
m_attachments_revealer.set_reveal_child(true);
|
||||
m_input.Get().grab_focus();
|
||||
}
|
||||
} else if (m_attachments.AddFile(file)) {
|
||||
m_attachments_revealer.set_reveal_child(true);
|
||||
m_input.Get().grab_focus();
|
||||
}
|
||||
}
|
||||
|
||||
void ChatInput::IndicateTooLarge() {
|
||||
m_input.Get().get_style_context()->add_class("bad-input");
|
||||
const auto cb = [this] {
|
||||
m_input.Get().get_style_context()->remove_class("bad-input");
|
||||
};
|
||||
Glib::signal_timeout().connect_seconds_once(sigc::track_obj(cb, *this), 2);
|
||||
}
|
||||
|
||||
void ChatInput::SetActiveChannel(Snowflake id) {
|
||||
m_active_channel = id;
|
||||
if (CanAttachFiles()) {
|
||||
m_input.Get().get_style_context()->add_class("with-browse-icon");
|
||||
m_input.ShowChooserIcon();
|
||||
} else {
|
||||
m_input.Get().get_style_context()->remove_class("with-browse-icon");
|
||||
m_input.HideChooserIcon();
|
||||
}
|
||||
}
|
||||
|
||||
void ChatInput::StartReplying() {
|
||||
m_input.Get().grab_focus();
|
||||
m_input.Get().get_style_context()->add_class("replying");
|
||||
}
|
||||
|
||||
void ChatInput::StopReplying() {
|
||||
m_input.Get().get_style_context()->remove_class("replying");
|
||||
}
|
||||
|
||||
bool ChatInput::AddFileAsImageAttachment(const Glib::RefPtr<Gio::File> &file) {
|
||||
try {
|
||||
const auto read_stream = file->read();
|
||||
if (!read_stream) return false;
|
||||
const auto pb = Gdk::Pixbuf::create_from_stream(read_stream);
|
||||
return m_attachments.AddFile(file, pb);
|
||||
} catch (...) {
|
||||
return m_attachments.AddFile(file);
|
||||
}
|
||||
}
|
||||
|
||||
bool ChatInput::CanAttachFiles() {
|
||||
return Abaddon::Get().GetDiscordClient().HasSelfChannelPermission(m_active_channel, Permission::ATTACH_FILES | Permission::SEND_MESSAGES);
|
||||
}
|
||||
|
||||
ChatInput::type_signal_submit ChatInput::signal_submit() {
|
||||
return m_signal_submit;
|
||||
}
|
||||
|
||||
@@ -1,9 +1,72 @@
|
||||
#pragma once
|
||||
#include <gtkmm.h>
|
||||
#include "discord/chatsubmitparams.hpp"
|
||||
#include "discord/permissions.hpp"
|
||||
|
||||
class ChatInput : public Gtk::ScrolledWindow {
|
||||
class ChatInputAttachmentItem : public Gtk::EventBox {
|
||||
public:
|
||||
ChatInput();
|
||||
ChatInputAttachmentItem(const Glib::RefPtr<Gio::File> &file);
|
||||
ChatInputAttachmentItem(const Glib::RefPtr<Gio::File> &file, const Glib::RefPtr<Gdk::Pixbuf> &pb, bool is_extant = false);
|
||||
|
||||
[[nodiscard]] Glib::RefPtr<Gio::File> GetFile() const;
|
||||
[[nodiscard]] ChatSubmitParams::AttachmentType GetType() const;
|
||||
[[nodiscard]] std::string GetFilename() const;
|
||||
[[nodiscard]] bool IsTemp() const noexcept;
|
||||
void RemoveIfTemp();
|
||||
|
||||
private:
|
||||
void SetFilenameFromFile();
|
||||
void SetupMenu();
|
||||
void UpdateTooltip();
|
||||
|
||||
Gtk::Menu m_menu;
|
||||
Gtk::MenuItem m_menu_remove;
|
||||
Gtk::MenuItem m_menu_set_filename;
|
||||
|
||||
Gtk::Box m_box;
|
||||
Gtk::Label m_label;
|
||||
Gtk::Image *m_img = nullptr;
|
||||
|
||||
Glib::RefPtr<Gio::File> m_file;
|
||||
ChatSubmitParams::AttachmentType m_type;
|
||||
std::string m_filename;
|
||||
|
||||
private:
|
||||
using type_signal_item_removed = sigc::signal<void>;
|
||||
|
||||
type_signal_item_removed m_signal_item_removed;
|
||||
|
||||
public:
|
||||
type_signal_item_removed signal_item_removed();
|
||||
};
|
||||
|
||||
class ChatInputAttachmentContainer : public Gtk::ScrolledWindow {
|
||||
public:
|
||||
ChatInputAttachmentContainer();
|
||||
|
||||
void Clear();
|
||||
void ClearNoPurge();
|
||||
bool AddImage(const Glib::RefPtr<Gdk::Pixbuf> &pb);
|
||||
bool AddFile(const Glib::RefPtr<Gio::File> &file, Glib::RefPtr<Gdk::Pixbuf> pb = {});
|
||||
[[nodiscard]] std::vector<ChatSubmitParams::Attachment> GetAttachments() const;
|
||||
|
||||
private:
|
||||
std::vector<ChatInputAttachmentItem *> m_attachments;
|
||||
|
||||
Gtk::Box m_box;
|
||||
|
||||
private:
|
||||
using type_signal_emptied = sigc::signal<void>;
|
||||
|
||||
type_signal_emptied m_signal_emptied;
|
||||
|
||||
public:
|
||||
type_signal_emptied signal_emptied();
|
||||
};
|
||||
|
||||
class ChatInputText : public Gtk::ScrolledWindow {
|
||||
public:
|
||||
ChatInputText();
|
||||
|
||||
void InsertText(const Glib::ustring &text);
|
||||
Glib::RefPtr<Gtk::TextBuffer> GetBuffer();
|
||||
@@ -15,9 +78,78 @@ protected:
|
||||
private:
|
||||
Gtk::TextView m_textview;
|
||||
|
||||
bool CheckHandleClipboardPaste();
|
||||
|
||||
public:
|
||||
typedef sigc::signal<bool, Glib::ustring> type_signal_submit;
|
||||
typedef sigc::signal<void> type_signal_escape;
|
||||
using type_signal_submit = sigc::signal<bool, Glib::ustring>;
|
||||
using type_signal_escape = sigc::signal<void>;
|
||||
using type_signal_image_paste = sigc::signal<void, Glib::RefPtr<Gdk::Pixbuf>>;
|
||||
|
||||
type_signal_submit signal_submit();
|
||||
type_signal_escape signal_escape();
|
||||
type_signal_image_paste signal_image_paste();
|
||||
|
||||
private:
|
||||
type_signal_submit m_signal_submit;
|
||||
type_signal_escape m_signal_escape;
|
||||
type_signal_image_paste m_signal_image_paste;
|
||||
};
|
||||
|
||||
// file upload, text
|
||||
class ChatInputTextContainer : public Gtk::Overlay {
|
||||
public:
|
||||
ChatInputTextContainer();
|
||||
|
||||
// not proxying everythign lol!!
|
||||
ChatInputText &Get();
|
||||
|
||||
void ShowChooserIcon();
|
||||
void HideChooserIcon();
|
||||
|
||||
private:
|
||||
void ShowFileChooser();
|
||||
bool GetChildPosition(Gtk::Widget *child, Gdk::Rectangle &pos);
|
||||
|
||||
Gtk::EventBox m_upload_ev;
|
||||
Gtk::Image m_upload_img;
|
||||
ChatInputText m_input;
|
||||
|
||||
public:
|
||||
using type_signal_add_attachment = sigc::signal<void, Glib::RefPtr<Gio::File>>;
|
||||
type_signal_add_attachment signal_add_attachment();
|
||||
|
||||
private:
|
||||
type_signal_add_attachment m_signal_add_attachment;
|
||||
};
|
||||
|
||||
class ChatInput : public Gtk::Box {
|
||||
public:
|
||||
ChatInput();
|
||||
|
||||
void InsertText(const Glib::ustring &text);
|
||||
Glib::RefPtr<Gtk::TextBuffer> GetBuffer();
|
||||
bool ProcessKeyPress(GdkEventKey *event);
|
||||
void AddAttachment(const Glib::RefPtr<Gio::File> &file);
|
||||
void IndicateTooLarge();
|
||||
|
||||
void SetActiveChannel(Snowflake id);
|
||||
|
||||
void StartReplying();
|
||||
void StopReplying();
|
||||
|
||||
private:
|
||||
bool AddFileAsImageAttachment(const Glib::RefPtr<Gio::File> &file);
|
||||
bool CanAttachFiles();
|
||||
|
||||
Gtk::Revealer m_attachments_revealer;
|
||||
ChatInputAttachmentContainer m_attachments;
|
||||
ChatInputTextContainer m_input;
|
||||
|
||||
Snowflake m_active_channel;
|
||||
|
||||
public:
|
||||
using type_signal_submit = sigc::signal<bool, ChatSubmitParams>;
|
||||
using type_signal_escape = sigc::signal<void>;
|
||||
|
||||
type_signal_submit signal_submit();
|
||||
type_signal_escape signal_escape();
|
||||
|
||||
@@ -34,6 +34,9 @@ void ChatList::Clear() {
|
||||
delete *it;
|
||||
it++;
|
||||
}
|
||||
m_id_to_widget.clear();
|
||||
m_num_messages = 0;
|
||||
m_num_rows = 0;
|
||||
}
|
||||
|
||||
void ChatList::SetActiveChannel(Snowflake id) {
|
||||
@@ -352,10 +355,6 @@ 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;
|
||||
}
|
||||
|
||||
@@ -63,7 +63,6 @@ private:
|
||||
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>;
|
||||
@@ -73,7 +72,6 @@ public:
|
||||
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();
|
||||
@@ -84,7 +82,6 @@ public:
|
||||
|
||||
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;
|
||||
|
||||
@@ -32,7 +32,6 @@ ChatMessageItemContainer *ChatMessageItemContainer::FromMessage(const Message &d
|
||||
|
||||
if (!data.Content.empty() || 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);
|
||||
}
|
||||
|
||||
@@ -101,7 +100,6 @@ void ChatMessageItemContainer::UpdateContent() {
|
||||
|
||||
if (!data->Embeds.empty()) {
|
||||
m_embed_component = CreateEmbedsComponent(data->Embeds);
|
||||
AttachEventHandlers(*m_embed_component);
|
||||
m_main.add(*m_embed_component);
|
||||
m_embed_component->show_all();
|
||||
}
|
||||
@@ -152,12 +150,12 @@ void ChatMessageItemContainer::UpdateAttributes() {
|
||||
|
||||
void ChatMessageItemContainer::AddClickHandler(Gtk::Widget *widget, const std::string &url) {
|
||||
// clang-format off
|
||||
widget->signal_button_press_event().connect([url](GdkEventButton *event) -> bool {
|
||||
if (event->type == GDK_BUTTON_PRESS && event->button == GDK_BUTTON_PRIMARY) {
|
||||
widget->signal_button_release_event().connect([url](GdkEventButton *event) -> bool {
|
||||
if (event->type == GDK_BUTTON_RELEASE && event->button == GDK_BUTTON_PRIMARY) {
|
||||
LaunchBrowser(url);
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
return true;
|
||||
return false;
|
||||
}, false);
|
||||
// clang-format on
|
||||
}
|
||||
@@ -174,6 +172,8 @@ Gtk::TextView *ChatMessageItemContainer::CreateTextComponent(const Message &data
|
||||
tv->set_halign(Gtk::ALIGN_FILL);
|
||||
tv->set_hexpand(true);
|
||||
|
||||
tv->signal_button_press_event().connect(sigc::mem_fun(*this, &ChatMessageItemContainer::OnTextViewButtonPress), false);
|
||||
|
||||
UpdateTextComponent(tv);
|
||||
|
||||
return tv;
|
||||
@@ -281,8 +281,6 @@ void ChatMessageItemContainer::UpdateTextComponent(Gtk::TextView *tv) {
|
||||
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>");
|
||||
}
|
||||
@@ -297,12 +295,10 @@ Gtk::Widget *ChatMessageItemContainer::CreateEmbedsComponent(const std::vector<E
|
||||
if (IsEmbedImageOnly(embed)) {
|
||||
auto *widget = CreateImageComponent(*embed.Thumbnail->ProxyURL, *embed.Thumbnail->URL, *embed.Thumbnail->Width, *embed.Thumbnail->Height);
|
||||
widget->show();
|
||||
AttachEventHandlers(*widget);
|
||||
box->add(*widget);
|
||||
} else {
|
||||
auto *widget = CreateEmbedComponent(embed);
|
||||
widget->show();
|
||||
AttachEventHandlers(*widget);
|
||||
box->add(*widget);
|
||||
}
|
||||
}
|
||||
@@ -361,8 +357,8 @@ Gtk::Widget *ChatMessageItemContainer::CreateEmbedComponent(const EmbedData &emb
|
||||
if (embed.URL.has_value()) {
|
||||
AddPointerCursor(*title_ev);
|
||||
auto url = *embed.URL;
|
||||
title_ev->signal_button_press_event().connect([url = std::move(url)](GdkEventButton *event) -> bool {
|
||||
if (event->button == GDK_BUTTON_PRIMARY) {
|
||||
title_ev->signal_button_release_event().connect([url = std::move(url)](GdkEventButton *event) -> bool {
|
||||
if (event->type == GDK_BUTTON_RELEASE && event->button == GDK_BUTTON_PRIMARY) {
|
||||
LaunchBrowser(url);
|
||||
return true;
|
||||
}
|
||||
@@ -493,12 +489,22 @@ Gtk::Widget *ChatMessageItemContainer::CreateImageComponent(const std::string &p
|
||||
Gtk::EventBox *ev = Gtk::manage(new Gtk::EventBox);
|
||||
Gtk::Image *widget = Gtk::manage(new LazyImage(proxy_url, w, h, false));
|
||||
ev->add(*widget);
|
||||
ev->set_halign(Gtk::ALIGN_START);
|
||||
widget->set_halign(Gtk::ALIGN_START);
|
||||
widget->set_size_request(w, h);
|
||||
|
||||
AttachEventHandlers(*ev);
|
||||
AddClickHandler(ev, url);
|
||||
|
||||
const auto on_button_press_event = [this, url](GdkEventButton *e) -> bool {
|
||||
if (e->type == GDK_BUTTON_PRESS && e->button == GDK_BUTTON_SECONDARY) {
|
||||
m_selected_link = url;
|
||||
m_link_menu.popup_at_pointer(reinterpret_cast<GdkEvent *>(e));
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
ev->signal_button_press_event().connect(on_button_press_event, false);
|
||||
|
||||
return ev;
|
||||
}
|
||||
|
||||
@@ -510,9 +516,18 @@ Gtk::Widget *ChatMessageItemContainer::CreateAttachmentComponent(const Attachmen
|
||||
ev->get_style_context()->add_class("message-attachment-box");
|
||||
ev->add(*btn);
|
||||
|
||||
AttachEventHandlers(*ev);
|
||||
AddClickHandler(ev, data.URL);
|
||||
|
||||
const auto on_button_press_event = [this, url = data.URL](GdkEventButton *e) -> bool {
|
||||
if (e->type == GDK_BUTTON_PRESS && e->button == GDK_BUTTON_SECONDARY) {
|
||||
m_selected_link = url;
|
||||
m_link_menu.popup_at_pointer(reinterpret_cast<GdkEvent *>(e));
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
ev->signal_button_press_event().connect(on_button_press_event, false);
|
||||
|
||||
return ev;
|
||||
}
|
||||
|
||||
@@ -534,7 +549,6 @@ Gtk::Widget *ChatMessageItemContainer::CreateStickersComponent(const std::vector
|
||||
|
||||
box->show();
|
||||
|
||||
AttachEventHandlers(*box);
|
||||
return box;
|
||||
}
|
||||
|
||||
@@ -641,13 +655,19 @@ Gtk::Widget *ChatMessageItemContainer::CreateReplyComponent(const Message &data)
|
||||
const auto role = discord.GetRole(role_id);
|
||||
if (role.has_value()) {
|
||||
const auto author = discord.GetUser(author_id);
|
||||
return "<b><span color=\"#" + IntToCSSColor(role->Color) + "\">" + author->GetEscapedString() + "</span></b>";
|
||||
if (author.has_value()) {
|
||||
return "<b><span color=\"#" + IntToCSSColor(role->Color) + "\">" + author->GetEscapedString() + "</span></b>";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const auto author = discord.GetUser(author_id);
|
||||
return author->GetEscapedBoldString<false>();
|
||||
if (author.has_value()) {
|
||||
return author->GetEscapedBoldString<false>();
|
||||
}
|
||||
|
||||
return "<b>Unknown User</b>";
|
||||
};
|
||||
|
||||
// if the message wasnt fetched from store it might have an un-fetched reference
|
||||
@@ -659,15 +679,15 @@ Gtk::Widget *ChatMessageItemContainer::CreateReplyComponent(const Message &data)
|
||||
}
|
||||
|
||||
if (data.Interaction.has_value()) {
|
||||
const auto user = *discord.GetUser(data.Interaction->User.ID);
|
||||
|
||||
if (data.GuildID.has_value()) {
|
||||
lbl->set_markup(get_author_markup(user.ID, *data.GuildID) +
|
||||
lbl->set_markup(get_author_markup(data.Interaction->User.ID, *data.GuildID) +
|
||||
" used <span color='#697ec4'>/" +
|
||||
Glib::Markup::escape_text(data.Interaction->Name) +
|
||||
"</span>");
|
||||
} else if (const auto user = discord.GetUser(data.Interaction->User.ID); user.has_value()) {
|
||||
lbl->set_markup(user->GetEscapedBoldString<false>());
|
||||
} else {
|
||||
lbl->set_markup(user.GetEscapedBoldString<false>());
|
||||
lbl->set_markup("<b>Unknown User</b>");
|
||||
}
|
||||
} else if (referenced_message.has_value()) {
|
||||
if (referenced_message.value() == nullptr) {
|
||||
@@ -956,7 +976,6 @@ void ChatMessageItemContainer::HandleChannelMentions(const Glib::RefPtr<Gtk::Tex
|
||||
}
|
||||
|
||||
void ChatMessageItemContainer::HandleChannelMentions(Gtk::TextView *tv) {
|
||||
tv->signal_button_press_event().connect(sigc::mem_fun(*this, &ChatMessageItemContainer::OnClickChannel), false);
|
||||
HandleChannelMentions(tv->get_buffer());
|
||||
}
|
||||
|
||||
@@ -990,6 +1009,20 @@ bool ChatMessageItemContainer::OnClickChannel(GdkEventButton *ev) {
|
||||
return false;
|
||||
}
|
||||
|
||||
bool ChatMessageItemContainer::OnTextViewButtonPress(GdkEventButton *ev) {
|
||||
// run all button press handlers and propagate if none return true
|
||||
if (OnLinkClick(ev)) return true;
|
||||
if (OnClickChannel(ev)) return true;
|
||||
|
||||
if (ev->type == GDK_BUTTON_PRESS && ev->button == GDK_BUTTON_SECONDARY) {
|
||||
// send the event upward skipping TextView's handler because we dont want it
|
||||
gtk_propagate_event(GTK_WIDGET(m_main.gobj()), reinterpret_cast<GdkEvent *>(ev));
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void ChatMessageItemContainer::on_link_menu_copy() {
|
||||
Gtk::Clipboard::get()->set_text(m_selected_link);
|
||||
}
|
||||
@@ -997,8 +1030,6 @@ void ChatMessageItemContainer::on_link_menu_copy() {
|
||||
void ChatMessageItemContainer::HandleLinks(Gtk::TextView &tv) {
|
||||
const auto rgx = Glib::Regex::create(R"(\bhttps?:\/\/[^\s]+\.[^\s]+\b)");
|
||||
|
||||
tv.signal_button_press_event().connect(sigc::mem_fun(*this, &ChatMessageItemContainer::OnLinkClick), false);
|
||||
|
||||
auto buf = tv.get_buffer();
|
||||
Glib::ustring text = GetText(buf);
|
||||
|
||||
@@ -1070,18 +1101,6 @@ ChatMessageItemContainer::type_signal_action_reaction_remove ChatMessageItemCont
|
||||
return m_signal_action_reaction_remove;
|
||||
}
|
||||
|
||||
void ChatMessageItemContainer::AttachEventHandlers(Gtk::Widget &widget) {
|
||||
const auto on_button_press_event = [this](GdkEventButton *e) -> bool {
|
||||
if (e->type == GDK_BUTTON_PRESS && e->button == GDK_BUTTON_SECONDARY) {
|
||||
event(reinterpret_cast<GdkEvent *>(e)); // illegal ooooooh
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
widget.signal_button_press_event().connect(on_button_press_event, false);
|
||||
}
|
||||
|
||||
ChatMessageHeader::ChatMessageHeader(const Message &data)
|
||||
: m_main_box(Gtk::ORIENTATION_HORIZONTAL)
|
||||
, m_content_box(Gtk::ORIENTATION_VERTICAL)
|
||||
@@ -1122,7 +1141,7 @@ ChatMessageHeader::ChatMessageHeader(const Message &data)
|
||||
|
||||
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->IsABot() || 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);
|
||||
@@ -1130,7 +1149,7 @@ ChatMessageHeader::ChatMessageHeader(const Message &data)
|
||||
m_extra->set_can_focus(false);
|
||||
m_extra->set_use_markup(true);
|
||||
}
|
||||
if (author->IsBot)
|
||||
if (author->IsABot())
|
||||
m_extra->set_markup("<b>BOT</b>");
|
||||
else if (data.WebhookID.has_value())
|
||||
m_extra->set_markup("<b>Webhook</b>");
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
#include <gtkmm.h>
|
||||
#include "discord/discord.hpp"
|
||||
|
||||
class ChatMessageItemContainer : public Gtk::Box {
|
||||
class ChatMessageItemContainer : public Gtk::EventBox {
|
||||
public:
|
||||
Snowflake ID;
|
||||
Snowflake ChannelID;
|
||||
@@ -44,6 +44,7 @@ protected:
|
||||
void HandleChannelMentions(const Glib::RefPtr<Gtk::TextBuffer> &buf);
|
||||
void HandleChannelMentions(Gtk::TextView *tv);
|
||||
bool OnClickChannel(GdkEventButton *ev);
|
||||
bool OnTextViewButtonPress(GdkEventButton *ev);
|
||||
|
||||
// reused for images and links
|
||||
Gtk::Menu m_link_menu;
|
||||
@@ -57,8 +58,6 @@ protected:
|
||||
std::map<Glib::RefPtr<Gtk::TextTag>, std::string> m_link_tagmap;
|
||||
std::map<Glib::RefPtr<Gtk::TextTag>, Snowflake> m_channel_tagmap;
|
||||
|
||||
void AttachEventHandlers(Gtk::Widget &widget);
|
||||
|
||||
Gtk::EventBox *_ev;
|
||||
Gtk::Box m_main;
|
||||
Gtk::Label *m_attrib_label = nullptr;
|
||||
|
||||
@@ -4,9 +4,14 @@
|
||||
#include "ratelimitindicator.hpp"
|
||||
#include "chatinput.hpp"
|
||||
#include "chatlist.hpp"
|
||||
#include "constants.hpp"
|
||||
#ifdef WITH_LIBHANDY
|
||||
#include "channeltabswitcherhandy.hpp"
|
||||
#endif
|
||||
|
||||
ChatWindow::ChatWindow() {
|
||||
Abaddon::Get().GetDiscordClient().signal_message_send_fail().connect(sigc::mem_fun(*this, &ChatWindow::OnMessageSendFail));
|
||||
auto &discord = Abaddon::Get().GetDiscordClient();
|
||||
discord.signal_message_send_fail().connect(sigc::mem_fun(*this, &ChatWindow::OnMessageSendFail));
|
||||
|
||||
m_main = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_VERTICAL));
|
||||
m_chat = Gtk::manage(new ChatList);
|
||||
@@ -15,6 +20,13 @@ ChatWindow::ChatWindow() {
|
||||
m_rate_limit_indicator = Gtk::manage(new RateLimitIndicator);
|
||||
m_meta = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL));
|
||||
|
||||
#ifdef WITH_LIBHANDY
|
||||
m_tab_switcher = Gtk::make_managed<ChannelTabSwitcherHandy>();
|
||||
m_tab_switcher->signal_channel_switched_to().connect([this](Snowflake id) {
|
||||
m_signal_action_channel_click.emit(id, false);
|
||||
});
|
||||
#endif
|
||||
|
||||
m_rate_limit_indicator->set_margin_end(5);
|
||||
m_rate_limit_indicator->set_hexpand(true);
|
||||
m_rate_limit_indicator->set_halign(Gtk::ALIGN_END);
|
||||
@@ -35,6 +47,8 @@ ChatWindow::ChatWindow() {
|
||||
m_topic_text.set_halign(Gtk::ALIGN_START);
|
||||
m_topic_text.show();
|
||||
|
||||
m_input->set_valign(Gtk::ALIGN_END);
|
||||
|
||||
m_input->signal_submit().connect(sigc::mem_fun(*this, &ChatWindow::OnInputSubmit));
|
||||
m_input->signal_escape().connect([this]() {
|
||||
if (m_is_replying)
|
||||
@@ -44,25 +58,22 @@ ChatWindow::ChatWindow() {
|
||||
m_input->show();
|
||||
|
||||
m_completer.SetBuffer(m_input->GetBuffer());
|
||||
m_completer.SetGetChannelID([this]() -> auto {
|
||||
m_completer.SetGetChannelID([this]() {
|
||||
return m_active_channel;
|
||||
});
|
||||
|
||||
m_completer.SetGetRecentAuthors([this]() -> auto {
|
||||
m_completer.SetGetRecentAuthors([this]() {
|
||||
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_signal_action_channel_click.emit(id, true);
|
||||
});
|
||||
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);
|
||||
@@ -88,11 +99,19 @@ ChatWindow::ChatWindow() {
|
||||
m_meta->add(*m_input_indicator);
|
||||
m_meta->add(*m_rate_limit_indicator);
|
||||
// m_scroll->add(*m_list);
|
||||
#ifdef WITH_LIBHANDY
|
||||
m_main->add(*m_tab_switcher);
|
||||
m_tab_switcher->show();
|
||||
#endif
|
||||
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);
|
||||
m_main->add(m_progress);
|
||||
|
||||
m_progress.show();
|
||||
|
||||
m_main->show();
|
||||
}
|
||||
|
||||
@@ -111,10 +130,15 @@ void ChatWindow::SetMessages(const std::vector<Message> &msgs) {
|
||||
void ChatWindow::SetActiveChannel(Snowflake id) {
|
||||
m_active_channel = id;
|
||||
m_chat->SetActiveChannel(id);
|
||||
m_input->SetActiveChannel(id);
|
||||
m_input_indicator->SetActiveChannel(id);
|
||||
m_rate_limit_indicator->SetActiveChannel(id);
|
||||
if (m_is_replying)
|
||||
StopReplying();
|
||||
|
||||
#ifdef WITH_LIBHANDY
|
||||
m_tab_switcher->ReplaceActiveTab(id);
|
||||
#endif
|
||||
}
|
||||
|
||||
void ChatWindow::AddNewMessage(const Message &data) {
|
||||
@@ -150,19 +174,110 @@ void ChatWindow::SetTopic(const std::string &text) {
|
||||
m_topic.set_visible(text.length() > 0);
|
||||
}
|
||||
|
||||
void ChatWindow::AddAttachment(const Glib::RefPtr<Gio::File> &file) {
|
||||
m_input->AddAttachment(file);
|
||||
}
|
||||
|
||||
#ifdef WITH_LIBHANDY
|
||||
void ChatWindow::OpenNewTab(Snowflake id) {
|
||||
// open if its the first tab (in which case it really isnt a tab but whatever)
|
||||
if (m_tab_switcher->GetNumberOfTabs() == 0) {
|
||||
m_signal_action_channel_click.emit(id, false);
|
||||
}
|
||||
m_tab_switcher->AddChannelTab(id);
|
||||
}
|
||||
|
||||
TabsState ChatWindow::GetTabsState() {
|
||||
return m_tab_switcher->GetTabsState();
|
||||
}
|
||||
|
||||
void ChatWindow::UseTabsState(const TabsState &state) {
|
||||
m_tab_switcher->UseTabsState(state);
|
||||
}
|
||||
|
||||
void ChatWindow::GoBack() {
|
||||
m_tab_switcher->GoBackOnCurrent();
|
||||
}
|
||||
|
||||
void ChatWindow::GoForward() {
|
||||
m_tab_switcher->GoForwardOnCurrent();
|
||||
}
|
||||
|
||||
void ChatWindow::GoToPreviousTab() {
|
||||
m_tab_switcher->GoToPreviousTab();
|
||||
}
|
||||
|
||||
void ChatWindow::GoToNextTab() {
|
||||
m_tab_switcher->GoToNextTab();
|
||||
}
|
||||
|
||||
void ChatWindow::GoToTab(int idx) {
|
||||
m_tab_switcher->GoToTab(idx);
|
||||
}
|
||||
#endif
|
||||
|
||||
Snowflake ChatWindow::GetActiveChannel() const {
|
||||
return m_active_channel;
|
||||
}
|
||||
|
||||
bool ChatWindow::OnInputSubmit(const Glib::ustring &text) {
|
||||
bool ChatWindow::OnInputSubmit(ChatSubmitParams data) {
|
||||
auto &discord = Abaddon::Get().GetDiscordClient();
|
||||
if (!discord.HasSelfChannelPermission(m_active_channel, Permission::SEND_MESSAGES)) return false;
|
||||
if (!data.Attachments.empty() && !discord.HasSelfChannelPermission(m_active_channel, Permission::ATTACH_FILES)) return false;
|
||||
|
||||
int nitro_restriction = BaseAttachmentSizeLimit;
|
||||
const auto nitro = discord.GetUserData().PremiumType;
|
||||
if (!nitro.has_value() || nitro == EPremiumType::None) {
|
||||
nitro_restriction = BaseAttachmentSizeLimit;
|
||||
} else if (nitro == EPremiumType::NitroClassic) {
|
||||
nitro_restriction = NitroClassicAttachmentSizeLimit;
|
||||
} else if (nitro == EPremiumType::Nitro) {
|
||||
nitro_restriction = NitroAttachmentSizeLimit;
|
||||
}
|
||||
|
||||
int guild_restriction = BaseAttachmentSizeLimit;
|
||||
if (const auto channel = discord.GetChannel(m_active_channel); channel.has_value() && channel->GuildID.has_value()) {
|
||||
if (const auto guild = discord.GetGuild(*channel->GuildID); guild.has_value()) {
|
||||
if (!guild->PremiumTier.has_value() || guild->PremiumTier == GuildPremiumTier::NONE || guild->PremiumTier == GuildPremiumTier::TIER_1) {
|
||||
guild_restriction = BaseAttachmentSizeLimit;
|
||||
} else if (guild->PremiumTier == GuildPremiumTier::TIER_2) {
|
||||
guild_restriction = BoostLevel2AttachmentSizeLimit;
|
||||
} else if (guild->PremiumTier == GuildPremiumTier::TIER_3) {
|
||||
guild_restriction = BoostLevel3AttachmentSizeLimit;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int restriction = std::max(nitro_restriction, guild_restriction);
|
||||
|
||||
goffset total_size = 0;
|
||||
for (const auto &attachment : data.Attachments) {
|
||||
const auto info = attachment.File->query_info();
|
||||
if (info) {
|
||||
const auto size = info->get_size();
|
||||
if (size > restriction) {
|
||||
m_input->IndicateTooLarge();
|
||||
return false;
|
||||
}
|
||||
total_size += size;
|
||||
if (total_size > MaxMessagePayloadSize) {
|
||||
m_input->IndicateTooLarge();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!m_rate_limit_indicator->CanSpeak())
|
||||
return false;
|
||||
|
||||
if (text.empty())
|
||||
if (data.Message.empty() && data.Attachments.empty())
|
||||
return false;
|
||||
|
||||
data.ChannelID = m_active_channel;
|
||||
data.InReplyToID = m_replying_to;
|
||||
|
||||
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
|
||||
m_signal_action_chat_submit.emit(data); // m_replying_to is checked for invalid in the handler
|
||||
if (m_is_replying)
|
||||
StopReplying();
|
||||
|
||||
@@ -185,8 +300,7 @@ void ChatWindow::StartReplying(Snowflake message_id) {
|
||||
const auto author = discord.GetUser(message.Author.ID);
|
||||
m_replying_to = message_id;
|
||||
m_is_replying = true;
|
||||
m_input->grab_focus();
|
||||
m_input->get_style_context()->add_class("replying");
|
||||
m_input->StartReplying();
|
||||
if (author.has_value())
|
||||
m_input_indicator->SetCustomMarkup("Replying to " + author->GetEscapedBoldString<false>());
|
||||
else
|
||||
@@ -196,7 +310,7 @@ void ChatWindow::StartReplying(Snowflake message_id) {
|
||||
void ChatWindow::StopReplying() {
|
||||
m_is_replying = false;
|
||||
m_replying_to = Snowflake::Invalid;
|
||||
m_input->get_style_context()->remove_class("replying");
|
||||
m_input->StopReplying();
|
||||
m_input_indicator->ClearCustom();
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,14 @@
|
||||
#include <string>
|
||||
#include <set>
|
||||
#include "discord/discord.hpp"
|
||||
#include "discord/chatsubmitparams.hpp"
|
||||
#include "completer.hpp"
|
||||
#include "state.hpp"
|
||||
#include "progressbar.hpp"
|
||||
|
||||
#ifdef WITH_LIBHANDY
|
||||
class ChannelTabSwitcherHandy;
|
||||
#endif
|
||||
|
||||
class ChatMessageHeader;
|
||||
class ChatMessageItemContainer;
|
||||
@@ -25,10 +32,22 @@ public:
|
||||
void DeleteMessage(Snowflake id); // add [deleted] indicator
|
||||
void UpdateMessage(Snowflake id); // add [edited] indicator
|
||||
void AddNewHistory(const std::vector<Message> &msgs); // prepend messages
|
||||
void InsertChatInput(const std::string& text);
|
||||
void InsertChatInput(const std::string &text);
|
||||
Snowflake GetOldestListedMessage(); // oldest message that is currently in the ListBox
|
||||
void UpdateReactions(Snowflake id);
|
||||
void SetTopic(const std::string &text);
|
||||
void AddAttachment(const Glib::RefPtr<Gio::File> &file);
|
||||
|
||||
#ifdef WITH_LIBHANDY
|
||||
void OpenNewTab(Snowflake id);
|
||||
TabsState GetTabsState();
|
||||
void UseTabsState(const TabsState &state);
|
||||
void GoBack();
|
||||
void GoForward();
|
||||
void GoToPreviousTab();
|
||||
void GoToNextTab();
|
||||
void GoToTab(int idx);
|
||||
#endif
|
||||
|
||||
protected:
|
||||
bool m_is_replying = false;
|
||||
@@ -39,7 +58,7 @@ protected:
|
||||
|
||||
Snowflake m_active_channel;
|
||||
|
||||
bool OnInputSubmit(const Glib::ustring &text);
|
||||
bool OnInputSubmit(ChatSubmitParams data);
|
||||
|
||||
bool OnKeyPressEvent(GdkEventKey *e);
|
||||
void OnScrollEdgeOvershot(Gtk::PositionType pos);
|
||||
@@ -61,15 +80,20 @@ protected:
|
||||
ChatInputIndicator *m_input_indicator;
|
||||
RateLimitIndicator *m_rate_limit_indicator;
|
||||
Gtk::Box *m_meta;
|
||||
MessageUploadProgressBar m_progress;
|
||||
|
||||
#ifdef WITH_LIBHANDY
|
||||
ChannelTabSwitcherHandy *m_tab_switcher;
|
||||
#endif
|
||||
|
||||
public:
|
||||
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, Snowflake, Glib::ustring> type_signal_action_reaction_add;
|
||||
typedef sigc::signal<void, Snowflake, Glib::ustring> type_signal_action_reaction_remove;
|
||||
using type_signal_action_message_edit = sigc::signal<void, Snowflake, Snowflake>;
|
||||
using type_signal_action_chat_submit = sigc::signal<void, ChatSubmitParams>;
|
||||
using type_signal_action_chat_load_history = sigc::signal<void, Snowflake>;
|
||||
using type_signal_action_channel_click = sigc::signal<void, Snowflake, bool>;
|
||||
using type_signal_action_insert_mention = sigc::signal<void, 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>;
|
||||
|
||||
type_signal_action_message_edit signal_action_message_edit();
|
||||
type_signal_action_chat_submit signal_action_chat_submit();
|
||||
|
||||
@@ -160,10 +160,11 @@ void MemberList::UpdateMemberList() {
|
||||
}
|
||||
|
||||
int num_rows = 0;
|
||||
const auto guild = *discord.GetGuild(m_guild_id);
|
||||
const auto guild = discord.GetGuild(m_guild_id);
|
||||
if (!guild.has_value()) return;
|
||||
auto add_user = [this, &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);
|
||||
|
||||
24
src/components/progressbar.cpp
Normal file
24
src/components/progressbar.cpp
Normal file
@@ -0,0 +1,24 @@
|
||||
#include "progressbar.hpp"
|
||||
#include "abaddon.hpp"
|
||||
|
||||
MessageUploadProgressBar::MessageUploadProgressBar() {
|
||||
get_style_context()->add_class("message-progress");
|
||||
auto &discord = Abaddon::Get().GetDiscordClient();
|
||||
discord.signal_message_progress().connect([this](const std::string &nonce, float percent) {
|
||||
if (nonce == m_last_nonce) {
|
||||
set_fraction(percent);
|
||||
}
|
||||
});
|
||||
discord.signal_message_send_fail().connect([this](const std::string &nonce, float) {
|
||||
if (nonce == m_last_nonce)
|
||||
set_fraction(0.0);
|
||||
});
|
||||
discord.signal_message_create().connect([this](const Message &msg) {
|
||||
if (msg.IsPending) {
|
||||
m_last_nonce = *msg.Nonce;
|
||||
} else if (msg.Nonce.has_value() && (*msg.Nonce == m_last_nonce)) {
|
||||
m_last_nonce = "";
|
||||
set_fraction(0.0);
|
||||
}
|
||||
});
|
||||
}
|
||||
11
src/components/progressbar.hpp
Normal file
11
src/components/progressbar.hpp
Normal file
@@ -0,0 +1,11 @@
|
||||
#pragma once
|
||||
#include <gtkmm/progressbar.h>
|
||||
#include <string>
|
||||
|
||||
class MessageUploadProgressBar : public Gtk::ProgressBar {
|
||||
public:
|
||||
MessageUploadProgressBar();
|
||||
|
||||
private:
|
||||
std::string m_last_nonce;
|
||||
};
|
||||
@@ -1,4 +1,12 @@
|
||||
#pragma once
|
||||
#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
|
||||
constexpr static int AttachmentItemSize = 120;
|
||||
constexpr static int BaseAttachmentSizeLimit = 8 * 1024 * 1024;
|
||||
constexpr static int NitroClassicAttachmentSizeLimit = 50 * 1024 * 1024;
|
||||
constexpr static int NitroAttachmentSizeLimit = 100 * 1024 * 1024;
|
||||
constexpr static int BoostLevel2AttachmentSizeLimit = 50 * 1024 * 1024;
|
||||
constexpr static int BoostLevel3AttachmentSizeLimit = 100 * 1024 * 1024;
|
||||
constexpr static int MaxMessagePayloadSize = 199 * 1024 * 1024;
|
||||
|
||||
@@ -34,3 +34,7 @@ ConfirmDialog::ConfirmDialog(Gtk::Window &parent)
|
||||
void ConfirmDialog::SetConfirmText(const Glib::ustring &text) {
|
||||
m_label.set_text(text);
|
||||
}
|
||||
|
||||
void ConfirmDialog::SetAcceptOnly(bool accept_only) {
|
||||
m_cancel.set_visible(!accept_only);
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ class ConfirmDialog : public Gtk::Dialog {
|
||||
public:
|
||||
ConfirmDialog(Gtk::Window &parent);
|
||||
void SetConfirmText(const Glib::ustring &text);
|
||||
void SetAcceptOnly(bool accept_only);
|
||||
|
||||
protected:
|
||||
Gtk::Label m_label;
|
||||
|
||||
@@ -1,97 +0,0 @@
|
||||
#include "joinguild.hpp"
|
||||
#include "abaddon.hpp"
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <regex>
|
||||
|
||||
JoinGuildDialog::JoinGuildDialog(Gtk::Window &parent)
|
||||
: Gtk::Dialog("Join Server", parent, true)
|
||||
, m_layout(Gtk::ORIENTATION_VERTICAL)
|
||||
, m_ok("OK")
|
||||
, m_cancel("Cancel")
|
||||
, m_info("Enter code") {
|
||||
set_default_size(300, 50);
|
||||
get_style_context()->add_class("app-window");
|
||||
get_style_context()->add_class("app-popup");
|
||||
|
||||
Glib::signal_idle().connect(sigc::mem_fun(*this, &JoinGuildDialog::on_idle_slot));
|
||||
|
||||
m_entry.signal_changed().connect(sigc::mem_fun(*this, &JoinGuildDialog::on_entry_changed));
|
||||
|
||||
m_ok.set_sensitive(false);
|
||||
|
||||
m_ok.signal_clicked().connect([&]() {
|
||||
response(Gtk::RESPONSE_OK);
|
||||
});
|
||||
|
||||
m_cancel.signal_clicked().connect([&]() {
|
||||
response(Gtk::RESPONSE_CANCEL);
|
||||
});
|
||||
|
||||
m_entry.set_hexpand(true);
|
||||
m_layout.add(m_entry);
|
||||
m_lower.set_hexpand(true);
|
||||
m_lower.pack_start(m_info);
|
||||
m_info.set_halign(Gtk::ALIGN_START);
|
||||
m_lower.pack_start(m_ok, Gtk::PACK_SHRINK);
|
||||
m_lower.pack_start(m_cancel, Gtk::PACK_SHRINK);
|
||||
m_ok.set_halign(Gtk::ALIGN_END);
|
||||
m_cancel.set_halign(Gtk::ALIGN_END);
|
||||
m_layout.add(m_lower);
|
||||
get_content_area()->add(m_layout);
|
||||
|
||||
show_all_children();
|
||||
}
|
||||
|
||||
void JoinGuildDialog::on_entry_changed() {
|
||||
std::string s = m_entry.get_text();
|
||||
std::regex invite_regex(R"((https?:\/\/)?discord\.(gg(\/invite)?\/|com\/invite\/)([A-Za-z0-9\-]+))", std::regex_constants::ECMAScript);
|
||||
std::smatch match;
|
||||
bool full_url = std::regex_search(s, match, invite_regex);
|
||||
if (full_url || IsCode(s)) {
|
||||
m_code = full_url ? match[4].str() : s;
|
||||
m_needs_request = true;
|
||||
m_ok.set_sensitive(false);
|
||||
} else {
|
||||
m_ok.set_sensitive(false);
|
||||
}
|
||||
}
|
||||
|
||||
void JoinGuildDialog::CheckCode() {
|
||||
auto cb = [this](const std::optional<InviteData> &invite) {
|
||||
if (invite.has_value()) {
|
||||
m_ok.set_sensitive(true);
|
||||
if (invite->Guild.has_value()) {
|
||||
if (invite->MemberCount.has_value())
|
||||
m_info.set_text(invite->Guild->Name + " (" + std::to_string(*invite->MemberCount) + " members)");
|
||||
else
|
||||
m_info.set_text(invite->Guild->Name);
|
||||
} else {
|
||||
m_info.set_text("Group DM (" + std::to_string(*invite->MemberCount) + " members)");
|
||||
}
|
||||
} else {
|
||||
m_ok.set_sensitive(false);
|
||||
m_info.set_text("Invalid invite");
|
||||
}
|
||||
};
|
||||
Abaddon::Get().GetDiscordClient().FetchInvite(m_code, sigc::track_obj(cb, *this));
|
||||
}
|
||||
|
||||
bool JoinGuildDialog::IsCode(std::string str) {
|
||||
return str.length() >= 2 && std::all_of(str.begin(), str.end(), [](char c) -> bool { return std::isalnum(c) || c == '-'; });
|
||||
}
|
||||
|
||||
std::string JoinGuildDialog::GetCode() {
|
||||
return m_code;
|
||||
}
|
||||
|
||||
static const constexpr int RateLimitMS = 1500;
|
||||
bool JoinGuildDialog::on_idle_slot() {
|
||||
const auto now = std::chrono::steady_clock::now();
|
||||
if (m_needs_request && ((now - m_last_req_time) > std::chrono::milliseconds(RateLimitMS))) {
|
||||
m_needs_request = false;
|
||||
m_last_req_time = now;
|
||||
CheckCode();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
#pragma once
|
||||
#include <gtkmm.h>
|
||||
#include <string>
|
||||
#include <chrono>
|
||||
|
||||
class JoinGuildDialog : public Gtk::Dialog {
|
||||
public:
|
||||
JoinGuildDialog(Gtk::Window &parent);
|
||||
std::string GetCode();
|
||||
|
||||
protected:
|
||||
void on_entry_changed();
|
||||
static bool IsCode(std::string str);
|
||||
|
||||
Gtk::Box m_layout;
|
||||
Gtk::Button m_ok;
|
||||
Gtk::Button m_cancel;
|
||||
Gtk::Box m_lower;
|
||||
Gtk::Label m_info;
|
||||
Gtk::Entry m_entry;
|
||||
|
||||
void CheckCode();
|
||||
|
||||
// needs a rate limit cuz if u hit it u get ip banned from /invites for a long time :(
|
||||
bool m_needs_request = false;
|
||||
std::chrono::time_point<std::chrono::steady_clock> m_last_req_time;
|
||||
bool on_idle_slot();
|
||||
|
||||
private:
|
||||
std::string m_code;
|
||||
};
|
||||
26
src/dialogs/textinput.cpp
Normal file
26
src/dialogs/textinput.cpp
Normal file
@@ -0,0 +1,26 @@
|
||||
#include "textinput.hpp"
|
||||
|
||||
TextInputDialog::TextInputDialog(const Glib::ustring &prompt, const Glib::ustring &title, const Glib::ustring &placeholder, Gtk::Window &parent)
|
||||
: Gtk::Dialog(title, parent, true)
|
||||
, m_label(prompt) {
|
||||
get_style_context()->add_class("app-window");
|
||||
get_style_context()->add_class("app-popup");
|
||||
|
||||
auto ok = add_button("OK", Gtk::RESPONSE_OK);
|
||||
auto cancel = add_button("Cancel", Gtk::RESPONSE_CANCEL);
|
||||
|
||||
get_content_area()->add(m_label);
|
||||
get_content_area()->add(m_entry);
|
||||
|
||||
m_entry.set_text(placeholder);
|
||||
|
||||
m_entry.set_activates_default(true);
|
||||
ok->set_can_default(true);
|
||||
ok->grab_default();
|
||||
|
||||
show_all_children();
|
||||
}
|
||||
|
||||
Glib::ustring TextInputDialog::GetInput() const {
|
||||
return m_entry.get_text();
|
||||
}
|
||||
14
src/dialogs/textinput.hpp
Normal file
14
src/dialogs/textinput.hpp
Normal file
@@ -0,0 +1,14 @@
|
||||
#pragma once
|
||||
#include <gtkmm/dialog.h>
|
||||
#include <gtkmm/entry.h>
|
||||
|
||||
class TextInputDialog : public Gtk::Dialog {
|
||||
public:
|
||||
TextInputDialog(const Glib::ustring &prompt, const Glib::ustring &title, const Glib::ustring &placeholder, Gtk::Window &parent);
|
||||
|
||||
Glib::ustring GetInput() const;
|
||||
|
||||
private:
|
||||
Gtk::Label m_label;
|
||||
Gtk::Entry m_entry;
|
||||
};
|
||||
@@ -89,7 +89,28 @@ bool ChannelData::HasIcon() const noexcept {
|
||||
}
|
||||
|
||||
std::string ChannelData::GetIconURL() const {
|
||||
return "https://cdn.discordapp.com/channel-icons/" + std::to_string(ID) + "/" + *Icon + ".png";
|
||||
if (HasIcon()) {
|
||||
return "https://cdn.discordapp.com/channel-icons/" + std::to_string(ID) + "/" + *Icon + ".png";
|
||||
} else {
|
||||
const auto recipients = GetDMRecipients();
|
||||
if (!recipients.empty())
|
||||
return recipients[0].GetAvatarURL("png", "32");
|
||||
else
|
||||
return "https://cdn.discordapp.com/embed/avatars/0.png";
|
||||
}
|
||||
}
|
||||
|
||||
std::string ChannelData::GetDisplayName() const {
|
||||
if (Name.has_value()) {
|
||||
return "#" + *Name;
|
||||
} else {
|
||||
const auto recipients = GetDMRecipients();
|
||||
if (Type == ChannelType::DM && !recipients.empty())
|
||||
return recipients[0].Username;
|
||||
else if (Type == ChannelType::GROUP_DM)
|
||||
return std::to_string(recipients.size()) + " members";
|
||||
}
|
||||
return "Unknown";
|
||||
}
|
||||
|
||||
std::vector<Snowflake> ChannelData::GetChildIDs() const {
|
||||
|
||||
@@ -102,6 +102,7 @@ struct ChannelData {
|
||||
[[nodiscard]] bool IsText() const noexcept;
|
||||
[[nodiscard]] bool HasIcon() const noexcept;
|
||||
[[nodiscard]] std::string GetIconURL() const;
|
||||
[[nodiscard]] std::string GetDisplayName() const;
|
||||
[[nodiscard]] std::vector<Snowflake> GetChildIDs() const;
|
||||
[[nodiscard]] std::optional<PermissionOverwrite> GetOverwrite(Snowflake id) const;
|
||||
[[nodiscard]] std::vector<UserData> GetDMRecipients() const;
|
||||
|
||||
24
src/discord/chatsubmitparams.hpp
Normal file
24
src/discord/chatsubmitparams.hpp
Normal file
@@ -0,0 +1,24 @@
|
||||
#pragma once
|
||||
#include <vector>
|
||||
#include <string>
|
||||
#include <glibmm/ustring.h>
|
||||
#include <giomm/file.h>
|
||||
#include "discord/snowflake.hpp"
|
||||
|
||||
struct ChatSubmitParams {
|
||||
enum AttachmentType {
|
||||
PastedImage,
|
||||
ExtantFile,
|
||||
};
|
||||
|
||||
struct Attachment {
|
||||
Glib::RefPtr<Gio::File> File;
|
||||
AttachmentType Type;
|
||||
std::string Filename;
|
||||
};
|
||||
|
||||
Snowflake ChannelID;
|
||||
Snowflake InReplyToID;
|
||||
Glib::ustring Message;
|
||||
std::vector<Attachment> Attachments;
|
||||
};
|
||||
@@ -30,6 +30,7 @@ void DiscordClient::Start() {
|
||||
if (m_client_started) return;
|
||||
|
||||
m_http.SetBase(GetAPIURL());
|
||||
SetHeaders();
|
||||
|
||||
std::memset(&m_zstream, 0, sizeof(m_zstream));
|
||||
inflateInit2(&m_zstream, MAX_WBITS + 32);
|
||||
@@ -41,7 +42,7 @@ void DiscordClient::Start() {
|
||||
m_websocket.StartConnection(GetGatewayURL());
|
||||
}
|
||||
|
||||
void DiscordClient::Stop() {
|
||||
bool DiscordClient::Stop() {
|
||||
if (m_client_started) {
|
||||
inflateEnd(&m_zstream);
|
||||
m_compressed_buf.clear();
|
||||
@@ -55,9 +56,15 @@ void DiscordClient::Stop() {
|
||||
m_guild_to_users.clear();
|
||||
|
||||
m_websocket.Stop();
|
||||
|
||||
m_client_started = false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
m_client_started = false;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool DiscordClient::IsStarted() const {
|
||||
@@ -312,6 +319,10 @@ bool DiscordClient::HasGuildPermission(Snowflake user_id, Snowflake guild_id, Pe
|
||||
return (base & perm) == perm;
|
||||
}
|
||||
|
||||
bool DiscordClient::HasSelfChannelPermission(Snowflake channel_id, Permission perm) const {
|
||||
return HasChannelPermission(m_user_data.ID, channel_id, perm);
|
||||
}
|
||||
|
||||
bool DiscordClient::HasAnyChannelPermission(Snowflake user_id, Snowflake channel_id, Permission perm) const {
|
||||
const auto channel = m_store.GetChannel(channel_id);
|
||||
if (!channel.has_value() || !channel->GuildID.has_value()) return false;
|
||||
@@ -403,7 +414,7 @@ bool DiscordClient::CanManageMember(Snowflake guild_id, Snowflake actor, Snowfla
|
||||
return actor_highest->Position > target_highest->Position;
|
||||
}
|
||||
|
||||
void DiscordClient::ChatMessageCallback(const std::string &nonce, const http::response_type &response) {
|
||||
void DiscordClient::ChatMessageCallback(const std::string &nonce, const http::response_type &response, const sigc::slot<void(DiscordError)> &callback) {
|
||||
if (!CheckCode(response)) {
|
||||
if (response.status_code == http::TooManyRequests) {
|
||||
try { // not sure if this body is guaranteed
|
||||
@@ -415,54 +426,108 @@ void DiscordClient::ChatMessageCallback(const std::string &nonce, const http::re
|
||||
} else {
|
||||
m_signal_message_send_fail.emit(nonce, 0);
|
||||
}
|
||||
|
||||
// todo actually callback with correct error code (not necessary rn)
|
||||
callback(DiscordError::GENERIC);
|
||||
} else {
|
||||
callback(DiscordError::NONE);
|
||||
}
|
||||
}
|
||||
|
||||
void DiscordClient::SendChatMessage(const std::string &content, Snowflake channel) {
|
||||
// @([^@#]{1,32})#(\\d{4})
|
||||
void DiscordClient::SendChatMessageNoAttachments(const ChatSubmitParams ¶ms, const sigc::slot<void(DiscordError)> &callback) {
|
||||
const auto nonce = std::to_string(Snowflake::FromNow());
|
||||
|
||||
CreateMessageObject obj;
|
||||
obj.Content = content;
|
||||
obj.Content = params.Message;
|
||||
obj.Nonce = nonce;
|
||||
m_http.MakePOST("/channels/" + std::to_string(channel) + "/messages", nlohmann::json(obj).dump(), sigc::bind<0>(sigc::mem_fun(*this, &DiscordClient::ChatMessageCallback), nonce));
|
||||
// dummy data so the content can be shown while waiting for MESSAGE_CREATE
|
||||
if (params.InReplyToID.IsValid())
|
||||
obj.MessageReference.emplace().MessageID = params.InReplyToID;
|
||||
|
||||
m_http.MakePOST("/channels/" + std::to_string(params.ChannelID) + "/messages",
|
||||
nlohmann::json(obj).dump(),
|
||||
[this, nonce, callback](const http::response_type &r) {
|
||||
ChatMessageCallback(nonce, r, callback);
|
||||
});
|
||||
|
||||
// dummy preview data
|
||||
Message tmp;
|
||||
tmp.Content = content;
|
||||
tmp.Content = params.Message;
|
||||
tmp.ID = nonce;
|
||||
tmp.ChannelID = channel;
|
||||
tmp.ChannelID = params.ChannelID;
|
||||
tmp.Author = GetUserData();
|
||||
tmp.IsTTS = false;
|
||||
tmp.DoesMentionEveryone = false;
|
||||
tmp.Type = MessageType::DEFAULT;
|
||||
tmp.IsPinned = false;
|
||||
tmp.Timestamp = "2000-01-01T00:00:00.000000+00:00";
|
||||
tmp.Nonce = obj.Nonce;
|
||||
tmp.Nonce = nonce;
|
||||
tmp.IsPending = true;
|
||||
|
||||
m_store.SetMessage(tmp.ID, tmp);
|
||||
m_signal_message_sent.emit(tmp);
|
||||
m_signal_message_create.emit(tmp);
|
||||
}
|
||||
|
||||
void DiscordClient::SendChatMessage(const std::string &content, Snowflake channel, Snowflake referenced_message) {
|
||||
void DiscordClient::SendChatMessageAttachments(const ChatSubmitParams ¶ms, const sigc::slot<void(DiscordError)> &callback) {
|
||||
const auto nonce = std::to_string(Snowflake::FromNow());
|
||||
|
||||
CreateMessageObject obj;
|
||||
obj.Content = content;
|
||||
obj.Content = params.Message;
|
||||
obj.Nonce = nonce;
|
||||
obj.MessageReference.emplace().MessageID = referenced_message;
|
||||
m_http.MakePOST("/channels/" + std::to_string(channel) + "/messages", nlohmann::json(obj).dump(), sigc::bind<0>(sigc::mem_fun(*this, &DiscordClient::ChatMessageCallback), nonce));
|
||||
if (params.InReplyToID.IsValid())
|
||||
obj.MessageReference.emplace().MessageID = params.InReplyToID;
|
||||
|
||||
auto req = m_http.CreateRequest(http::REQUEST_POST, "/channels/" + std::to_string(params.ChannelID) + "/messages");
|
||||
m_progress_cb_timer.start();
|
||||
req.set_progress_callback([this, nonce](curl_off_t ultotal, curl_off_t ulnow) {
|
||||
if (m_progress_cb_timer.elapsed() < 0.0417) return; // try to prevent it from blocking ui
|
||||
m_progress_cb_timer.start();
|
||||
m_generic_mutex.lock();
|
||||
m_generic_queue.push([this, nonce, ultotal, ulnow] {
|
||||
m_signal_message_progress.emit(
|
||||
nonce,
|
||||
static_cast<float>(ulnow) / static_cast<float>(ultotal));
|
||||
});
|
||||
m_generic_mutex.unlock();
|
||||
m_generic_dispatch.emit();
|
||||
});
|
||||
req.make_form();
|
||||
req.add_field("payload_json", nlohmann::json(obj).dump().c_str(), CURL_ZERO_TERMINATED);
|
||||
for (size_t i = 0; i < params.Attachments.size(); i++) {
|
||||
const auto field_name = "files[" + std::to_string(i) + "]";
|
||||
req.add_file(field_name, params.Attachments.at(i).File, params.Attachments.at(i).Filename);
|
||||
}
|
||||
m_http.Execute(std::move(req), [this, params, nonce, callback](const http::response_type &res) {
|
||||
for (const auto &attachment : params.Attachments) {
|
||||
if (attachment.Type == ChatSubmitParams::AttachmentType::PastedImage) {
|
||||
attachment.File->remove();
|
||||
}
|
||||
}
|
||||
ChatMessageCallback(nonce, res, callback);
|
||||
});
|
||||
|
||||
// dummy preview data
|
||||
Message tmp;
|
||||
tmp.Content = content;
|
||||
tmp.Content = params.Message;
|
||||
tmp.ID = nonce;
|
||||
tmp.ChannelID = channel;
|
||||
tmp.ChannelID = params.ChannelID;
|
||||
tmp.Author = GetUserData();
|
||||
tmp.IsTTS = false;
|
||||
tmp.DoesMentionEveryone = false;
|
||||
tmp.Type = MessageType::DEFAULT;
|
||||
tmp.IsPinned = false;
|
||||
tmp.Timestamp = "2000-01-01T00:00:00.000000+00:00";
|
||||
tmp.Nonce = obj.Nonce;
|
||||
tmp.Nonce = nonce;
|
||||
tmp.IsPending = true;
|
||||
|
||||
m_store.SetMessage(tmp.ID, tmp);
|
||||
m_signal_message_sent.emit(tmp);
|
||||
m_signal_message_create.emit(tmp);
|
||||
}
|
||||
|
||||
void DiscordClient::SendChatMessage(const ChatSubmitParams ¶ms, const sigc::slot<void(DiscordError)> &callback) {
|
||||
if (params.Attachments.empty())
|
||||
SendChatMessageNoAttachments(params, callback);
|
||||
else
|
||||
SendChatMessageAttachments(params, callback);
|
||||
}
|
||||
|
||||
void DiscordClient::DeleteMessage(Snowflake channel_id, Snowflake id) {
|
||||
@@ -508,10 +573,6 @@ void DiscordClient::SendThreadLazyLoad(Snowflake id) {
|
||||
m_websocket.Send(msg);
|
||||
}
|
||||
|
||||
void DiscordClient::JoinGuild(const std::string &code) {
|
||||
m_http.MakePOST("/invites/" + code, "{}", [](auto) {});
|
||||
}
|
||||
|
||||
void DiscordClient::LeaveGuild(Snowflake id) {
|
||||
m_http.MakeDELETE("/users/@me/guilds/" + std::to_string(id), [](auto) {});
|
||||
}
|
||||
@@ -546,19 +607,6 @@ void DiscordClient::UpdateStatus(PresenceStatus status, bool is_afk, const Activ
|
||||
m_signal_presence_update.emit(GetUserData(), status);
|
||||
}
|
||||
|
||||
void DiscordClient::CreateDM(Snowflake user_id, const sigc::slot<void(DiscordError code, Snowflake channel_id)> &callback) {
|
||||
CreateDMObject obj;
|
||||
obj.Recipients.push_back(user_id);
|
||||
m_http.MakePOST("/users/@me/channels", nlohmann::json(obj).dump(), [callback](const http::response &response) {
|
||||
if (!CheckCode(response)) {
|
||||
callback(DiscordError::NONE, Snowflake::Invalid);
|
||||
return;
|
||||
}
|
||||
auto channel = nlohmann::json::parse(response.text).get<ChannelData>();
|
||||
callback(GetCodeFromResponse(response), channel.ID);
|
||||
});
|
||||
}
|
||||
|
||||
void DiscordClient::CloseDM(Snowflake channel_id) {
|
||||
m_http.MakeDELETE("/channels/" + std::to_string(channel_id), [](const http::response &response) {
|
||||
CheckCode(response);
|
||||
@@ -1117,6 +1165,33 @@ void DiscordClient::AcceptVerificationGate(Snowflake guild_id, VerificationGateI
|
||||
});
|
||||
}
|
||||
|
||||
void DiscordClient::SetReferringChannel(Snowflake id) {
|
||||
if (!id.IsValid()) {
|
||||
m_http.SetPersistentHeader("Referer", "https://discord.com/channels/@me");
|
||||
} else {
|
||||
const auto channel = GetChannel(id);
|
||||
if (channel.has_value()) {
|
||||
if (channel->IsDM()) {
|
||||
m_http.SetPersistentHeader("Referer", "https://discord.com/channels/@me/" + std::to_string(id));
|
||||
} else if (channel->GuildID.has_value()) {
|
||||
m_http.SetPersistentHeader("Referer", "https://discord.com/channels/" + std::to_string(*channel->GuildID) + "/" + std::to_string(id));
|
||||
} else {
|
||||
m_http.SetPersistentHeader("Referer", "https://discord.com/channels/@me");
|
||||
}
|
||||
} else {
|
||||
m_http.SetPersistentHeader("Referer", "https://discord.com/channels/@me");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void DiscordClient::SetBuildNumber(uint32_t build_number) {
|
||||
m_build_number = build_number;
|
||||
}
|
||||
|
||||
void DiscordClient::SetCookie(std::string_view cookie) {
|
||||
m_http.SetCookie(cookie);
|
||||
}
|
||||
|
||||
void DiscordClient::UpdateToken(const std::string &token) {
|
||||
if (!IsStarted()) {
|
||||
m_token = token;
|
||||
@@ -1622,18 +1697,27 @@ void DiscordClient::HandleGatewayChannelDelete(const GatewayMessage &msg) {
|
||||
it->second.erase(id);
|
||||
m_store.ClearChannel(id);
|
||||
m_signal_channel_delete.emit(id);
|
||||
m_signal_channel_accessibility_changed.emit(id, false);
|
||||
}
|
||||
|
||||
void DiscordClient::HandleGatewayChannelUpdate(const GatewayMessage &msg) {
|
||||
const auto id = msg.Data.at("id").get<Snowflake>();
|
||||
auto cur = m_store.GetChannel(id);
|
||||
if (cur.has_value()) {
|
||||
const bool old_perms = HasChannelPermission(m_user_data.ID, id, Permission::VIEW_CHANNEL);
|
||||
|
||||
cur->update_from_json(msg.Data);
|
||||
m_store.SetChannel(id, *cur);
|
||||
if (cur->PermissionOverwrites.has_value())
|
||||
for (const auto &p : *cur->PermissionOverwrites)
|
||||
m_store.SetPermissionOverwrite(id, p.ID, p);
|
||||
m_signal_channel_update.emit(id);
|
||||
|
||||
const bool new_perms = HasChannelPermission(m_user_data.ID, id, Permission::VIEW_CHANNEL);
|
||||
if (old_perms && !new_perms)
|
||||
m_signal_channel_accessibility_changed.emit(id, false);
|
||||
else if (!old_perms && new_perms)
|
||||
m_signal_channel_accessibility_changed.emit(id, true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1660,29 +1744,37 @@ void DiscordClient::HandleGatewayGuildUpdate(const GatewayMessage &msg) {
|
||||
|
||||
void DiscordClient::HandleGatewayGuildRoleUpdate(const GatewayMessage &msg) {
|
||||
GuildRoleUpdateObject data = msg.Data;
|
||||
|
||||
const auto channels = GetChannelsInGuild(data.GuildID);
|
||||
std::unordered_set<Snowflake> accessible;
|
||||
for (auto channel : channels) {
|
||||
if (HasChannelPermission(m_user_data.ID, channel, Permission::VIEW_CHANNEL))
|
||||
accessible.insert(channel);
|
||||
}
|
||||
|
||||
m_store.SetRole(data.GuildID, data.Role);
|
||||
m_signal_role_update.emit(data.GuildID, data.Role.ID);
|
||||
|
||||
for (auto channel : channels) {
|
||||
const bool old_perms = accessible.find(channel) != accessible.end();
|
||||
const bool new_perms = HasChannelPermission(m_user_data.ID, channel, Permission::VIEW_CHANNEL);
|
||||
if (old_perms && !new_perms) {
|
||||
m_signal_channel_accessibility_changed.emit(channel, false);
|
||||
} else if (!old_perms && new_perms) {
|
||||
m_signal_channel_accessibility_changed.emit(channel, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void DiscordClient::HandleGatewayGuildRoleCreate(const GatewayMessage &msg) {
|
||||
GuildRoleCreateObject data = msg.Data;
|
||||
auto guild = *m_store.GetGuild(data.GuildID);
|
||||
guild.Roles->push_back(data.Role);
|
||||
m_store.BeginTransaction();
|
||||
m_store.SetRole(guild.ID, data.Role);
|
||||
m_store.SetGuild(guild.ID, guild);
|
||||
m_store.EndTransaction();
|
||||
m_store.SetRole(data.GuildID, data.Role);
|
||||
m_signal_role_create.emit(data.GuildID, data.Role.ID);
|
||||
}
|
||||
|
||||
void DiscordClient::HandleGatewayGuildRoleDelete(const GatewayMessage &msg) {
|
||||
GuildRoleDeleteObject data = msg.Data;
|
||||
auto guild = *m_store.GetGuild(data.GuildID);
|
||||
const auto pred = [id = data.RoleID](const RoleData &role) -> bool {
|
||||
return role.ID == id;
|
||||
};
|
||||
guild.Roles->erase(std::remove_if(guild.Roles->begin(), guild.Roles->end(), pred), guild.Roles->end());
|
||||
m_store.SetGuild(guild.ID, guild);
|
||||
m_store.ClearRole(data.RoleID);
|
||||
m_signal_role_delete.emit(data.GuildID, data.RoleID);
|
||||
}
|
||||
|
||||
@@ -2152,7 +2244,7 @@ void DiscordClient::HandleGatewayGuildCreate(const GatewayMessage &msg) {
|
||||
|
||||
void DiscordClient::HandleGatewayGuildDelete(const GatewayMessage &msg) {
|
||||
Snowflake id = msg.Data.at("id");
|
||||
bool unavailable = msg.Data.contains("unavilable") && msg.Data.at("unavailable").get<bool>();
|
||||
bool unavailable = msg.Data.contains("unavailable") && msg.Data.at("unavailable").get<bool>();
|
||||
|
||||
if (unavailable)
|
||||
printf("guild %" PRIu64 " became unavailable\n", static_cast<uint64_t>(id));
|
||||
@@ -2165,9 +2257,12 @@ void DiscordClient::HandleGatewayGuildDelete(const GatewayMessage &msg) {
|
||||
}
|
||||
|
||||
m_store.ClearGuild(id);
|
||||
if (guild->Channels.has_value())
|
||||
for (const auto &c : *guild->Channels)
|
||||
if (guild->Channels.has_value()) {
|
||||
for (const auto &c : *guild->Channels) {
|
||||
m_store.ClearChannel(c.ID);
|
||||
m_signal_channel_accessibility_changed.emit(c.ID, false);
|
||||
}
|
||||
}
|
||||
|
||||
m_signal_guild_delete.emit(id);
|
||||
}
|
||||
@@ -2210,7 +2305,7 @@ void DiscordClient::HeartbeatThread() {
|
||||
void DiscordClient::SendIdentify() {
|
||||
IdentifyMessage msg;
|
||||
msg.Token = m_token;
|
||||
msg.Capabilities = 125; // no idea what this is
|
||||
msg.Capabilities = 509; // no idea what this is
|
||||
msg.Properties.OS = "Windows";
|
||||
msg.Properties.Browser = "Chrome";
|
||||
msg.Properties.Device = "";
|
||||
@@ -2223,7 +2318,7 @@ void DiscordClient::SendIdentify() {
|
||||
msg.Properties.ReferrerCurrent = "";
|
||||
msg.Properties.ReferringDomainCurrent = "";
|
||||
msg.Properties.ReleaseChannel = "stable";
|
||||
msg.Properties.ClientBuildNumber = 105691;
|
||||
msg.Properties.ClientBuildNumber = m_build_number;
|
||||
msg.Properties.ClientEventSource = "";
|
||||
msg.Presence.Status = "online";
|
||||
msg.Presence.Since = 0;
|
||||
@@ -2232,6 +2327,7 @@ void DiscordClient::SendIdentify() {
|
||||
msg.ClientState.HighestLastMessageID = "0";
|
||||
msg.ClientState.ReadStateVersion = 0;
|
||||
msg.ClientState.UserGuildSettingsVersion = -1;
|
||||
SetSuperPropertiesFromIdentity(msg);
|
||||
const bool b = m_websocket.GetPrintMessages();
|
||||
m_websocket.SetPrintMessages(false);
|
||||
m_websocket.Send(msg);
|
||||
@@ -2246,6 +2342,36 @@ void DiscordClient::SendResume() {
|
||||
m_websocket.Send(msg);
|
||||
}
|
||||
|
||||
void DiscordClient::SetHeaders() {
|
||||
m_http.SetPersistentHeader("Sec-Fetch-Dest", "empty");
|
||||
m_http.SetPersistentHeader("Sec-Fetch-Mode", "cors");
|
||||
m_http.SetPersistentHeader("Sec-Fetch-Site", "same-origin");
|
||||
m_http.SetPersistentHeader("X-Debug-Options", "bugReporterEnabled");
|
||||
m_http.SetPersistentHeader("Accept-Language", "en-US,en;q=0.9");
|
||||
|
||||
SetReferringChannel(Snowflake::Invalid);
|
||||
}
|
||||
|
||||
void DiscordClient::SetSuperPropertiesFromIdentity(const IdentifyMessage &identity) {
|
||||
nlohmann::ordered_json j;
|
||||
j["os"] = identity.Properties.OS;
|
||||
j["browser"] = identity.Properties.Browser;
|
||||
j["device"] = identity.Properties.Device;
|
||||
j["system_locale"] = identity.Properties.SystemLocale;
|
||||
j["browser_user_agent"] = identity.Properties.BrowserUserAgent;
|
||||
j["browser_version"] = identity.Properties.BrowserVersion;
|
||||
j["os_version"] = identity.Properties.OSVersion;
|
||||
j["referrer"] = identity.Properties.Referrer;
|
||||
j["referring_domain"] = identity.Properties.ReferringDomain;
|
||||
j["referrer_current"] = identity.Properties.ReferrerCurrent;
|
||||
j["referring_domain_current"] = identity.Properties.ReferringDomainCurrent;
|
||||
j["release_channel"] = identity.Properties.ReleaseChannel;
|
||||
j["client_build_number"] = identity.Properties.ClientBuildNumber;
|
||||
j["client_event_source"] = nullptr; // probably will never be non-null ("") anyways
|
||||
m_http.SetPersistentHeader("X-Super-Properties", Glib::Base64::encode(j.dump()));
|
||||
m_http.SetPersistentHeader("X-Discord-Locale", identity.Properties.SystemLocale);
|
||||
}
|
||||
|
||||
void DiscordClient::HandleSocketOpen() {
|
||||
}
|
||||
|
||||
@@ -2311,9 +2437,11 @@ void DiscordClient::StoreMessageData(Message &msg) {
|
||||
if (msg.Member.has_value())
|
||||
m_store.SetGuildMember(*msg.GuildID, msg.Author.ID, *msg.Member);
|
||||
|
||||
if (msg.Interaction.has_value() && msg.Interaction->Member.has_value()) {
|
||||
if (msg.Interaction.has_value()) {
|
||||
m_store.SetUser(msg.Interaction->User.ID, msg.Interaction->User);
|
||||
m_store.SetGuildMember(*msg.GuildID, msg.Interaction->User.ID, *msg.Interaction->Member);
|
||||
if (msg.Interaction->Member.has_value()) {
|
||||
m_store.SetGuildMember(*msg.GuildID, msg.Interaction->User.ID, *msg.Interaction->Member);
|
||||
}
|
||||
}
|
||||
|
||||
m_store.EndTransaction();
|
||||
@@ -2513,6 +2641,10 @@ DiscordClient::type_signal_connected DiscordClient::signal_connected() {
|
||||
return m_signal_connected;
|
||||
}
|
||||
|
||||
DiscordClient::type_signal_message_progress DiscordClient::signal_message_progress() {
|
||||
return m_signal_message_progress;
|
||||
}
|
||||
|
||||
DiscordClient::type_signal_role_update DiscordClient::signal_role_update() {
|
||||
return m_signal_role_update;
|
||||
}
|
||||
@@ -2657,6 +2789,10 @@ DiscordClient::type_signal_guild_unmuted DiscordClient::signal_guild_unmuted() {
|
||||
return m_signal_guild_unmuted;
|
||||
}
|
||||
|
||||
DiscordClient::type_signal_channel_accessibility_changed DiscordClient::signal_channel_accessibility_changed() {
|
||||
return m_signal_channel_accessibility_changed;
|
||||
}
|
||||
|
||||
DiscordClient::type_signal_message_send_fail DiscordClient::signal_message_send_fail() {
|
||||
return m_signal_message_send_fail;
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#include "httpclient.hpp"
|
||||
#include "objects.hpp"
|
||||
#include "store.hpp"
|
||||
#include "chatsubmitparams.hpp"
|
||||
#include <sigc++/sigc++.h>
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <thread>
|
||||
@@ -49,7 +50,7 @@ class DiscordClient {
|
||||
public:
|
||||
DiscordClient(bool mem_store = false);
|
||||
void Start();
|
||||
void Stop();
|
||||
bool Stop();
|
||||
bool IsStarted() const;
|
||||
bool IsStoreValid() const;
|
||||
|
||||
@@ -96,27 +97,27 @@ public:
|
||||
bool IsThreadJoined(Snowflake thread_id) const;
|
||||
bool HasGuildPermission(Snowflake user_id, Snowflake guild_id, Permission perm) const;
|
||||
|
||||
bool HasSelfChannelPermission(Snowflake channel_id, Permission perm) const;
|
||||
bool HasAnyChannelPermission(Snowflake user_id, Snowflake channel_id, Permission perm) const;
|
||||
bool HasChannelPermission(Snowflake user_id, Snowflake channel_id, Permission perm) const;
|
||||
Permission ComputePermissions(Snowflake member_id, Snowflake guild_id) const;
|
||||
Permission ComputeOverwrites(Permission base, Snowflake member_id, Snowflake channel_id) const;
|
||||
bool CanManageMember(Snowflake guild_id, Snowflake actor, Snowflake target) const; // kick, ban, edit nickname (cant think of a better name)
|
||||
|
||||
void ChatMessageCallback(const std::string &nonce, const http::response_type &response);
|
||||
void ChatMessageCallback(const std::string &nonce, const http::response_type &response, const sigc::slot<void(DiscordError code)> &callback);
|
||||
void SendChatMessageNoAttachments(const ChatSubmitParams ¶ms, const sigc::slot<void(DiscordError code)> &callback);
|
||||
void SendChatMessageAttachments(const ChatSubmitParams ¶ms, const sigc::slot<void(DiscordError code)> &callback);
|
||||
|
||||
void SendChatMessage(const std::string &content, Snowflake channel);
|
||||
void SendChatMessage(const std::string &content, Snowflake channel, Snowflake referenced_message);
|
||||
void SendChatMessage(const ChatSubmitParams ¶ms, const sigc::slot<void(DiscordError code)> &callback);
|
||||
void DeleteMessage(Snowflake channel_id, Snowflake id);
|
||||
void EditMessage(Snowflake channel_id, Snowflake id, std::string content);
|
||||
void SendLazyLoad(Snowflake id);
|
||||
void SendThreadLazyLoad(Snowflake id);
|
||||
void JoinGuild(const std::string &code);
|
||||
void LeaveGuild(Snowflake id);
|
||||
void KickUser(Snowflake user_id, Snowflake guild_id);
|
||||
void BanUser(Snowflake user_id, Snowflake guild_id); // todo: reason, delete messages
|
||||
void UpdateStatus(PresenceStatus status, bool is_afk);
|
||||
void UpdateStatus(PresenceStatus status, bool is_afk, const ActivityData &obj);
|
||||
void CreateDM(Snowflake user_id, const sigc::slot<void(DiscordError code, Snowflake channel_id)> &callback);
|
||||
void CloseDM(Snowflake channel_id);
|
||||
std::optional<Snowflake> FindDM(Snowflake user_id); // wont find group dms
|
||||
void AddReaction(Snowflake id, Glib::ustring param);
|
||||
@@ -202,6 +203,11 @@ public:
|
||||
void GetVerificationGateInfo(Snowflake guild_id, const sigc::slot<void(std::optional<VerificationGateInfoObject>)> &callback);
|
||||
void AcceptVerificationGate(Snowflake guild_id, VerificationGateInfoObject info, const sigc::slot<void(DiscordError code)> &callback);
|
||||
|
||||
void SetReferringChannel(Snowflake id);
|
||||
|
||||
void SetBuildNumber(uint32_t build_number);
|
||||
void SetCookie(std::string_view cookie);
|
||||
|
||||
void UpdateToken(const std::string &token);
|
||||
void SetUserAgent(const std::string &agent);
|
||||
|
||||
@@ -283,6 +289,9 @@ private:
|
||||
void SendIdentify();
|
||||
void SendResume();
|
||||
|
||||
void SetHeaders();
|
||||
void SetSuperPropertiesFromIdentity(const IdentifyMessage &identity);
|
||||
|
||||
void HandleSocketOpen();
|
||||
void HandleSocketClose(uint16_t code);
|
||||
|
||||
@@ -296,6 +305,8 @@ private:
|
||||
|
||||
std::string m_token;
|
||||
|
||||
uint32_t m_build_number = 142000;
|
||||
|
||||
void AddUserToGuild(Snowflake user_id, Snowflake guild_id);
|
||||
std::map<Snowflake, std::set<Snowflake>> m_guild_to_users;
|
||||
std::map<Snowflake, std::set<Snowflake>> m_guild_to_channels;
|
||||
@@ -342,6 +353,8 @@ private:
|
||||
Glib::Dispatcher m_generic_dispatch;
|
||||
std::queue<std::function<void()>> m_generic_queue;
|
||||
|
||||
Glib::Timer m_progress_cb_timer;
|
||||
|
||||
std::set<Snowflake> m_channels_pinned_requested;
|
||||
std::set<Snowflake> m_channels_lazy_loaded;
|
||||
|
||||
@@ -396,10 +409,12 @@ public:
|
||||
typedef sigc::signal<void, Snowflake> type_signal_channel_unmuted;
|
||||
typedef sigc::signal<void, Snowflake> type_signal_guild_muted;
|
||||
typedef sigc::signal<void, Snowflake> type_signal_guild_unmuted;
|
||||
typedef sigc::signal<void, Snowflake, bool> type_signal_channel_accessibility_changed;
|
||||
|
||||
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> type_signal_connected;
|
||||
typedef sigc::signal<void, std::string, float> type_signal_message_progress;
|
||||
|
||||
type_signal_gateway_ready signal_gateway_ready();
|
||||
type_signal_message_create signal_message_create();
|
||||
@@ -449,9 +464,11 @@ public:
|
||||
type_signal_channel_unmuted signal_channel_unmuted();
|
||||
type_signal_guild_muted signal_guild_muted();
|
||||
type_signal_guild_unmuted signal_guild_unmuted();
|
||||
type_signal_channel_accessibility_changed signal_channel_accessibility_changed();
|
||||
type_signal_message_send_fail signal_message_send_fail();
|
||||
type_signal_disconnected signal_disconnected();
|
||||
type_signal_connected signal_connected();
|
||||
type_signal_message_progress signal_message_progress();
|
||||
|
||||
protected:
|
||||
type_signal_gateway_ready m_signal_gateway_ready;
|
||||
@@ -502,7 +519,9 @@ protected:
|
||||
type_signal_channel_unmuted m_signal_channel_unmuted;
|
||||
type_signal_guild_muted m_signal_guild_muted;
|
||||
type_signal_guild_unmuted m_signal_guild_unmuted;
|
||||
type_signal_channel_accessibility_changed m_signal_channel_accessibility_changed;
|
||||
type_signal_message_send_fail m_signal_message_send_fail;
|
||||
type_signal_disconnected m_signal_disconnected;
|
||||
type_signal_connected m_signal_connected;
|
||||
type_signal_message_progress m_signal_message_progress;
|
||||
};
|
||||
|
||||
@@ -16,6 +16,13 @@ enum class GuildApplicationStatus {
|
||||
UNKNOWN,
|
||||
};
|
||||
|
||||
enum class GuildPremiumTier {
|
||||
NONE = 0,
|
||||
TIER_1 = 1,
|
||||
TIER_2 = 2,
|
||||
TIER_3 = 3,
|
||||
};
|
||||
|
||||
struct GuildApplicationData {
|
||||
Snowflake UserID;
|
||||
Snowflake GuildID;
|
||||
@@ -73,7 +80,7 @@ struct GuildData {
|
||||
std::optional<std::string> VanityURL; // null
|
||||
std::optional<std::string> Description; // null
|
||||
std::optional<std::string> BannerHash; // null
|
||||
std::optional<int> PremiumTier;
|
||||
std::optional<GuildPremiumTier> PremiumTier;
|
||||
std::optional<int> PremiumSubscriptionCount;
|
||||
std::optional<std::string> PreferredLocale;
|
||||
std::optional<Snowflake> PublicUpdatesChannelID; // null
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
#include <utility>
|
||||
|
||||
//#define USE_LOCAL_PROXY
|
||||
HTTPClient::HTTPClient() {
|
||||
m_dispatcher.connect(sigc::mem_fun(*this, &HTTPClient::RunCallbacks));
|
||||
}
|
||||
@@ -19,16 +18,22 @@ void HTTPClient::SetAuth(std::string auth) {
|
||||
m_authorization = std::move(auth);
|
||||
}
|
||||
|
||||
void HTTPClient::SetPersistentHeader(std::string name, std::string value) {
|
||||
m_headers.insert_or_assign(std::move(name), std::move(value));
|
||||
}
|
||||
|
||||
void HTTPClient::SetCookie(std::string_view cookie) {
|
||||
m_cookie = cookie;
|
||||
}
|
||||
|
||||
void HTTPClient::MakeDELETE(const std::string &path, const std::function<void(http::response_type r)> &cb) {
|
||||
printf("DELETE %s\n", path.c_str());
|
||||
m_futures.push_back(std::async(std::launch::async, [this, path, cb] {
|
||||
http::request req(http::REQUEST_DELETE, m_api_base + path);
|
||||
AddHeaders(req);
|
||||
req.set_header("Authorization", m_authorization);
|
||||
req.set_header("Origin", "https://discord.com");
|
||||
req.set_user_agent(!m_agent.empty() ? m_agent : "Abaddon");
|
||||
#ifdef USE_LOCAL_PROXY
|
||||
req.set_proxy("http://127.0.0.1:8888");
|
||||
req.set_verify_ssl(false);
|
||||
#endif
|
||||
|
||||
auto res = req.execute();
|
||||
|
||||
@@ -40,14 +45,12 @@ void HTTPClient::MakePATCH(const std::string &path, const std::string &payload,
|
||||
printf("PATCH %s\n", path.c_str());
|
||||
m_futures.push_back(std::async(std::launch::async, [this, path, cb, payload] {
|
||||
http::request req(http::REQUEST_PATCH, m_api_base + path);
|
||||
AddHeaders(req);
|
||||
req.set_header("Authorization", m_authorization);
|
||||
req.set_header("Content-Type", "application/json");
|
||||
req.set_header("Origin", "https://discord.com");
|
||||
req.set_user_agent(!m_agent.empty() ? m_agent : "Abaddon");
|
||||
req.set_body(payload);
|
||||
#ifdef USE_LOCAL_PROXY
|
||||
req.set_proxy("http://127.0.0.1:8888");
|
||||
req.set_verify_ssl(false);
|
||||
#endif
|
||||
|
||||
auto res = req.execute();
|
||||
|
||||
@@ -59,14 +62,12 @@ void HTTPClient::MakePOST(const std::string &path, const std::string &payload, c
|
||||
printf("POST %s\n", path.c_str());
|
||||
m_futures.push_back(std::async(std::launch::async, [this, path, cb, payload] {
|
||||
http::request req(http::REQUEST_POST, m_api_base + path);
|
||||
AddHeaders(req);
|
||||
req.set_header("Authorization", m_authorization);
|
||||
req.set_header("Content-Type", "application/json");
|
||||
req.set_header("Origin", "https://discord.com");
|
||||
req.set_user_agent(!m_agent.empty() ? m_agent : "Abaddon");
|
||||
req.set_body(payload);
|
||||
#ifdef USE_LOCAL_PROXY
|
||||
req.set_proxy("http://127.0.0.1:8888");
|
||||
req.set_verify_ssl(false);
|
||||
#endif
|
||||
|
||||
auto res = req.execute();
|
||||
|
||||
@@ -78,15 +79,13 @@ void HTTPClient::MakePUT(const std::string &path, const std::string &payload, co
|
||||
printf("PUT %s\n", path.c_str());
|
||||
m_futures.push_back(std::async(std::launch::async, [this, path, cb, payload] {
|
||||
http::request req(http::REQUEST_PUT, m_api_base + path);
|
||||
AddHeaders(req);
|
||||
req.set_header("Authorization", m_authorization);
|
||||
req.set_header("Origin", "https://discord.com");
|
||||
if (!payload.empty())
|
||||
req.set_header("Content-Type", "application/json");
|
||||
req.set_user_agent(!m_agent.empty() ? m_agent : "Abaddon");
|
||||
req.set_body(payload);
|
||||
#ifdef USE_LOCAL_PROXY
|
||||
req.set_proxy("http://127.0.0.1:8888");
|
||||
req.set_verify_ssl(false);
|
||||
#endif
|
||||
|
||||
auto res = req.execute();
|
||||
|
||||
@@ -98,13 +97,9 @@ void HTTPClient::MakeGET(const std::string &path, const std::function<void(http:
|
||||
printf("GET %s\n", path.c_str());
|
||||
m_futures.push_back(std::async(std::launch::async, [this, path, cb] {
|
||||
http::request req(http::REQUEST_GET, m_api_base + path);
|
||||
AddHeaders(req);
|
||||
req.set_header("Authorization", m_authorization);
|
||||
req.set_header("Content-Type", "application/json");
|
||||
req.set_user_agent(!m_agent.empty() ? m_agent : "Abaddon");
|
||||
#ifdef USE_LOCAL_PROXY
|
||||
req.set_proxy("http://127.0.0.1:8888");
|
||||
req.set_verify_ssl(false);
|
||||
#endif
|
||||
|
||||
auto res = req.execute();
|
||||
|
||||
@@ -112,6 +107,21 @@ void HTTPClient::MakeGET(const std::string &path, const std::function<void(http:
|
||||
}));
|
||||
}
|
||||
|
||||
http::request HTTPClient::CreateRequest(http::EMethod method, std::string path) {
|
||||
http::request req(method, m_api_base + path);
|
||||
req.set_header("Authorization", m_authorization);
|
||||
req.set_user_agent(!m_agent.empty() ? m_agent : "Abaddon");
|
||||
return req;
|
||||
}
|
||||
|
||||
void HTTPClient::Execute(http::request &&req, const std::function<void(http::response_type r)> &cb) {
|
||||
printf("%s %s\n", req.get_method(), req.get_url().c_str());
|
||||
m_futures.push_back(std::async(std::launch::async, [this, cb, req = std::move(req)]() mutable {
|
||||
auto res = req.execute();
|
||||
OnResponse(res, cb);
|
||||
}));
|
||||
}
|
||||
|
||||
void HTTPClient::CleanupFutures() {
|
||||
for (auto it = m_futures.begin(); it != m_futures.end();) {
|
||||
if (it->wait_for(std::chrono::seconds(0)) == std::future_status::ready)
|
||||
@@ -128,6 +138,14 @@ void HTTPClient::RunCallbacks() {
|
||||
m_mutex.unlock();
|
||||
}
|
||||
|
||||
void HTTPClient::AddHeaders(http::request &r) {
|
||||
for (const auto &[name, val] : m_headers) {
|
||||
r.set_header(name, val);
|
||||
}
|
||||
curl_easy_setopt(r.get_curl(), CURLOPT_COOKIE, m_cookie.c_str());
|
||||
curl_easy_setopt(r.get_curl(), CURLOPT_ACCEPT_ENCODING, "");
|
||||
}
|
||||
|
||||
void HTTPClient::OnResponse(const http::response_type &r, const std::function<void(http::response_type r)> &cb) {
|
||||
CleanupFutures();
|
||||
try {
|
||||
|
||||
@@ -17,13 +17,21 @@ public:
|
||||
|
||||
void SetUserAgent(std::string agent);
|
||||
void SetAuth(std::string auth);
|
||||
void SetPersistentHeader(std::string name, std::string value);
|
||||
void SetCookie(std::string_view cookie);
|
||||
|
||||
void MakeDELETE(const std::string &path, const std::function<void(http::response_type r)> &cb);
|
||||
void MakeGET(const std::string &path, const std::function<void(http::response_type r)> &cb);
|
||||
void MakePATCH(const std::string &path, const std::string &payload, const std::function<void(http::response_type r)> &cb);
|
||||
void MakePOST(const std::string &path, const std::string &payload, const std::function<void(http::response_type r)> &cb);
|
||||
void MakePUT(const std::string &path, const std::string &payload, const std::function<void(http::response_type r)> &cb);
|
||||
|
||||
[[nodiscard]] http::request CreateRequest(http::EMethod method, std::string path);
|
||||
void Execute(http::request &&req, const std::function<void(http::response_type r)> &cb);
|
||||
|
||||
private:
|
||||
void AddHeaders(http::request &r);
|
||||
|
||||
void OnResponse(const http::response_type &r, const std::function<void(http::response_type r)> &cb);
|
||||
void CleanupFutures();
|
||||
|
||||
@@ -36,4 +44,6 @@ private:
|
||||
std::string m_api_base;
|
||||
std::string m_authorization;
|
||||
std::string m_agent;
|
||||
std::unordered_map<std::string, std::string> m_headers;
|
||||
std::string m_cookie;
|
||||
};
|
||||
|
||||
@@ -41,7 +41,7 @@ void from_json(const nlohmann::json &j, GuildMemberListUpdateMessage::MemberItem
|
||||
JS_D("mute", m.IsMuted);
|
||||
JS_D("joined_at", m.JoinedAt);
|
||||
JS_D("deaf", m.IsDefeaned);
|
||||
JS_N("hoisted_role", m.HoistedRole);
|
||||
JS_ON("hoisted_role", m.HoistedRole);
|
||||
JS_ON("premium_since", m.PremiumSince);
|
||||
JS_ON("nick", m.Nickname);
|
||||
JS_ON("presence", m.Presence);
|
||||
@@ -262,6 +262,7 @@ void to_json(nlohmann::json &j, const ClientStateProperties &m) {
|
||||
j["highest_last_message_id"] = m.HighestLastMessageID;
|
||||
j["read_state_version"] = m.ReadStateVersion;
|
||||
j["user_guild_settings_version"] = m.UserGuildSettingsVersion;
|
||||
j["user_settings_version"] = m.UserSettingsVersion;
|
||||
}
|
||||
|
||||
void to_json(nlohmann::json &j, const IdentifyMessage &m) {
|
||||
|
||||
@@ -382,6 +382,7 @@ struct ClientStateProperties {
|
||||
std::string HighestLastMessageID = "0";
|
||||
int ReadStateVersion = 0;
|
||||
int UserGuildSettingsVersion = -1;
|
||||
int UserSettingsVersion = -1;
|
||||
|
||||
friend void to_json(nlohmann::json &j, const ClientStateProperties &m);
|
||||
};
|
||||
|
||||
@@ -253,6 +253,14 @@ void Store::SetGuildMember(Snowflake guild_id, Snowflake user_id, const GuildMem
|
||||
|
||||
s->Reset();
|
||||
|
||||
{
|
||||
auto &s = m_stmt_clr_member_roles;
|
||||
s->Bind(1, user_id);
|
||||
s->Bind(2, guild_id);
|
||||
s->Step();
|
||||
s->Reset();
|
||||
}
|
||||
|
||||
{
|
||||
auto &s = m_stmt_set_member_roles;
|
||||
|
||||
@@ -738,6 +746,7 @@ std::optional<GuildData> Store::GetGuild(Snowflake id) const {
|
||||
s->Get(2, r.Icon);
|
||||
s->Get(5, r.OwnerID);
|
||||
s->Get(20, r.IsUnavailable);
|
||||
s->Get(27, r.PremiumTier);
|
||||
|
||||
s->Reset();
|
||||
|
||||
@@ -906,21 +915,28 @@ Message Store::GetMessageBound(std::unique_ptr<Statement> &s) const {
|
||||
}
|
||||
|
||||
if (!s->IsNull(25)) {
|
||||
auto &a = r.Attachments.emplace_back();
|
||||
s->Get(25, a.ID);
|
||||
s->Get(26, a.Filename);
|
||||
s->Get(27, a.Bytes);
|
||||
s->Get(28, a.URL);
|
||||
s->Get(29, a.ProxyURL);
|
||||
s->Get(30, a.Height);
|
||||
s->Get(31, a.Width);
|
||||
auto &q = r.MessageReference.emplace();
|
||||
s->Get(25, q.MessageID);
|
||||
s->Get(26, q.ChannelID);
|
||||
s->Get(27, q.GuildID);
|
||||
}
|
||||
|
||||
if (!s->IsNull(32)) {
|
||||
auto &q = r.MessageReference.emplace();
|
||||
s->Get(32, q.MessageID);
|
||||
s->Get(33, q.ChannelID);
|
||||
s->Get(34, q.GuildID);
|
||||
int num_attachments;
|
||||
s->Get(28, num_attachments);
|
||||
if (num_attachments > 0) {
|
||||
auto &s = m_stmt_get_attachments;
|
||||
s->Bind(1, r.ID);
|
||||
while (s->FetchOne()) {
|
||||
auto &q = r.Attachments.emplace_back();
|
||||
s->Get(1, q.ID);
|
||||
s->Get(2, q.Filename);
|
||||
s->Get(3, q.Bytes);
|
||||
s->Get(4, q.URL);
|
||||
s->Get(5, q.ProxyURL);
|
||||
s->Get(6, q.Height);
|
||||
s->Get(7, q.Width);
|
||||
}
|
||||
s->Reset();
|
||||
}
|
||||
|
||||
{
|
||||
@@ -1078,6 +1094,14 @@ void Store::ClearRecipient(Snowflake channel_id, Snowflake user_id) {
|
||||
s->Reset();
|
||||
}
|
||||
|
||||
void Store::ClearRole(Snowflake id) {
|
||||
auto &s = m_stmt_clr_role;
|
||||
|
||||
s->Bind(1, id);
|
||||
s->Step();
|
||||
s->Reset();
|
||||
}
|
||||
|
||||
std::unordered_set<Snowflake> Store::GetChannels() const {
|
||||
auto &s = m_stmt_get_chan_ids;
|
||||
std::unordered_set<Snowflake> r;
|
||||
@@ -1522,6 +1546,16 @@ bool Store::CreateTables() {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (m_db.Execute(R"(
|
||||
CREATE TRIGGER remove_deleted_roles AFTER DELETE ON roles
|
||||
BEGIN
|
||||
DELETE FROM member_roles WHERE role = old.id;
|
||||
END
|
||||
)") != SQLITE_OK) {
|
||||
fprintf(stderr, "failed to create roles trigger: %s\n", m_db.ErrStr());
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1611,55 +1645,43 @@ bool Store::CreateStatements() {
|
||||
message_interactions.name,
|
||||
message_interactions.type,
|
||||
message_interactions.user_id,
|
||||
attachments.id,
|
||||
attachments.filename,
|
||||
attachments.size,
|
||||
attachments.url,
|
||||
attachments.proxy,
|
||||
attachments.height,
|
||||
attachments.width,
|
||||
message_references.message,
|
||||
message_references.channel,
|
||||
message_references.guild
|
||||
message_references.guild,
|
||||
COUNT(attachments.id)
|
||||
FROM messages
|
||||
LEFT OUTER JOIN
|
||||
message_interactions
|
||||
ON messages.id = message_interactions.message_id
|
||||
LEFT OUTER JOIN
|
||||
attachments
|
||||
ON messages.id = attachments.message
|
||||
LEFT OUTER JOIN
|
||||
message_references
|
||||
ON messages.id = message_references.id
|
||||
WHERE messages.id = ?1
|
||||
LEFT JOIN
|
||||
attachments
|
||||
ON messages.id = attachments.message
|
||||
WHERE messages.id = ?1 GROUP BY messages.id
|
||||
UNION ALL
|
||||
SELECT messages.*,
|
||||
message_interactions.interaction_id,
|
||||
message_interactions.name,
|
||||
message_interactions.type,
|
||||
message_interactions.user_id,
|
||||
attachments.id,
|
||||
attachments.filename,
|
||||
attachments.size,
|
||||
attachments.url,
|
||||
attachments.proxy,
|
||||
attachments.height,
|
||||
attachments.width,
|
||||
message_references.message,
|
||||
message_references.channel,
|
||||
message_references.guild
|
||||
message_references.guild,
|
||||
COUNT(attachments.id)
|
||||
FROM messages
|
||||
LEFT OUTER JOIN
|
||||
message_interactions
|
||||
ON messages.id = message_interactions.message_id
|
||||
LEFT OUTER JOIN
|
||||
attachments
|
||||
ON messages.id = attachments.message
|
||||
LEFT OUTER JOIN
|
||||
message_references
|
||||
ON messages.id = message_references.id
|
||||
LEFT JOIN
|
||||
attachments
|
||||
ON messages.id = attachments.message
|
||||
WHERE messages.id = (SELECT message FROM message_references WHERE id = ?1)
|
||||
ORDER BY messages.id DESC
|
||||
GROUP BY messages.id ORDER BY messages.id DESC
|
||||
)");
|
||||
if (!m_stmt_get_msg->OK()) {
|
||||
fprintf(stderr, "failed to prepare get message statement: %s\n", m_db.ErrStr());
|
||||
@@ -1683,27 +1705,21 @@ bool Store::CreateStatements() {
|
||||
message_interactions.name,
|
||||
message_interactions.type,
|
||||
message_interactions.user_id,
|
||||
attachments.id,
|
||||
attachments.filename,
|
||||
attachments.size,
|
||||
attachments.url,
|
||||
attachments.proxy,
|
||||
attachments.height,
|
||||
attachments.width,
|
||||
message_references.message,
|
||||
message_references.channel,
|
||||
message_references.guild
|
||||
message_references.guild,
|
||||
COUNT(attachments.id)
|
||||
FROM messages
|
||||
LEFT OUTER JOIN
|
||||
message_interactions
|
||||
ON messages.id = message_interactions.message_id
|
||||
LEFT OUTER JOIN
|
||||
attachments
|
||||
ON messages.id = attachments.message
|
||||
LEFT OUTER JOIN
|
||||
message_references
|
||||
ON messages.id = message_references.id
|
||||
WHERE channel_id = ? AND pending = 0 ORDER BY id DESC LIMIT ?
|
||||
LEFT JOIN
|
||||
attachments
|
||||
ON messages.id = attachments.message
|
||||
WHERE channel_id = ? AND pending = 0 GROUP BY messages.id ORDER BY id DESC LIMIT ?
|
||||
) ORDER BY id ASC
|
||||
)");
|
||||
if (!m_stmt_get_last_msgs->OK()) {
|
||||
@@ -1874,6 +1890,20 @@ bool Store::CreateStatements() {
|
||||
return false;
|
||||
}
|
||||
|
||||
m_stmt_clr_member_roles = std::make_unique<Statement>(m_db, R"(
|
||||
DELETE FROM member_roles
|
||||
WHERE user = ? AND
|
||||
EXISTS (
|
||||
SELECT 1 FROM roles
|
||||
WHERE member_roles.role = roles.id
|
||||
AND roles.guild = ?
|
||||
)
|
||||
)");
|
||||
if (!m_stmt_clr_member_roles->OK()) {
|
||||
fprintf(stderr, "failed to prepare clear member roles statement: %s\n", m_db.ErrStr());
|
||||
return false;
|
||||
}
|
||||
|
||||
m_stmt_set_guild_emoji = std::make_unique<Statement>(m_db, R"(
|
||||
REPLACE INTO guild_emojis VALUES (
|
||||
?, ?
|
||||
@@ -2159,6 +2189,15 @@ bool Store::CreateStatements() {
|
||||
return false;
|
||||
}
|
||||
|
||||
m_stmt_clr_role = std::make_unique<Statement>(m_db, R"(
|
||||
DELETE FROM roles
|
||||
WHERE id = ?1;
|
||||
)");
|
||||
if (!m_stmt_clr_role->OK()) {
|
||||
fprintf(stderr, "failed to prepare clear role statement: %s\n", m_db.ErrStr());
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -54,6 +54,7 @@ public:
|
||||
void ClearChannel(Snowflake id);
|
||||
void ClearBan(Snowflake guild_id, Snowflake user_id);
|
||||
void ClearRecipient(Snowflake channel_id, Snowflake user_id);
|
||||
void ClearRole(Snowflake id);
|
||||
|
||||
std::unordered_set<Snowflake> GetChannels() const;
|
||||
std::unordered_set<Snowflake> GetGuilds() const;
|
||||
@@ -280,6 +281,7 @@ private:
|
||||
STMT(set_interaction);
|
||||
STMT(set_member_roles);
|
||||
STMT(get_member_roles);
|
||||
STMT(clr_member_roles);
|
||||
STMT(set_guild_emoji);
|
||||
STMT(get_guild_emojis);
|
||||
STMT(clr_guild_emoji);
|
||||
@@ -305,5 +307,6 @@ private:
|
||||
STMT(get_reactions);
|
||||
STMT(get_chan_ids_parent);
|
||||
STMT(get_guild_member_ids);
|
||||
STMT(clr_role);
|
||||
#undef STMT
|
||||
};
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
#include "user.hpp"
|
||||
#include "abaddon.hpp"
|
||||
|
||||
bool UserData::IsABot() const noexcept {
|
||||
return IsBot.has_value() && *IsBot;
|
||||
}
|
||||
|
||||
bool UserData::IsDeleted() const {
|
||||
return Discriminator == "0000";
|
||||
}
|
||||
|
||||
@@ -60,6 +60,7 @@ struct UserData {
|
||||
friend void to_json(nlohmann::json &j, const UserData &m);
|
||||
void update_from_json(const nlohmann::json &j);
|
||||
|
||||
[[nodiscard]] bool IsABot() const noexcept;
|
||||
[[nodiscard]] bool IsDeleted() const;
|
||||
[[nodiscard]] bool HasAvatar() const;
|
||||
[[nodiscard]] bool HasAnimatedAvatar() const noexcept;
|
||||
|
||||
@@ -8,33 +8,5 @@ void from_json(const nlohmann::json &j, UserSettingsGuildFoldersEntry &m) {
|
||||
}
|
||||
|
||||
void from_json(const nlohmann::json &j, UserSettings &m) {
|
||||
JS_D("timezone_offset", m.TimezoneOffset);
|
||||
JS_D("theme", m.Theme);
|
||||
JS_D("stream_notifications_enabled", m.AreStreamNotificationsEnabled);
|
||||
JS_D("status", m.Status);
|
||||
JS_D("show_current_game", m.ShouldShowCurrentGame);
|
||||
// JS_D("restricted_guilds", m.RestrictedGuilds);
|
||||
JS_D("render_reactions", m.ShouldRenderReactions);
|
||||
JS_D("render_embeds", m.ShouldRenderEmbeds);
|
||||
JS_D("native_phone_integration_enabled", m.IsNativePhoneIntegrationEnabled);
|
||||
JS_D("message_display_compact", m.ShouldMessageDisplayCompact);
|
||||
JS_D("locale", m.Locale);
|
||||
JS_D("inline_embed_media", m.ShouldInlineEmbedMedia);
|
||||
JS_D("inline_attachment_media", m.ShouldInlineAttachmentMedia);
|
||||
JS_D("guild_positions", m.GuildPositions);
|
||||
JS_D("guild_folders", m.GuildFolders);
|
||||
JS_D("gif_auto_play", m.ShouldGIFAutoplay);
|
||||
// JS_D("friend_source_flags", m.FriendSourceFlags);
|
||||
JS_D("explicit_content_filter", m.ExplicitContentFilter);
|
||||
JS_D("enable_tts_command", m.IsTTSCommandEnabled);
|
||||
JS_D("disable_games_tab", m.ShouldDisableGamesTab);
|
||||
JS_D("developer_mode", m.DeveloperMode);
|
||||
JS_D("detect_platform_accounts", m.ShouldDetectPlatformAccounts);
|
||||
JS_D("default_guilds_restricted", m.AreDefaultGuildsRestricted);
|
||||
// JS_N("custom_status", m.CustomStatus);
|
||||
JS_D("convert_emoticons", m.ShouldConvertEmoticons);
|
||||
JS_D("contact_sync_enabled", m.IsContactSyncEnabled);
|
||||
JS_D("animate_emoji", m.ShouldAnimateEmojis);
|
||||
JS_D("allow_accessibility_detection", m.IsAccessibilityDetectionAllowed);
|
||||
JS_D("afk_timeout", m.AFKTimeout);
|
||||
}
|
||||
|
||||
@@ -13,35 +13,36 @@ struct UserSettingsGuildFoldersEntry {
|
||||
};
|
||||
|
||||
struct UserSettings {
|
||||
int TimezoneOffset; //
|
||||
std::string Theme; //
|
||||
bool AreStreamNotificationsEnabled; //
|
||||
std::string Status; //
|
||||
bool ShouldShowCurrentGame; //
|
||||
// std::vector<Unknown> RestrictedGuilds; //
|
||||
bool ShouldRenderReactions; //
|
||||
bool ShouldRenderEmbeds; //
|
||||
bool IsNativePhoneIntegrationEnabled; //
|
||||
bool ShouldMessageDisplayCompact; //
|
||||
std::string Locale; //
|
||||
bool ShouldInlineEmbedMedia; //
|
||||
bool ShouldInlineAttachmentMedia; //
|
||||
std::vector<Snowflake> GuildPositions; // deprecated?
|
||||
std::vector<UserSettingsGuildFoldersEntry> GuildFolders; //
|
||||
bool ShouldGIFAutoplay; //
|
||||
// Unknown FriendSourceFlags; //
|
||||
int ExplicitContentFilter; //
|
||||
bool IsTTSCommandEnabled; //
|
||||
bool ShouldDisableGamesTab; //
|
||||
bool DeveloperMode; //
|
||||
bool ShouldDetectPlatformAccounts; //
|
||||
bool AreDefaultGuildsRestricted; //
|
||||
std::vector<UserSettingsGuildFoldersEntry> GuildFolders;
|
||||
/*
|
||||
int TimezoneOffset;
|
||||
std::string Theme;
|
||||
bool AreStreamNotificationsEnabled;
|
||||
std::string Status;
|
||||
bool ShouldShowCurrentGame;
|
||||
// std::vector<Unknown> RestrictedGuilds;
|
||||
bool ShouldRenderReactions;
|
||||
bool ShouldRenderEmbeds;
|
||||
bool IsNativePhoneIntegrationEnabled;
|
||||
bool ShouldMessageDisplayCompact;
|
||||
std::string Locale;
|
||||
bool ShouldInlineEmbedMedia;
|
||||
bool ShouldInlineAttachmentMedia;
|
||||
std::vector<Snowflake> GuildPositions; // deprecated?
|
||||
bool ShouldGIFAutoplay;
|
||||
// Unknown FriendSourceFlags;
|
||||
int ExplicitContentFilter;
|
||||
bool IsTTSCommandEnabled;
|
||||
bool ShouldDisableGamesTab;
|
||||
bool DeveloperMode;
|
||||
bool ShouldDetectPlatformAccounts;
|
||||
bool AreDefaultGuildsRestricted;
|
||||
// Unknown CustomStatus; // null
|
||||
bool ShouldConvertEmoticons; //
|
||||
bool IsContactSyncEnabled; //
|
||||
bool ShouldAnimateEmojis; //
|
||||
bool IsAccessibilityDetectionAllowed; //
|
||||
int AFKTimeout;
|
||||
bool ShouldConvertEmoticons;
|
||||
bool IsContactSyncEnabled;
|
||||
bool ShouldAnimateEmojis;
|
||||
bool IsAccessibilityDetectionAllowed;
|
||||
int AFKTimeout;*/
|
||||
|
||||
friend void from_json(const nlohmann::json &j, UserSettings &m);
|
||||
};
|
||||
|
||||
@@ -2,6 +2,22 @@
|
||||
#include <sstream>
|
||||
#include <utility>
|
||||
|
||||
#ifdef ABADDON_IS_BIG_ENDIAN
|
||||
/* Allows processing emojis.bin correctly on big-endian systems. */
|
||||
int emojis_int32_correct_endian(int little_endian_in) {
|
||||
/* this does the same thing as __bswap_32() but can be done without
|
||||
non-standard headers. */
|
||||
return ((little_endian_in >> 24) & 0xff) | // move byte 3 to byte 0
|
||||
((little_endian_in << 8) & 0xff0000) | // move byte 1 to byte 2
|
||||
((little_endian_in >> 8) & 0xff00) | // move byte 2 to byte 1
|
||||
((little_endian_in << 24) & 0xff000000); // byte 0 to byte 3
|
||||
}
|
||||
#else
|
||||
int emojis_int32_correct_endian(int little_endian_in) {
|
||||
return little_endian_in;
|
||||
}
|
||||
#endif
|
||||
|
||||
EmojiResource::EmojiResource(std::string filepath)
|
||||
: m_filepath(std::move(filepath)) {}
|
||||
|
||||
@@ -11,18 +27,22 @@ bool EmojiResource::Load() {
|
||||
|
||||
int index_offset;
|
||||
std::fread(&index_offset, 4, 1, m_fp);
|
||||
index_offset = emojis_int32_correct_endian(index_offset);
|
||||
std::fseek(m_fp, index_offset, SEEK_SET);
|
||||
|
||||
int emojis_count;
|
||||
std::fread(&emojis_count, 4, 1, m_fp);
|
||||
emojis_count = emojis_int32_correct_endian(emojis_count);
|
||||
for (int i = 0; i < emojis_count; i++) {
|
||||
std::vector<std::string> shortcodes;
|
||||
|
||||
int shortcodes_count;
|
||||
std::fread(&shortcodes_count, 4, 1, m_fp);
|
||||
shortcodes_count = emojis_int32_correct_endian(shortcodes_count);
|
||||
for (int j = 0; j < shortcodes_count; j++) {
|
||||
int shortcode_length;
|
||||
std::fread(&shortcode_length, 4, 1, m_fp);
|
||||
shortcode_length = emojis_int32_correct_endian(shortcode_length);
|
||||
std::string shortcode(shortcode_length, '\0');
|
||||
std::fread(shortcode.data(), shortcode_length, 1, m_fp);
|
||||
shortcodes.push_back(std::move(shortcode));
|
||||
@@ -30,13 +50,16 @@ bool EmojiResource::Load() {
|
||||
|
||||
int surrogates_count;
|
||||
std::fread(&surrogates_count, 4, 1, m_fp);
|
||||
surrogates_count = emojis_int32_correct_endian(surrogates_count);
|
||||
std::string surrogates(surrogates_count, '\0');
|
||||
std::fread(surrogates.data(), surrogates_count, 1, m_fp);
|
||||
m_patterns.emplace_back(surrogates);
|
||||
|
||||
int data_size, data_offset;
|
||||
std::fread(&data_size, 4, 1, m_fp);
|
||||
data_size = emojis_int32_correct_endian(data_size);
|
||||
std::fread(&data_offset, 4, 1, m_fp);
|
||||
data_offset = emojis_int32_correct_endian(data_offset);
|
||||
m_index[surrogates] = { data_offset, data_size };
|
||||
|
||||
for (const auto &shortcode : shortcodes)
|
||||
|
||||
91
src/http.cpp
91
src/http.cpp
@@ -2,6 +2,8 @@
|
||||
|
||||
#include <utility>
|
||||
|
||||
// #define USE_LOCAL_PROXY
|
||||
|
||||
namespace http {
|
||||
request::request(EMethod method, std::string url)
|
||||
: m_url(std::move(url)) {
|
||||
@@ -29,12 +31,52 @@ request::request(EMethod method, std::string url)
|
||||
prepare();
|
||||
}
|
||||
|
||||
request::request(request &&other) noexcept
|
||||
: m_curl(std::exchange(other.m_curl, nullptr))
|
||||
, m_url(std::exchange(other.m_url, ""))
|
||||
, m_method(std::exchange(other.m_method, nullptr))
|
||||
, m_header_list(std::exchange(other.m_header_list, nullptr))
|
||||
, m_form(std::exchange(other.m_form, nullptr))
|
||||
, m_read_streams(std::move(other.m_read_streams))
|
||||
, m_progress_callback(std::move(other.m_progress_callback)) {
|
||||
if (m_progress_callback) {
|
||||
curl_easy_setopt(m_curl, CURLOPT_XFERINFODATA, this);
|
||||
}
|
||||
}
|
||||
|
||||
request::~request() {
|
||||
if (m_curl != nullptr)
|
||||
curl_easy_cleanup(m_curl);
|
||||
|
||||
if (m_header_list != nullptr)
|
||||
curl_slist_free_all(m_header_list);
|
||||
|
||||
if (m_form != nullptr)
|
||||
curl_mime_free(m_form);
|
||||
}
|
||||
|
||||
const std::string &request::get_url() const {
|
||||
return m_url;
|
||||
}
|
||||
|
||||
const char *request::get_method() const {
|
||||
return m_method;
|
||||
}
|
||||
|
||||
size_t http_req_xferinfofunc(void *clientp, curl_off_t dltotal, curl_off_t dlnow, curl_off_t ultotal, curl_off_t ulnow) {
|
||||
if (ultotal > 0) {
|
||||
auto *req = reinterpret_cast<request *>(clientp);
|
||||
req->m_progress_callback(ultotal, ulnow);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
void request::set_progress_callback(std::function<void(curl_off_t, curl_off_t)> func) {
|
||||
m_progress_callback = std::move(func);
|
||||
curl_easy_setopt(m_curl, CURLOPT_NOPROGRESS, 0L);
|
||||
curl_easy_setopt(m_curl, CURLOPT_XFERINFOFUNCTION, http_req_xferinfofunc);
|
||||
curl_easy_setopt(m_curl, CURLOPT_XFERINFODATA, this);
|
||||
}
|
||||
|
||||
void request::set_verify_ssl(bool verify) {
|
||||
@@ -57,6 +99,46 @@ void request::set_user_agent(const std::string &data) {
|
||||
curl_easy_setopt(m_curl, CURLOPT_USERAGENT, data.c_str());
|
||||
}
|
||||
|
||||
CURL *request::get_curl() {
|
||||
return m_curl;
|
||||
}
|
||||
|
||||
void request::make_form() {
|
||||
m_form = curl_mime_init(m_curl);
|
||||
}
|
||||
|
||||
static size_t http_readfunc(char *buffer, size_t size, size_t nitems, void *arg) {
|
||||
auto stream = Glib::wrap(G_FILE_INPUT_STREAM(arg), true);
|
||||
int r = stream->read(buffer, size * nitems);
|
||||
if (r == -1) {
|
||||
// https://github.com/curl/curl/blob/ad9bc5976d6661cd5b03ebc379313bf657701c14/lib/mime.c#L724
|
||||
return size_t(-1);
|
||||
}
|
||||
return r;
|
||||
}
|
||||
|
||||
// file must exist until request completes
|
||||
void request::add_file(std::string_view name, const Glib::RefPtr<Gio::File> &file, std::string_view filename) {
|
||||
if (!file->query_exists()) return;
|
||||
|
||||
auto *field = curl_mime_addpart(m_form);
|
||||
curl_mime_name(field, name.data());
|
||||
auto info = file->query_info();
|
||||
auto stream = file->read();
|
||||
curl_mime_data_cb(field, info->get_size(), http_readfunc, nullptr, nullptr, stream->gobj());
|
||||
curl_mime_filename(field, filename.data());
|
||||
|
||||
// hold ref
|
||||
m_read_streams.insert(stream);
|
||||
}
|
||||
|
||||
// copied
|
||||
void request::add_field(std::string_view name, const char *data, size_t size) {
|
||||
auto *field = curl_mime_addpart(m_form);
|
||||
curl_mime_name(field, name.data());
|
||||
curl_mime_data(field, data, size);
|
||||
}
|
||||
|
||||
response request::execute() {
|
||||
if (m_curl == nullptr) {
|
||||
auto response = detail::make_response(m_url, EStatusCode::ClientErrorCURLInit);
|
||||
@@ -66,22 +148,25 @@ response request::execute() {
|
||||
detail::check_init();
|
||||
|
||||
std::string str;
|
||||
#ifdef USE_LOCAL_PROXY
|
||||
set_proxy("http://127.0.0.1:8888");
|
||||
set_verify_ssl(false);
|
||||
#endif
|
||||
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);
|
||||
if (m_form != nullptr)
|
||||
curl_easy_setopt(m_curl, CURLOPT_MIMEPOST, m_form);
|
||||
|
||||
CURLcode result = curl_easy_perform(m_curl);
|
||||
if (result != CURLE_OK) {
|
||||
auto response = detail::make_response(m_url, EStatusCode::ClientErrorCURLPerform);
|
||||
response.error_string = curl_easy_strerror(result);
|
||||
response.error_string += " " + std::string(m_error_buf);
|
||||
return response;
|
||||
}
|
||||
|
||||
|
||||
27
src/http.hpp
27
src/http.hpp
@@ -1,8 +1,10 @@
|
||||
#pragma once
|
||||
#include <array>
|
||||
#include <functional>
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <curl/curl.h>
|
||||
|
||||
// i regret not using snake case for everything oh well
|
||||
#include <giomm/file.h>
|
||||
|
||||
namespace http {
|
||||
enum EStatusCode : int {
|
||||
@@ -98,16 +100,30 @@ struct response {
|
||||
|
||||
struct request {
|
||||
request(EMethod method, std::string url);
|
||||
request(request &&other) noexcept;
|
||||
~request();
|
||||
|
||||
request(const request &) = delete;
|
||||
request &operator=(const request &) = delete;
|
||||
request &operator=(request &&) noexcept = delete;
|
||||
|
||||
const std::string &get_url() const;
|
||||
const char *get_method() const;
|
||||
|
||||
void set_progress_callback(std::function<void(curl_off_t, curl_off_t)> func);
|
||||
void set_verify_ssl(bool verify);
|
||||
void set_proxy(const std::string &proxy);
|
||||
void set_header(const std::string &name, const std::string &value);
|
||||
void set_body(const std::string &data);
|
||||
void set_user_agent(const std::string &data);
|
||||
void make_form();
|
||||
void add_file(std::string_view name, const Glib::RefPtr<Gio::File> &file, std::string_view filename);
|
||||
void add_field(std::string_view name, const char *data, size_t size);
|
||||
|
||||
response execute();
|
||||
|
||||
CURL *get_curl();
|
||||
|
||||
private:
|
||||
void prepare();
|
||||
|
||||
@@ -115,7 +131,12 @@ private:
|
||||
std::string m_url;
|
||||
const char *m_method;
|
||||
curl_slist *m_header_list = nullptr;
|
||||
char m_error_buf[CURL_ERROR_SIZE] = { 0 };
|
||||
curl_mime *m_form = nullptr;
|
||||
std::function<void(curl_off_t, curl_off_t)> m_progress_callback;
|
||||
|
||||
std::set<Glib::RefPtr<Gio::FileInputStream>> m_read_streams;
|
||||
|
||||
friend size_t http_req_xferinfofunc(void *, curl_off_t, curl_off_t, curl_off_t, curl_off_t);
|
||||
};
|
||||
|
||||
using response_type = response;
|
||||
|
||||
@@ -48,6 +48,7 @@ void SettingsManager::ReadSettings() {
|
||||
SMBOOL("gui", "save_state", SaveState);
|
||||
SMBOOL("gui", "stock_emojis", ShowStockEmojis);
|
||||
SMBOOL("gui", "unreads", Unreads);
|
||||
SMBOOL("gui", "hide_to_tray", HideToTray);
|
||||
SMINT("http", "concurrent", CacheHTTPConcurrency);
|
||||
SMSTR("http", "user_agent", UserAgent);
|
||||
SMSTR("style", "expandercolor", ChannelsExpanderColor);
|
||||
@@ -101,6 +102,7 @@ void SettingsManager::Close() {
|
||||
SMBOOL("gui", "save_state", SaveState);
|
||||
SMBOOL("gui", "stock_emojis", ShowStockEmojis);
|
||||
SMBOOL("gui", "unreads", Unreads);
|
||||
SMBOOL("gui", "hide_to_tray", HideToTray);
|
||||
SMINT("http", "concurrent", CacheHTTPConcurrency);
|
||||
SMSTR("http", "user_agent", UserAgent);
|
||||
SMSTR("style", "expandercolor", ChannelsExpanderColor);
|
||||
|
||||
@@ -28,6 +28,8 @@ public:
|
||||
#endif
|
||||
bool Unreads { true };
|
||||
|
||||
bool HideToTray { false };
|
||||
|
||||
// [http]
|
||||
int CacheHTTPConcurrency { 20 };
|
||||
std::string UserAgent { "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.87 Safari/537.36" };
|
||||
|
||||
126
src/startup.cpp
Normal file
126
src/startup.cpp
Normal file
@@ -0,0 +1,126 @@
|
||||
#include "startup.hpp"
|
||||
#include "abaddon.hpp"
|
||||
#include <future>
|
||||
#include <memory>
|
||||
|
||||
DiscordStartupDialog::DiscordStartupDialog(Gtk::Window &window)
|
||||
: Gtk::MessageDialog(window, "", false, Gtk::MESSAGE_INFO, Gtk::BUTTONS_NONE, true) {
|
||||
m_dispatcher.connect(sigc::mem_fun(*this, &DiscordStartupDialog::DispatchCallback));
|
||||
|
||||
property_text() = "Getting connection info...";
|
||||
|
||||
RunAsync();
|
||||
}
|
||||
|
||||
std::optional<std::string> DiscordStartupDialog::GetCookie() const {
|
||||
return m_cookie;
|
||||
}
|
||||
|
||||
std::optional<uint32_t> DiscordStartupDialog::GetBuildNumber() const {
|
||||
return m_build_number;
|
||||
}
|
||||
|
||||
// good enough
|
||||
std::optional<std::pair<std::string, std::string>> ParseCookie(const Glib::ustring &str) {
|
||||
auto regex = Glib::Regex::create("\\t");
|
||||
const std::vector<Glib::ustring> split = regex->split(str);
|
||||
if (split.size() < 7) return {};
|
||||
|
||||
return { { split[5], split[6] } };
|
||||
}
|
||||
|
||||
std::optional<Glib::ustring> GetJavascriptFileFromAppPage(const Glib::ustring &contents) {
|
||||
auto regex = Glib::Regex::create(R"(app-mount.*(/assets/[\w\d]*.js).*/assets/[\w\d]*.js)");
|
||||
Glib::MatchInfo match;
|
||||
if (regex->match(contents, match)) {
|
||||
return match.fetch(1);
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
std::optional<uint32_t> GetBuildNumberFromJSURL(const Glib::ustring &url, const std::string &cookie) {
|
||||
http::request req(http::REQUEST_GET, "https://discord.com" + url);
|
||||
req.set_header("Accept-Language", "en-US,en;q=0.9");
|
||||
req.set_header("Sec-Fetch-Dest", "document");
|
||||
req.set_header("Sec-Fetch-Mode", "navigate");
|
||||
req.set_header("Sec-Fetch-Site", "none");
|
||||
req.set_header("Sec-Fetch-User", "?1");
|
||||
req.set_user_agent(Abaddon::Get().GetSettings().UserAgent);
|
||||
|
||||
curl_easy_setopt(req.get_curl(), CURLOPT_COOKIE, cookie.c_str());
|
||||
|
||||
auto res = req.execute();
|
||||
if (res.error) return {};
|
||||
|
||||
auto regex = Glib::Regex::create(R"("buildNumber",null!==\(t="(\d+)\"\))");
|
||||
Glib::MatchInfo match;
|
||||
if (regex->match(res.text, match)) {
|
||||
const auto str_value = match.fetch(1);
|
||||
try {
|
||||
return std::stoul(str_value);
|
||||
} catch (...) { return {}; }
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
std::pair<std::optional<std::string>, std::string> GetCookieTask() {
|
||||
http::request req(http::REQUEST_GET, "https://discord.com/app");
|
||||
req.set_header("Accept-Language", "en-US,en;q=0.9");
|
||||
req.set_header("Sec-Fetch-Dest", "document");
|
||||
req.set_header("Sec-Fetch-Mode", "navigate");
|
||||
req.set_header("Sec-Fetch-Site", "none");
|
||||
req.set_header("Sec-Fetch-User", "?1");
|
||||
req.set_user_agent(Abaddon::Get().GetSettings().UserAgent);
|
||||
|
||||
curl_easy_setopt(req.get_curl(), CURLOPT_COOKIEFILE, "");
|
||||
|
||||
auto res = req.execute();
|
||||
if (res.error) return {};
|
||||
|
||||
curl_slist *slist;
|
||||
if (curl_easy_getinfo(req.get_curl(), CURLINFO_COOKIELIST, &slist) != CURLE_OK) {
|
||||
return {};
|
||||
}
|
||||
|
||||
std::string dcfduid;
|
||||
std::string sdcfduid;
|
||||
|
||||
for (auto *cur = slist; cur != nullptr; cur = cur->next) {
|
||||
const auto cookie = ParseCookie(cur->data);
|
||||
if (cookie.has_value()) {
|
||||
if (cookie->first == "__dcfduid") {
|
||||
dcfduid = cookie->second;
|
||||
} else if (cookie->first == "__sdcfduid") {
|
||||
sdcfduid = cookie->second;
|
||||
}
|
||||
}
|
||||
}
|
||||
curl_slist_free_all(slist);
|
||||
|
||||
if (!dcfduid.empty() && !sdcfduid.empty()) {
|
||||
return { "__dcfduid=" + dcfduid + "; __sdcfduid=" + sdcfduid, res.text };
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
void DiscordStartupDialog::RunAsync() {
|
||||
auto futptr = std::make_shared<std::future<void>>();
|
||||
*futptr = std::async(std::launch::async, [this, futptr] {
|
||||
auto [opt_cookie, app_page] = GetCookieTask();
|
||||
m_cookie = opt_cookie;
|
||||
if (opt_cookie.has_value()) {
|
||||
auto js_url = GetJavascriptFileFromAppPage(app_page);
|
||||
if (js_url.has_value()) {
|
||||
m_build_number = GetBuildNumberFromJSURL(*js_url, *opt_cookie);
|
||||
}
|
||||
}
|
||||
m_dispatcher.emit();
|
||||
});
|
||||
}
|
||||
|
||||
void DiscordStartupDialog::DispatchCallback() {
|
||||
response(Gtk::RESPONSE_OK);
|
||||
}
|
||||
25
src/startup.hpp
Normal file
25
src/startup.hpp
Normal file
@@ -0,0 +1,25 @@
|
||||
#pragma once
|
||||
#include <glibmm/dispatcher.h>
|
||||
#include <gtkmm/messagedialog.h>
|
||||
#include <gtkmm/window.h>
|
||||
#include <optional>
|
||||
|
||||
// fetch cookies, build number async
|
||||
|
||||
class DiscordStartupDialog : public Gtk::MessageDialog {
|
||||
public:
|
||||
DiscordStartupDialog(Gtk::Window &window);
|
||||
|
||||
[[nodiscard]] std::optional<std::string> GetCookie() const;
|
||||
[[nodiscard]] std::optional<uint32_t> GetBuildNumber() const;
|
||||
|
||||
private:
|
||||
void RunAsync();
|
||||
|
||||
void DispatchCallback();
|
||||
|
||||
Glib::Dispatcher m_dispatcher;
|
||||
|
||||
std::optional<std::string> m_cookie;
|
||||
std::optional<uint32_t> m_build_number;
|
||||
};
|
||||
@@ -24,9 +24,18 @@ void from_json(const nlohmann::json &j, ExpansionState &m) {
|
||||
j.at("c").get_to(m.Children);
|
||||
}
|
||||
|
||||
void to_json(nlohmann::json &j, const TabsState &m) {
|
||||
j = m.Channels;
|
||||
}
|
||||
|
||||
void from_json(const nlohmann::json &j, TabsState &m) {
|
||||
j.get_to(m.Channels);
|
||||
}
|
||||
|
||||
void to_json(nlohmann::json &j, const AbaddonApplicationState &m) {
|
||||
j["active_channel"] = m.ActiveChannel;
|
||||
j["expansion"] = m.Expansion;
|
||||
j["tabs"] = m.Tabs;
|
||||
}
|
||||
|
||||
void from_json(const nlohmann::json &j, AbaddonApplicationState &m) {
|
||||
@@ -34,4 +43,6 @@ void from_json(const nlohmann::json &j, AbaddonApplicationState &m) {
|
||||
j.at("active_channel").get_to(m.ActiveChannel);
|
||||
if (j.contains("expansion"))
|
||||
j.at("expansion").get_to(m.Expansion);
|
||||
if (j.contains("tabs"))
|
||||
j.at("tabs").get_to(m.Tabs);
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
#pragma once
|
||||
#include <vector>
|
||||
#include <nlohmann/json.hpp>
|
||||
#include "discord/snowflake.hpp"
|
||||
@@ -18,9 +19,17 @@ struct ExpansionState {
|
||||
friend void from_json(const nlohmann::json &j, ExpansionState &m);
|
||||
};
|
||||
|
||||
struct TabsState {
|
||||
std::vector<Snowflake> Channels;
|
||||
|
||||
friend void to_json(nlohmann::json &j, const TabsState &m);
|
||||
friend void from_json(const nlohmann::json &j, TabsState &m);
|
||||
};
|
||||
|
||||
struct AbaddonApplicationState {
|
||||
Snowflake ActiveChannel;
|
||||
ExpansionStateRoot Expansion;
|
||||
TabsState Tabs;
|
||||
|
||||
friend void to_json(nlohmann::json &j, const AbaddonApplicationState &m);
|
||||
friend void from_json(const nlohmann::json &j, AbaddonApplicationState &m);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include "util.hpp"
|
||||
#include <array>
|
||||
#include <cstring>
|
||||
#include <filesystem>
|
||||
|
||||
void LaunchBrowser(const Glib::ustring &url) {
|
||||
|
||||
@@ -56,10 +56,9 @@ GuildSettingsInfoPane::GuildSettingsInfoPane(Snowflake id)
|
||||
guild_icon_url = guild.GetIconURL("gif", "512");
|
||||
else
|
||||
guild_icon_url = guild.GetIconURL("png", "512");
|
||||
m_guild_icon_ev.signal_button_press_event().connect([guild_icon_url](GdkEventButton *event) -> bool {
|
||||
if (event->type == GDK_BUTTON_PRESS)
|
||||
if (event->button == GDK_BUTTON_PRIMARY)
|
||||
LaunchBrowser(guild_icon_url);
|
||||
m_guild_icon_ev.signal_button_release_event().connect([guild_icon_url](GdkEventButton *event) -> bool {
|
||||
if (event->type == GDK_BUTTON_RELEASE && event->button == GDK_BUTTON_PRIMARY)
|
||||
LaunchBrowser(guild_icon_url);
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
@@ -204,7 +204,7 @@ void GuildSettingsMembersPaneInfo::SetUser(Snowflake user_id) {
|
||||
auto member = *discord.GetMember(user_id, GuildID);
|
||||
member.User = discord.GetUser(user_id);
|
||||
|
||||
m_bot.set_visible(member.User->IsBot.has_value() && *member.User->IsBot);
|
||||
m_bot.set_visible(member.User->IsABot());
|
||||
|
||||
m_id.set_text("User ID: " + std::to_string(user_id));
|
||||
m_created.set_text("Account created: " + user_id.GetLocalTimestamp());
|
||||
|
||||
@@ -27,6 +27,12 @@ MainWindow::MainWindow()
|
||||
chat->set_hexpand(true);
|
||||
chat->show();
|
||||
|
||||
#ifdef WITH_LIBHANDY
|
||||
m_channel_list.signal_action_open_new_tab().connect([this](Snowflake id) {
|
||||
m_chat.OpenNewTab(id);
|
||||
});
|
||||
#endif
|
||||
|
||||
m_channel_list.set_vexpand(true);
|
||||
m_channel_list.set_size_request(-1, -1);
|
||||
m_channel_list.show();
|
||||
@@ -47,8 +53,10 @@ MainWindow::MainWindow()
|
||||
|
||||
m_chan_content_paned.pack1(m_channel_list);
|
||||
m_chan_content_paned.pack2(m_content_members_paned);
|
||||
m_chan_content_paned.child_property_shrink(m_channel_list) = false;
|
||||
m_chan_content_paned.child_property_resize(m_channel_list) = false;
|
||||
m_chan_content_paned.child_property_shrink(m_content_members_paned) = true;
|
||||
m_chan_content_paned.child_property_resize(m_content_members_paned) = true;
|
||||
m_chan_content_paned.child_property_shrink(m_channel_list) = true;
|
||||
m_chan_content_paned.child_property_resize(m_channel_list) = true;
|
||||
m_chan_content_paned.set_position(200);
|
||||
m_chan_content_paned.show();
|
||||
m_content_box.add(m_chan_content_paned);
|
||||
@@ -56,8 +64,10 @@ MainWindow::MainWindow()
|
||||
|
||||
m_content_members_paned.pack1(m_content_stack);
|
||||
m_content_members_paned.pack2(*member_list);
|
||||
m_content_members_paned.child_property_shrink(*member_list) = false;
|
||||
m_content_members_paned.child_property_resize(*member_list) = false;
|
||||
m_content_members_paned.child_property_shrink(m_content_stack) = true;
|
||||
m_content_members_paned.child_property_resize(m_content_stack) = true;
|
||||
m_content_members_paned.child_property_shrink(*member_list) = true;
|
||||
m_content_members_paned.child_property_resize(*member_list) = true;
|
||||
int w, h;
|
||||
get_default_size(w, h); // :s
|
||||
m_content_members_paned.set_position(w - m_chan_content_paned.get_position() - 150);
|
||||
@@ -66,6 +76,7 @@ MainWindow::MainWindow()
|
||||
add(m_main_box);
|
||||
|
||||
SetupMenu();
|
||||
SetupDND();
|
||||
}
|
||||
|
||||
void MainWindow::UpdateComponents() {
|
||||
@@ -95,10 +106,10 @@ void MainWindow::UpdateChatWindowContents() {
|
||||
m_members.UpdateMemberList();
|
||||
}
|
||||
|
||||
void MainWindow::UpdateChatActiveChannel(Snowflake id) {
|
||||
void MainWindow::UpdateChatActiveChannel(Snowflake id, bool expand_to) {
|
||||
m_chat.SetActiveChannel(id);
|
||||
m_members.SetActiveChannel(id);
|
||||
m_channel_list.SetActiveChannel(id);
|
||||
m_channel_list.SetActiveChannel(id, expand_to);
|
||||
m_content_stack.set_visible_child("chat");
|
||||
}
|
||||
|
||||
@@ -142,6 +153,33 @@ void MainWindow::UpdateChatReactionRemove(Snowflake id, const Glib::ustring &par
|
||||
m_chat.UpdateReactions(id);
|
||||
}
|
||||
|
||||
void MainWindow::UpdateMenus() {
|
||||
OnDiscordSubmenuPopup();
|
||||
OnViewSubmenuPopup();
|
||||
}
|
||||
|
||||
#ifdef WITH_LIBHANDY
|
||||
void MainWindow::GoBack() {
|
||||
m_chat.GoBack();
|
||||
}
|
||||
|
||||
void MainWindow::GoForward() {
|
||||
m_chat.GoForward();
|
||||
}
|
||||
|
||||
void MainWindow::GoToPreviousTab() {
|
||||
m_chat.GoToPreviousTab();
|
||||
}
|
||||
|
||||
void MainWindow::GoToNextTab() {
|
||||
m_chat.GoToNextTab();
|
||||
}
|
||||
|
||||
void MainWindow::GoToTab(int idx) {
|
||||
m_chat.GoToTab(idx);
|
||||
}
|
||||
#endif
|
||||
|
||||
void MainWindow::OnDiscordSubmenuPopup() {
|
||||
auto &discord = Abaddon::Get().GetDiscordClient();
|
||||
auto channel_id = GetChatActiveChannel();
|
||||
@@ -157,7 +195,6 @@ void MainWindow::OnDiscordSubmenuPopup() {
|
||||
std::string token = Abaddon::Get().GetDiscordToken();
|
||||
m_menu_discord_connect.set_sensitive(!token.empty() && !discord_active);
|
||||
m_menu_discord_disconnect.set_sensitive(discord_active);
|
||||
m_menu_discord_join_guild.set_sensitive(discord_active);
|
||||
m_menu_discord_set_token.set_sensitive(!discord_active);
|
||||
m_menu_discord_set_status.set_sensitive(discord_active);
|
||||
}
|
||||
@@ -200,15 +237,12 @@ void MainWindow::SetupMenu() {
|
||||
m_menu_discord_disconnect.set_label("Disconnect");
|
||||
m_menu_discord_disconnect.set_sensitive(false);
|
||||
m_menu_discord_set_token.set_label("Set Token");
|
||||
m_menu_discord_join_guild.set_label("Accept Invite");
|
||||
m_menu_discord_join_guild.set_sensitive(false);
|
||||
m_menu_discord_set_status.set_label("Set Status");
|
||||
m_menu_discord_set_status.set_sensitive(false);
|
||||
m_menu_discord_add_recipient.set_label("Add user to DM");
|
||||
m_menu_discord_sub.append(m_menu_discord_connect);
|
||||
m_menu_discord_sub.append(m_menu_discord_disconnect);
|
||||
m_menu_discord_sub.append(m_menu_discord_set_token);
|
||||
m_menu_discord_sub.append(m_menu_discord_join_guild);
|
||||
m_menu_discord_sub.append(m_menu_discord_set_status);
|
||||
m_menu_discord_sub.append(m_menu_discord_add_recipient);
|
||||
m_menu_discord.set_submenu(m_menu_discord_sub);
|
||||
@@ -227,22 +261,26 @@ void MainWindow::SetupMenu() {
|
||||
m_menu_view_threads.set_label("Threads");
|
||||
m_menu_view_mark_guild_as_read.set_label("Mark Server as Read");
|
||||
m_menu_view_mark_guild_as_read.add_accelerator("activate", m_accels, GDK_KEY_Escape, Gdk::SHIFT_MASK, Gtk::ACCEL_VISIBLE);
|
||||
#ifdef WITH_LIBHANDY
|
||||
m_menu_view_go_back.set_label("Go Back");
|
||||
m_menu_view_go_forward.set_label("Go Forward");
|
||||
m_menu_view_go_back.add_accelerator("activate", m_accels, GDK_KEY_Left, Gdk::MOD1_MASK, Gtk::ACCEL_VISIBLE);
|
||||
m_menu_view_go_forward.add_accelerator("activate", m_accels, GDK_KEY_Right, Gdk::MOD1_MASK, Gtk::ACCEL_VISIBLE);
|
||||
#endif
|
||||
m_menu_view_sub.append(m_menu_view_friends);
|
||||
m_menu_view_sub.append(m_menu_view_pins);
|
||||
m_menu_view_sub.append(m_menu_view_threads);
|
||||
m_menu_view_sub.append(m_menu_view_mark_guild_as_read);
|
||||
#ifdef WITH_LIBHANDY
|
||||
m_menu_view_sub.append(m_menu_view_go_back);
|
||||
m_menu_view_sub.append(m_menu_view_go_forward);
|
||||
#endif
|
||||
|
||||
m_menu_bar.append(m_menu_file);
|
||||
m_menu_bar.append(m_menu_discord);
|
||||
m_menu_bar.append(m_menu_view);
|
||||
m_menu_bar.show_all();
|
||||
|
||||
m_menu_bar.signal_event().connect([this](GdkEvent *ev) -> bool {
|
||||
OnViewSubmenuPopup();
|
||||
OnDiscordSubmenuPopup();
|
||||
return false;
|
||||
});
|
||||
|
||||
m_menu_discord_connect.signal_activate().connect([this] {
|
||||
m_signal_action_connect.emit();
|
||||
});
|
||||
@@ -255,10 +293,6 @@ void MainWindow::SetupMenu() {
|
||||
m_signal_action_set_token.emit();
|
||||
});
|
||||
|
||||
m_menu_discord_join_guild.signal_activate().connect([this] {
|
||||
m_signal_action_join_guild.emit();
|
||||
});
|
||||
|
||||
m_menu_file_reload_css.signal_activate().connect([this] {
|
||||
m_signal_action_reload_css.emit();
|
||||
});
|
||||
@@ -276,7 +310,7 @@ void MainWindow::SetupMenu() {
|
||||
});
|
||||
|
||||
m_menu_view_friends.signal_activate().connect([this] {
|
||||
UpdateChatActiveChannel(Snowflake::Invalid);
|
||||
UpdateChatActiveChannel(Snowflake::Invalid, true);
|
||||
m_members.UpdateMemberList();
|
||||
m_content_stack.set_visible_child("friends");
|
||||
});
|
||||
@@ -297,6 +331,32 @@ void MainWindow::SetupMenu() {
|
||||
discord.MarkGuildAsRead(*channel->GuildID, NOOP_CALLBACK);
|
||||
}
|
||||
});
|
||||
|
||||
#ifdef WITH_LIBHANDY
|
||||
m_menu_view_go_back.signal_activate().connect([this] {
|
||||
GoBack();
|
||||
});
|
||||
|
||||
m_menu_view_go_forward.signal_activate().connect([this] {
|
||||
GoForward();
|
||||
});
|
||||
#endif
|
||||
}
|
||||
|
||||
void MainWindow::SetupDND() {
|
||||
std::vector<Gtk::TargetEntry> targets;
|
||||
targets.emplace_back("text/uri-list", Gtk::TargetFlags(0), 0);
|
||||
drag_dest_set(targets, Gtk::DEST_DEFAULT_DROP | Gtk::DEST_DEFAULT_MOTION | Gtk::DEST_DEFAULT_HIGHLIGHT, Gdk::DragAction::ACTION_COPY);
|
||||
signal_drag_data_received().connect([this](const Glib::RefPtr<Gdk::DragContext> &ctx, int x, int y, const Gtk::SelectionData &selection, guint info, guint time) {
|
||||
HandleDroppedURIs(selection);
|
||||
});
|
||||
}
|
||||
|
||||
void MainWindow::HandleDroppedURIs(const Gtk::SelectionData &selection) {
|
||||
for (const auto &uri : selection.get_uris()) {
|
||||
// not using Glib::get_filename_for_uri or whatever because the conversion is BAD (on windows at least)
|
||||
m_chat.AddAttachment(Gio::File::create_for_uri(uri));
|
||||
}
|
||||
}
|
||||
|
||||
MainWindow::type_signal_action_connect MainWindow::signal_action_connect() {
|
||||
@@ -315,10 +375,6 @@ MainWindow::type_signal_action_reload_css MainWindow::signal_action_reload_css()
|
||||
return m_signal_action_reload_css;
|
||||
}
|
||||
|
||||
MainWindow::type_signal_action_join_guild MainWindow::signal_action_join_guild() {
|
||||
return m_signal_action_join_guild;
|
||||
}
|
||||
|
||||
MainWindow::type_signal_action_set_status MainWindow::signal_action_set_status() {
|
||||
return m_signal_action_set_status;
|
||||
}
|
||||
@@ -333,4 +389,4 @@ MainWindow::type_signal_action_view_pins MainWindow::signal_action_view_pins() {
|
||||
|
||||
MainWindow::type_signal_action_view_threads MainWindow::signal_action_view_threads() {
|
||||
return m_signal_action_view_threads;
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,7 @@ public:
|
||||
void UpdateMembers();
|
||||
void UpdateChannelListing();
|
||||
void UpdateChatWindowContents();
|
||||
void UpdateChatActiveChannel(Snowflake id);
|
||||
void UpdateChatActiveChannel(Snowflake id, bool expand_to);
|
||||
Snowflake GetChatActiveChannel() const;
|
||||
void UpdateChatNewMessage(const Message &data);
|
||||
void UpdateChatMessageDeleted(Snowflake id, Snowflake channel_id);
|
||||
@@ -23,6 +23,15 @@ public:
|
||||
Snowflake GetChatOldestListedMessage();
|
||||
void UpdateChatReactionAdd(Snowflake id, const Glib::ustring ¶m);
|
||||
void UpdateChatReactionRemove(Snowflake id, const Glib::ustring ¶m);
|
||||
void UpdateMenus();
|
||||
|
||||
#ifdef WITH_LIBHANDY
|
||||
void GoBack();
|
||||
void GoForward();
|
||||
void GoToPreviousTab();
|
||||
void GoToNextTab();
|
||||
void GoToTab(int idx);
|
||||
#endif
|
||||
|
||||
ChannelList *GetChannelList();
|
||||
ChatWindow *GetChatWindow();
|
||||
@@ -30,6 +39,9 @@ public:
|
||||
|
||||
private:
|
||||
void SetupMenu();
|
||||
void SetupDND();
|
||||
|
||||
void HandleDroppedURIs(const Gtk::SelectionData &selection);
|
||||
|
||||
Gtk::Box m_main_box;
|
||||
Gtk::Box m_content_box;
|
||||
@@ -51,7 +63,6 @@ private:
|
||||
Gtk::MenuItem m_menu_discord_connect;
|
||||
Gtk::MenuItem m_menu_discord_disconnect;
|
||||
Gtk::MenuItem m_menu_discord_set_token;
|
||||
Gtk::MenuItem m_menu_discord_join_guild;
|
||||
Gtk::MenuItem m_menu_discord_set_status;
|
||||
Gtk::MenuItem m_menu_discord_add_recipient; // move me somewhere else some day
|
||||
void OnDiscordSubmenuPopup();
|
||||
@@ -67,6 +78,11 @@ private:
|
||||
Gtk::MenuItem m_menu_view_pins;
|
||||
Gtk::MenuItem m_menu_view_threads;
|
||||
Gtk::MenuItem m_menu_view_mark_guild_as_read;
|
||||
#ifdef WITH_LIBHANDY
|
||||
Gtk::MenuItem m_menu_view_go_back;
|
||||
Gtk::MenuItem m_menu_view_go_forward;
|
||||
#endif
|
||||
|
||||
void OnViewSubmenuPopup();
|
||||
|
||||
public:
|
||||
@@ -74,7 +90,6 @@ public:
|
||||
typedef sigc::signal<void> type_signal_action_disconnect;
|
||||
typedef sigc::signal<void> type_signal_action_set_token;
|
||||
typedef sigc::signal<void> type_signal_action_reload_css;
|
||||
typedef sigc::signal<void> type_signal_action_join_guild;
|
||||
typedef sigc::signal<void> type_signal_action_set_status;
|
||||
// this should probably be removed
|
||||
typedef sigc::signal<void, Snowflake> type_signal_action_add_recipient; // channel id
|
||||
@@ -85,7 +100,6 @@ public:
|
||||
type_signal_action_disconnect signal_action_disconnect();
|
||||
type_signal_action_set_token signal_action_set_token();
|
||||
type_signal_action_reload_css signal_action_reload_css();
|
||||
type_signal_action_join_guild signal_action_join_guild();
|
||||
type_signal_action_set_status signal_action_set_status();
|
||||
type_signal_action_add_recipient signal_action_add_recipient();
|
||||
type_signal_action_view_pins signal_action_view_pins();
|
||||
@@ -96,7 +110,6 @@ private:
|
||||
type_signal_action_disconnect m_signal_action_disconnect;
|
||||
type_signal_action_set_token m_signal_action_set_token;
|
||||
type_signal_action_reload_css m_signal_action_reload_css;
|
||||
type_signal_action_join_guild m_signal_action_join_guild;
|
||||
type_signal_action_set_status m_signal_action_set_status;
|
||||
type_signal_action_add_recipient m_signal_action_add_recipient;
|
||||
type_signal_action_view_pins m_signal_action_view_pins;
|
||||
|
||||
@@ -41,13 +41,13 @@ ConnectionItem::ConnectionItem(const ConnectionData &conn)
|
||||
m_box.add(m_name);
|
||||
if (!url.empty()) {
|
||||
auto cb = [url](GdkEventButton *event) -> bool {
|
||||
if (event->type == GDK_BUTTON_PRESS && event->button == GDK_BUTTON_PRIMARY) {
|
||||
if (event->type == GDK_BUTTON_RELEASE && event->button == GDK_BUTTON_PRIMARY) {
|
||||
LaunchBrowser(url);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
signal_button_press_event().connect(sigc::track_obj(cb, *this));
|
||||
signal_button_release_event().connect(sigc::track_obj(cb, *this));
|
||||
AddPointerCursor(*this);
|
||||
}
|
||||
m_overlay.add(m_box);
|
||||
|
||||
@@ -34,8 +34,8 @@ ProfileWindow::ProfileWindow(Snowflake user_id)
|
||||
|
||||
if (user.HasAvatar())
|
||||
AddPointerCursor(m_avatar_ev);
|
||||
m_avatar_ev.signal_button_press_event().connect([user](GdkEventButton *event) -> bool {
|
||||
if (event->type == GDK_BUTTON_PRESS && event->button == GDK_BUTTON_PRIMARY) {
|
||||
m_avatar_ev.signal_button_release_event().connect([user](GdkEventButton *event) -> bool {
|
||||
if (event->type == GDK_BUTTON_RELEASE && event->button == GDK_BUTTON_PRIMARY) {
|
||||
if (user.HasAnimatedAvatar())
|
||||
LaunchBrowser(user.GetAvatarURL("gif", "512"));
|
||||
else
|
||||
|
||||
Reference in New Issue
Block a user