forked from OpenGamers/abaddon
Compare commits
242 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5c148d19dd | ||
|
|
acb03642c2 | ||
|
|
ba2aea86f9 | ||
|
|
c704703b14 | ||
|
|
33a329f16a | ||
|
|
f0df06a795 | ||
|
|
9ae41b7335 | ||
|
|
b9fee0f6c9 | ||
|
|
92273829bb | ||
|
|
86f6f81d9b | ||
|
|
573a619191 | ||
|
|
c5807a3463 | ||
|
|
2a9f49a148 | ||
|
|
64245bf745 | ||
|
|
772598996c | ||
|
|
ccb82c1676 | ||
|
|
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 | ||
|
|
77dd9fabfa | ||
|
|
ee67037a3f | ||
|
|
b46cf53be5 | ||
|
|
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 | ||
|
|
c22a49f64e | ||
|
|
436024b4a0 | ||
|
|
61cde0f7e1 | ||
|
|
a9399873fd | ||
|
|
c2be1d3668 | ||
|
|
1d981d2c5a | ||
|
|
c2b7ca780e | ||
|
|
57e95c8969 | ||
|
|
56a74fb5dd | ||
|
|
49685c3989 | ||
|
|
9767e1e7fd | ||
|
|
a83e9c01a6 | ||
|
|
7b1ceeedf4 | ||
|
|
a0b3c9f8a4 | ||
|
|
a2a45757e9 | ||
|
|
271d21c7bd | ||
|
|
46b88566f1 | ||
|
|
af60bceada | ||
|
|
3583a5d251 | ||
|
|
4503aeabc4 | ||
|
|
7f1d3df4a5 | ||
|
|
17f1289c84 | ||
|
|
fc3d0fddd2 | ||
|
|
4bd5c89266 | ||
|
|
a0599ab812 | ||
|
|
6c54296ba3 | ||
|
|
7ed415040a | ||
|
|
011cb159cf | ||
|
|
25fd2c3840 | ||
|
|
7e3976785f | ||
|
|
75213fcede | ||
|
|
179ff980e9 | ||
|
|
f784550964 | ||
|
|
ce238d08e9 | ||
|
|
f9864a24ed | ||
|
|
738d50dd43 | ||
|
|
7d49f934bc | ||
|
|
fbb5522861 | ||
|
|
0ce509f80e | ||
|
|
b6b215ee6f | ||
|
|
d7f3ee9f98 | ||
|
|
2328c8bafe | ||
|
|
dfd642bb82 | ||
|
|
6c9bf4ff81 | ||
|
|
481685b3bb | ||
|
|
f31d431517 | ||
|
|
604f2ffe3d | ||
|
|
4e0b22375f | ||
|
|
9d0c7691d8 | ||
|
|
cef28e94ea | ||
|
|
40106ddeb1 | ||
|
|
8695562cb4 | ||
|
|
5338eab3a5 | ||
|
|
d7bb6049e1 | ||
|
|
ea7464722b | ||
|
|
d6da646d87 | ||
|
|
17c1f913df | ||
|
|
6c94e75513 | ||
|
|
801894abc6 | ||
|
|
207c004228 | ||
|
|
36f73a6106 | ||
|
|
41d80af128 | ||
|
|
145504bdd6 | ||
|
|
9fd0d404a1 | ||
|
|
b75599e55d | ||
|
|
67062d6ed8 | ||
|
|
c43d49ed54 | ||
|
|
e9867173c9 | ||
|
|
f580535d35 | ||
|
|
1d7529e609 | ||
|
|
1fb7ca0007 | ||
|
|
b576bd0fcc | ||
|
|
f19dcc0114 | ||
|
|
38a49d172c | ||
|
|
72935b0558 | ||
|
|
a5332efcfb | ||
|
|
15954830e2 | ||
|
|
46ab760a56 | ||
|
|
0b0135268e | ||
|
|
511fb445d1 | ||
|
|
bcfb2146cd | ||
|
|
a1b662a325 | ||
|
|
14b5bf7d0d | ||
|
|
d288989386 | ||
|
|
d63941797f | ||
|
|
1ea2811713 | ||
|
|
af56784797 | ||
|
|
2461406887 | ||
|
|
8e11dd97e9 | ||
|
|
2690febf20 | ||
|
|
af3d278825 | ||
|
|
c9647f9b33 | ||
|
|
e1703aea3f | ||
|
|
e02107feea | ||
|
|
192b043e7a | ||
|
|
fd53a76bf6 | ||
|
|
b5c1394662 | ||
|
|
66d7cb581c | ||
|
|
8f823420b6 | ||
|
|
36d5e011e8 | ||
|
|
5a4bcbf377 | ||
|
|
0fe007569e | ||
|
|
c49e454ec0 | ||
|
|
8afc8c62ef | ||
|
|
4644adff94 | ||
|
|
5e08083b5a | ||
|
|
95a8da803a | ||
|
|
43a41b34bc | ||
|
|
9c285a09e5 | ||
|
|
1f68da6b77 | ||
|
|
98e0e84d57 | ||
|
|
6ddba4363a | ||
|
|
b5b5c40ecd | ||
|
|
d84fe2b800 | ||
|
|
da561ba4d5 |
119
.github/workflows/ci.yml
vendored
119
.github/workflows/ci.yml
vendored
@@ -3,57 +3,103 @@ name: Abaddon CI
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
windows:
|
||||
name: windows-${{ matrix.buildtype }}
|
||||
msys2:
|
||||
name: msys2-mingw64
|
||||
runs-on: windows-latest
|
||||
strategy:
|
||||
matrix:
|
||||
buildtype: [Debug, RelWithDebInfo, MinSizeRel]
|
||||
mindeps: [false]
|
||||
include:
|
||||
- buildtype: RelWithDebInfo
|
||||
mindeps: true
|
||||
defaults:
|
||||
run:
|
||||
shell: msys2 {0}
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
with:
|
||||
submodules: true
|
||||
|
||||
- name: Fetch CMake
|
||||
uses: lukka/get-cmake@v3.21.2
|
||||
|
||||
- name: Fetch dependencies
|
||||
uses: lukka/run-vcpkg@v7
|
||||
- name: Setup MSYS2 (1)
|
||||
uses: haya14busa/action-cond@v1
|
||||
id: setupmsys
|
||||
with:
|
||||
vcpkgArguments: gtkmm nlohmann-json zlib sqlite3 glibmm openssl ixwebsocket curl
|
||||
vcpkgDirectory: ${{ github.workspace }}/ci/vcpkg/
|
||||
vcpkgTriplet: x64-windows
|
||||
cond: ${{ matrix.mindeps == true }}
|
||||
if_true: >-
|
||||
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
|
||||
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
|
||||
uses: lukka/run-cmake@v3
|
||||
with:
|
||||
useVcpkgToolchainFile: true
|
||||
vcpkgTriplet: x64-windows
|
||||
buildDirectory: ${{ runner.workspace }}/build
|
||||
cmakeBuildType: ${{ matrix.buildtype }}
|
||||
|
||||
- name: Setup artifact files
|
||||
shell: cmd
|
||||
run: |
|
||||
del /f /s /q "${{ runner.workspace }}\build\CMakeFiles"
|
||||
rmdir /s /q "${{ runner.workspace }}\build\CMakeFiles"
|
||||
del /f /s /q "${{ runner.workspace }}\build\.ninja_deps"
|
||||
del /f /s /q "${{ runner.workspace }}\build\.ninja_log"
|
||||
del /f /s /q "${{ runner.workspace }}\build\abaddon.ilk"
|
||||
del /f /s /q "${{ runner.workspace }}\build\CMakeCache.txt"
|
||||
xcopy /E /I "${{ github.workspace }}\res\css" "${{ runner.workspace }}\build\css"
|
||||
xcopy /E /I "${{ github.workspace }}\res\res" "${{ runner.workspace }}\build\res"
|
||||
xcopy /E /I "${{ github.workspace }}\res\fonts" "${{ runner.workspace }}\build\fonts"
|
||||
mkdir "${{ runner.workspace }}\build\share"
|
||||
xcopy /E /I "${{ github.workspace }}\ci\gtk-for-windows\gtk-nsis-pack\share\glib-2.0" "${{ runner.workspace }}\build\share\glib-2.0"
|
||||
copy "${{ github.workspace }}\ci\vcpkg\installed\x64-windows\tools\glib\gspawn-win64-helper.exe" "${{ runner.workspace }}\build\gspawn-win64-helper.exe"
|
||||
copy "${{ github.workspace }}\ci\vcpkg\installed\x64-windows\tools\glib\gspawn-win64-helper-console.exe" "${{ runner.workspace }}\build\gspawn-win64-helper-console.exe"
|
||||
cmake -GNinja -Bbuild -DCMAKE_BUILD_TYPE=${{ matrix.buildtype }}
|
||||
cmake --build build
|
||||
|
||||
- name: Upload build
|
||||
- name: Setup Artifact
|
||||
run: |
|
||||
mkdir -p build/artifactdir/bin build/artifactdir/ssl/certs build/artifactdir/lib build/artifactdir/share/glib-2.0/schemas
|
||||
cd build
|
||||
cp *.exe artifactdir/bin
|
||||
cd ..
|
||||
cp /mingw64/ssl/certs/ca-bundle.crt build/artifactdir/ssl/certs
|
||||
cp -r /mingw64/lib/gdk-pixbuf-2.0 build/artifactdir/lib
|
||||
cp -r res/css res/res res/fonts build/artifactdir/bin
|
||||
cp /mingw64/share/glib-2.0/schemas/gschemas.compiled build/artifactdir/share/glib-2.0/schemas
|
||||
cat "ci/msys-deps.txt" | sed 's/\r$//' | xargs -I % cp /mingw64% build/artifactdir/bin || :
|
||||
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 (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-${{ matrix.buildtype }}
|
||||
path: ${{ runner.workspace }}/build
|
||||
name: build-windows-msys2-${{ steps.buildname.outputs.value }}
|
||||
path: build/artifactdir
|
||||
|
||||
mac:
|
||||
name: macos-${{ matrix.buildtype }}
|
||||
runs-on: macos-latest
|
||||
@@ -72,6 +118,7 @@ jobs:
|
||||
run: |
|
||||
brew install gtkmm3
|
||||
brew install nlohmann-json
|
||||
brew install jpeg
|
||||
|
||||
- name: Build
|
||||
uses: lukka/run-cmake@v3
|
||||
@@ -113,7 +160,7 @@ jobs:
|
||||
cd deps
|
||||
git clone https://github.com/nlohmann/json
|
||||
cd json
|
||||
git checkout db78ac1d7716f56fc9f1b030b715f872f93964e4
|
||||
git checkout bc889afb4c5bf1c0d8ee29ef35eaaf4c8bef8a5d
|
||||
mkdir build
|
||||
cd build
|
||||
cmake ..
|
||||
|
||||
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
|
||||
|
||||
9
.gitmodules
vendored
9
.gitmodules
vendored
@@ -1,12 +1,9 @@
|
||||
[submodule "vcpkg"]
|
||||
path = ci/vcpkg
|
||||
url = https://github.com/microsoft/vcpkg/
|
||||
[submodule "ci/vcpkg"]
|
||||
path = ci/vcpkg
|
||||
url = https://github.com/microsoft/vcpkg
|
||||
[submodule "ci/gtk-for-windows"]
|
||||
path = ci/gtk-for-windows
|
||||
url = https://github.com/tschoonj/GTK-for-Windows-Runtime-Environment-Installer
|
||||
[submodule "subprojects/ixwebsocket"]
|
||||
path = subprojects/ixwebsocket
|
||||
url = https://github.com/machinezone/ixwebsocket
|
||||
[submodule "subprojects/keychain"]
|
||||
path = subprojects/keychain
|
||||
url = https://github.com/hrantzsch/keychain
|
||||
|
||||
111
CMakeLists.txt
111
CMakeLists.txt
@@ -7,6 +7,9 @@ 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)
|
||||
option(USE_KEYCHAIN "Store the token in the keychain (default)" ON)
|
||||
|
||||
find_package(nlohmann_json REQUIRED)
|
||||
find_package(CURL)
|
||||
find_package(ZLIB REQUIRED)
|
||||
@@ -17,30 +20,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 +57,63 @@ 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 ()
|
||||
|
||||
if (USE_KEYCHAIN)
|
||||
find_package(keychain QUIET)
|
||||
if (NOT keychain_FOUND)
|
||||
message("keychain was not found and will be included as a submodule")
|
||||
add_subdirectory(subprojects/keychain)
|
||||
target_link_libraries(abaddon keychain)
|
||||
target_compile_definitions(abaddon PRIVATE WITH_KEYCHAIN)
|
||||
endif ()
|
||||
endif ()
|
||||
|
||||
354
README.md
354
README.md
@@ -7,11 +7,13 @@ Alternative Discord client made in C++ with GTK
|
||||
<a href="https://discord.gg/wkCU3vuzG5"><img src="https://discord.com/api/guilds/858156817711890443/widget.png?style=shield"></a>
|
||||
|
||||
Current features:
|
||||
|
||||
* Not Electron
|
||||
* Handles most types of chat messages including embeds, images, and replies
|
||||
* Completely styleable/customizable with CSS (if you have a system GTK theme it won't really use it though)
|
||||
* Identifies to Discord as the web client unlike other clients so less likely to be falsely flagged as spam<sup>1</sup>
|
||||
* Set status
|
||||
* Unread and mention indicators
|
||||
* Start new DMs and group DMs
|
||||
* View user profiles (notes, mutual servers, mutual friends)
|
||||
* Kick, ban, and unban members
|
||||
@@ -22,33 +24,66 @@ Current features:
|
||||
* Emojis<sup>2</sup>
|
||||
* Thread support<sup>3</sup>
|
||||
* Animated avatars, server icons, emojis (can be turned off)
|
||||
|
||||
1 - Abaddon tries its best to make Discord think it's a legitimate web client. Some of the things done to do this include: using a browser user agent, sending the same IDENTIFY message that the official web client does, using API v9 endpoints in all cases, and not using endpoints the web client does not normally use. There are still a few smaller inconsistencies, however. For example the web client sends lots of telemetry via the `/science` endpoint (uBlock origin stops this) as well as in the headers of all requests. **In any case,** you should use an official client for joining servers, sending new DMs, or managing your friends list if you are afraid of being caught in Discord's spam filters (unlikely).
|
||||
|
||||
2 - Unicode emojis are substituted manually as opposed to rendered by GTK on non-Windows platforms. This can be changed with the `stock_emojis` setting as shown at the bottom of this README. A CBDT-based font using Twemoji is provided to allow GTK to render emojis natively on Windows.
|
||||
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.<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
|
||||
allow GTK to render emojis natively on Windows.
|
||||
|
||||
3 - There are some inconsistencies with thread state that might be encountered in some more uncommon cases, but they are
|
||||
the result of fundamental issues with Discord's thread implementation.
|
||||
|
||||
3 - There are some inconsistencies with thread state that might be encountered in some more uncommon cases, but they are the result of fundamental issues with Discord's thread implementation.
|
||||
|
||||
### Building manually (recommended if not on Windows):
|
||||
#### Windows:
|
||||
1. `git clone https://github.com/uowuo/abaddon && cd abaddon`
|
||||
2. `vcpkg install gtkmm:x64-windows nlohmann-json:x64-windows ixwebsocket:x64-windows zlib:x64-windows sqlite3:x64-windows openssl:x64-windows curl:x64-windows`
|
||||
|
||||
#### Windows (with MSYS2):
|
||||
|
||||
1. Install following packages:
|
||||
* mingw-w64-x86_64-cmake
|
||||
* mingw-w64-x86_64-ninja
|
||||
* mingw-w64-x86_64-sqlite3
|
||||
* mingw-w64-x86_64-nlohmann-json
|
||||
* mingw-w64-x86_64-curl
|
||||
* mingw-w64-x86_64-zlib
|
||||
* mingw-w64-x86_64-gtkmm3
|
||||
* 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 -G"Visual Studio 16 2019" -A x64 -DCMAKE_TOOLCHAIN_FILE=c:\path\to\vcpkg\scripts\buildsystems\vcpkg.cmake -DCMAKE_BUILD_TYPE=RelWithDebInfo -DVCPKG_TARGET_TRIPLET=x64-windows ..`
|
||||
5. Build with Visual Studio
|
||||
|
||||
Or, do steps 1 and 2, and open CMakeLists.txt in Visual Studio if `vcpkg integrate install` was run
|
||||
4. `cmake -GNinja -DCMAKE_BUILD_TYPE=RelWithDebInfo ..`
|
||||
5. `ninja`
|
||||
|
||||
#### Mac:
|
||||
1. `git clone https://github.com/uowuo/abaddon && cd abaddon`
|
||||
2. `brew install gtkmm3 nlohmann-json`
|
||||
|
||||
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`
|
||||
|
||||
1. Install dependencies
|
||||
* On Ubuntu 20.04 (Focal) and newer:
|
||||
```Shell
|
||||
$ sudo apt install g++ cmake libgtkmm-3.0-dev libcurl4-gnutls-dev libsqlite3-dev libssl-dev nlohmann-json3-dev
|
||||
```
|
||||
* On Arch Linux
|
||||
```Shell
|
||||
$ sudo pacman -S gcc cmake gtkmm3 libcurl-gnutls lib32-sqlite lib32-openssl nlohmann-json libhandy
|
||||
```
|
||||
* On Fedora Linux:
|
||||
```Shell
|
||||
$ sudo dnf install g++ cmake gtkmm3.0-devel libcurl-devel sqlite-devel openssl-devel json-devel libsecret-devel libhandy-devel
|
||||
```
|
||||
2. `git clone https://github.com/uowuo/abaddon --recurse-submodules="subprojects" && cd abaddon`
|
||||
3. `mkdir build && cd build`
|
||||
4. `cmake ..`
|
||||
5. `make`
|
||||
@@ -59,158 +94,207 @@ 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)
|
||||
- MacOS: [here](https://nightly.link/uowuo/abaddon/workflows/ci/master/build-macos-RelWithDebInfo.zip) unsigned, unpackaged, requires gtkmm3 (e.g. from homebrew)
|
||||
- Linux: [here](https://nightly.link/uowuo/abaddon/workflows/ci/master/build-linux-MinSizeRel.zip) unpackaged (for now), requires gtkmm3. built on Ubuntu 18.04 + gcc9
|
||||
- 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),
|
||||
requires gtkmm3. built on Ubuntu 18.04 + gcc9
|
||||
|
||||
⚠️ If you use Windows, make sure to start from the directory containing `css` and `res`
|
||||
> **Warning**: If you use Windows, make sure to start from the `bin` directory
|
||||
|
||||
On Linux, `css` and `res` can also be loaded from `~/.local/share/abaddon` or `/usr/share/abaddon`
|
||||
|
||||
`abaddon.ini` will also be automatically used if located at `~/.config/abaddon/abaddon.ini` and there is no `abaddon.ini` in the working directory
|
||||
`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:
|
||||
|
||||
#### Dependencies:
|
||||
* [gtkmm](https://www.gtkmm.org/en/)
|
||||
* [JSON for Modern C++](https://github.com/nlohmann/json)
|
||||
* [IXWebSocket](https://github.com/machinezone/IXWebSocket)
|
||||
* [libcurl](https://curl.se/)
|
||||
* [zlib](https://zlib.net/)
|
||||
* [SQLite3](https://www.sqlite.org/index.html)
|
||||
* [libhandy](https://gnome.pages.gitlab.gnome.org/libhandy/) (optional)
|
||||
|
||||
### TODO:
|
||||
|
||||
* Voice support
|
||||
* Unread indicators
|
||||
* User activities
|
||||
* Nicknames
|
||||
* More server management stuff
|
||||
* Manage friends
|
||||
* A bunch of other stuff
|
||||
|
||||
### Styling
|
||||
#### CSS selectors
|
||||
.app-window - Applied to all windows. This means the main window and all popups
|
||||
.app-popup - Additional class for `.app-window`s when the window is not the main window
|
||||
|
||||
.channel-list - Container of the channel list
|
||||
|
||||
.messages - Container of user messages
|
||||
.message-container - The container which holds a user's messages
|
||||
.message-container-author - The author label for a message container
|
||||
.message-container-timestamp - The timestamp label for a message container
|
||||
.message-container-avatar - Avatar for a user in a message
|
||||
.message-container-extra - Label containing BOT/Webhook
|
||||
.message-text - The text of a user message
|
||||
.pending - Extra class of .message-text for messages pending to be sent
|
||||
.failed - Extra class of .message-text for messages that failed to be sent
|
||||
.message-attachment-box - Contains attachment info
|
||||
.message-reply - Container for the replied-to message in a reply (these elements will also have .message-text set)
|
||||
.message-input - Applied to the chat input container
|
||||
.replying - Extra class for chat input container when a reply is currently being created
|
||||
.reaction-box - Contains a reaction image and the count
|
||||
.reacted - Additional class for reaction-box when the user has reacted with a particular reaction
|
||||
.reaction-count - Contains the count for reaction
|
||||
|
||||
.completer - Container for the message completer
|
||||
.completer-entry - Container for a single entry in the completer
|
||||
.completer-entry-label - Contains the label for an entry in the completer
|
||||
.completer-entry-image - Contains the image for an entry in the completer
|
||||
|
||||
.embed - Container for a message embed
|
||||
.embed-author - The author of an embed
|
||||
.embed-title - The title of an embed
|
||||
.embed-description - The description of an embed
|
||||
.embed-field-title - The title of an embed field
|
||||
.embed-field-value - The value of an embed field
|
||||
.embed-footer - The footer of an embed
|
||||
|
||||
.members - Container of the member list
|
||||
.members-row - All rows within the members container
|
||||
.members-row-label - All labels in the members container
|
||||
.members-row-member - Rows containing a member
|
||||
.members-row-role - Rows containing a role
|
||||
.members-row-avatar - Contains the avatar for a row in the member list
|
||||
|
||||
.status-indicator - The status indicator
|
||||
.online - Applied to status indicators when the associated user is online
|
||||
.idle - Applied to status indicators when the associated user is away
|
||||
.dnd - Applied to status indicators when the associated user is on do not disturb
|
||||
.offline - Applied to status indicators when the associated user is offline
|
||||
|
||||
.typing-indicator - The typing indicator (also used for replies)
|
||||
|
||||
Used in reorderable list implementation:
|
||||
.drag-icon
|
||||
.drag-hover-top
|
||||
.drag-hover-bottom
|
||||
|
||||
Used in guild settings popup:
|
||||
.guild-settings-window
|
||||
.guild-members-pane-list - Container for list of members in the members pane
|
||||
.guild-members-pane-info - Container for member info
|
||||
.guild-roles-pane-list - Container for list of roles in the roles pane
|
||||
|
||||
Used in profile popup:
|
||||
.mutual-friend-item - Applied to every item in the mutual friends list
|
||||
.mutual-friend-item-name - Name in mutual friend item
|
||||
.mutual-friend-item-avatar - Avatar in mutual friend item
|
||||
.mutual-guild-item - Applied to every item in the mutual guilds list
|
||||
.mutual-guild-item-name - Name in mutual guild item
|
||||
.mutual-guild-item-icon - Icon in mutual guild item
|
||||
.mutual-guild-item-nick - User nickname in mutual guild item
|
||||
.profile-connection - Applied to every item in the user connections list
|
||||
.profile-connection-label - Label in profile connection item
|
||||
.profile-connection-check - Checkmark in verified profile connection items
|
||||
.profile-connections - Container for profile connections
|
||||
.profile-notes - Container for notes in profile window
|
||||
.profile-notes-label - Label that says "NOTE"
|
||||
.profile-notes-text - Actual note text
|
||||
.profile-info-pane - Applied to container for info section of profile popup
|
||||
.profile-info-created - Label for creation date of profile
|
||||
.user-profile-window
|
||||
.profile-main-container - Inner container for profile
|
||||
.profile-avatar
|
||||
.profile-username
|
||||
.profile-switcher - Buttons used to switch viewed section of profile
|
||||
.profile-stack - Container for profile info that can be switched between
|
||||
.profile-badges - Container for badges
|
||||
.profile-badge
|
||||
|
||||
#### CSS selectors
|
||||
|
||||
| Selector | Description |
|
||||
|--------------------------------|---------------------------------------------------------------------------------------------------|
|
||||
| `.app-window` | Applied to all windows. This means the main window and all popups |
|
||||
| `.app-popup` | Additional class for `.app-window`s when the window is not the main window |
|
||||
| `.channel-list` | Container of the channel list |
|
||||
| `.messages` | Container of user messages |
|
||||
| `.message-container` | The container which holds a user's messages |
|
||||
| `.message-container-author` | The author label for a message container |
|
||||
| `.message-container-timestamp` | The timestamp label for a message container |
|
||||
| `.message-container-avatar` | Avatar for a user in a message |
|
||||
| `.message-container-extra` | Label containing BOT/Webhook |
|
||||
| `.message-text` | The text of a user message |
|
||||
| `.pending` | Extra class of .message-text for messages pending to be sent |
|
||||
| `.failed` | Extra class of .message-text for messages that failed to be sent |
|
||||
| `.message-attachment-box` | Contains attachment info |
|
||||
| `.message-reply` | Container for the replied-to message in a reply (these elements will also have .message-text set) |
|
||||
| `.message-input` | Applied to the chat input container |
|
||||
| `.replying` | Extra class for chat input container when a reply is currently being created |
|
||||
| `.reaction-box` | Contains a reaction image and the count |
|
||||
| `.reacted` | Additional class for reaction-box when the user has reacted with a particular reaction |
|
||||
| `.reaction-count` | Contains the count for reaction |
|
||||
| `.completer` | Container for the message completer |
|
||||
| `.completer-entry` | Container for a single entry in the completer |
|
||||
| `.completer-entry-label` | Contains the label for an entry in the completer |
|
||||
| `.completer-entry-image` | Contains the image for an entry in the completer |
|
||||
| `.embed` | Container for a message embed |
|
||||
| `.embed-author` | The author of an embed |
|
||||
| `.embed-title` | The title of an embed |
|
||||
| `.embed-description` | The description of an embed |
|
||||
| `.embed-field-title` | The title of an embed field |
|
||||
| `.embed-field-value` | The value of an embed field |
|
||||
| `.embed-footer` | The footer of an embed |
|
||||
| `.members` | Container of the member list |
|
||||
| `.members-row` | All rows within the members container |
|
||||
| `.members-row-label` | All labels in the members container |
|
||||
| `.members-row-member` | Rows containing a member |
|
||||
| `.members-row-role` | Rows containing a role |
|
||||
| `.members-row-avatar` | Contains the avatar for a row in the member list |
|
||||
| `.status-indicator` | The status indicator |
|
||||
| `.online` | Applied to status indicators when the associated user is online |
|
||||
| `.idle` | Applied to status indicators when the associated user is away |
|
||||
| `.dnd` | Applied to status indicators when the associated user is on do not disturb |
|
||||
| `.offline` | Applied to status indicators when the associated user is offline |
|
||||
| `.typing-indicator` | The typing indicator (also used for replies) |
|
||||
|
||||
Used in reorderable list implementation:
|
||||
| Selector |
|
||||
|----------------------|
|
||||
| `.drag-icon` |
|
||||
| `.drag-hover-top` |
|
||||
| `.drag-hover-bottom` |
|
||||
|
||||
Used in guild settings popup:
|
||||
|
||||
| Selector | Description |
|
||||
|----------------------------|---------------------------------------------------|
|
||||
| `.guild-settings-window` | Container for list of members in the members pane |
|
||||
| `.guild-members-pane-list` | |
|
||||
| `.guild-members-pane-info` | Container for member info |
|
||||
| `.guild-roles-pane-list` | Container for list of roles in the roles pane |
|
||||
|
||||
Used in profile popup:
|
||||
|
||||
| Selector | Description |
|
||||
|------------------------------|---------------------------------------------------------|
|
||||
| `.mutual-friend-item` | Applied to every item in the mutual friends list |
|
||||
| `.mutual-friend-item-name` | Name in mutual friend item |
|
||||
| `.mutual-friend-item-avatar` | Avatar in mutual friend item |
|
||||
| `.mutual-guild-item` | Applied to every item in the mutual guilds list |
|
||||
| `.mutual-guild-item-name` | Name in mutual guild item |
|
||||
| `.mutual-guild-item-icon` | Icon in mutual guild item |
|
||||
| `.mutual-guild-item-nick` | User nickname in mutual guild item |
|
||||
| `.profile-connection` | Applied to every item in the user connections list |
|
||||
| `.profile-connection-label` | Label in profile connection item |
|
||||
| `.profile-connection-check` | Checkmark in verified profile connection items |
|
||||
| `.profile-connections` | Container for profile connections |
|
||||
| `.profile-notes` | Container for notes in profile window |
|
||||
| `.profile-notes-label` | Label that says "NOTE" |
|
||||
| `.profile-notes-text` | Actual note text |
|
||||
| `.profile-info-pane` | Applied to container for info section of profile popup |
|
||||
| `.profile-info-created` | Label for creation date of profile |
|
||||
| `.user-profile-window` | |
|
||||
| `.profile-main-container` | Inner container for profile |
|
||||
| `.profile-avatar` | |
|
||||
| `.profile-username` | |
|
||||
| `.profile-switcher` | Buttons used to switch viewed section of profile |
|
||||
| `.profile-stack` | Container for profile info that can be switched between |
|
||||
| `.profile-badges` | Container for badges |
|
||||
| `.profile-badge` | |
|
||||
|
||||
### Settings
|
||||
Settings are configured (for now) by editing abaddon.ini
|
||||
The format is similar to the standard Windows ini format **except**:
|
||||
|
||||
Settings are configured (for now) by editing `abaddon.ini`.
|
||||
The format is similar to the standard Windows ini format **except**:
|
||||
|
||||
* `#` is used to begin comments as opposed to `;`
|
||||
* Section and key names are case-sensitive
|
||||
|
||||
You should edit these while the client is closed even though there's an option to reload while running
|
||||
This listing is organized by section.
|
||||
> **Warning**: You should edit these while the client is closed, even though there's an option to reload while running.
|
||||
|
||||
This listing is organized by section.
|
||||
For example, memory_db would be set by adding `memory_db = true` under the line `[discord]`
|
||||
|
||||
#### discord
|
||||
* gateway (string) - override url for Discord gateway. must be json format and use zlib stream compression
|
||||
* api_base (string) - override base url for Discord API
|
||||
* memory_db (true or false, default false) - if true, Discord data will be kept in memory as opposed to on disk
|
||||
* token (string) - Discord token used to login, this can be set from the menu
|
||||
* prefetch (true or false, default false) - if true, new messages will cause the avatar and image attachments to be automatically downloaded
|
||||
|
||||
| Setting | Type | Default | Description |
|
||||
|---------------|---------|---------|--------------------------------------------------------------------------------------------------|
|
||||
| `gateway` | string | | override url for Discord gateway. must be json format and use zlib stream compression |
|
||||
| `api_base` | string | | override base url for Discord API |
|
||||
| `memory_db` | boolean | false | if true, Discord data will be kept in memory as opposed to on disk |
|
||||
| `token` | string | | Discord token used to login, this can be set from the menu |
|
||||
| `prefetch` | boolean | false | if true, new messages will cause the avatar and image attachments to be automatically downloaded |
|
||||
| `autoconnect` | boolean | false | autoconnect to discord |
|
||||
|
||||
#### http
|
||||
* user_agent (string) - sets the user-agent to use in HTTP requests to the Discord API (not including media/images)
|
||||
* concurrent (int, default 20) - how many images can be concurrently retrieved
|
||||
|
||||
| Setting | Type | Default | Description |
|
||||
|--------------|--------|---------|---------------------------------------------------------------------------------------------|
|
||||
| `user_agent` | string | | sets the user-agent to use in HTTP requests to the Discord API (not including media/images) |
|
||||
| `concurrent` | int | 20 | how many images can be concurrently retrieved |
|
||||
|
||||
#### gui
|
||||
* member_list_discriminator (true or false, default true) - show user discriminators in the member list
|
||||
* stock_emojis (true or false, default true) - allow abaddon to substitute unicode emojis with images from emojis.bin, must be false to allow GTK to render emojis itself
|
||||
* custom_emojis (true or false, default true) - download and use custom Discord emojis
|
||||
* css (string) - path to the main CSS file
|
||||
* animations (true or false, default true) - use animated images where available (e.g. server icons, emojis, avatars). false means static images will be used
|
||||
* animated_guild_hover_only (true or false, default true) - only animate guild icons when the guild is being hovered over
|
||||
* owner_crown (true or false, default true) - show a crown next to the owner
|
||||
|
||||
| Setting | Type | Default | Description |
|
||||
|-----------------------------|---------|---------|----------------------------------------------------------------------------------------------------------------------------|
|
||||
| `member_list_discriminator` | boolean | true | show user discriminators in the member list |
|
||||
| `stock_emojis` | boolean | true | allow abaddon to substitute unicode emojis with images from emojis.bin, must be false to allow GTK to render emojis itself |
|
||||
| `custom_emojis` | boolean | true | download and use custom Discord emojis |
|
||||
| `css` | string | | path to the main CSS file |
|
||||
| `animations` | boolean | true | use animated images where available (e.g. server icons, emojis, avatars). false means static images will be used |
|
||||
| `animated_guild_hover_only` | boolean | true | only animate guild icons when the guild is being hovered over |
|
||||
| `owner_crown` | boolean | true | show a crown next to the owner |
|
||||
| `unreads` | boolean | true | show unread indicators and mention badges |
|
||||
| `save_state` | boolean | true | save the state of the gui (active channels, tabs, expanded channels) |
|
||||
| `alt_menu` | boolean | false | keep the menu hidden unless revealed with alt key |
|
||||
| `hide_to_tray` | boolean | false | hide abaddon to the system tray on window close |
|
||||
|
||||
#### style
|
||||
* linkcolor (string) - color to use for links in messages
|
||||
* expandercolor (string) - color to use for the expander in the channel list
|
||||
* nsfwchannelcolor (string) - color to use for NSFW channels in the channel list
|
||||
|
||||
| Setting | Type | Description |
|
||||
|-------------------------|--------|-----------------------------------------------------|
|
||||
| `linkcolor` | string | color to use for links in messages |
|
||||
| `expandercolor` | string | color to use for the expander in the channel list |
|
||||
| `nsfwchannelcolor` | string | color to use for NSFW channels in the channel list |
|
||||
| `channelcolor` | string | color to use for SFW channels in the channel list |
|
||||
| `mentionbadgecolor` | string | background color for mention badges |
|
||||
| `mentionbadgetextcolor` | string | color to use for number displayed on mention badges |
|
||||
| `unreadcolor` | string | color to use for the unread indicator |
|
||||
|
||||
### Environment variables
|
||||
|
||||
* ABADDON_NO_FC (Windows only) - don't use custom font config
|
||||
* ABADDON_CONFIG - change path of configuration file to use. relative to cwd or can be absolute
|
||||
| variable | Description |
|
||||
|------------------|------------------------------------------------------------------------------|
|
||||
| `ABADDON_NO_FC` | (Windows only) don't use custom font config |
|
||||
| `ABADDON_CONFIG` | change path of configuration file to use. relative to cwd or can be absolute |
|
||||
|
||||
59
ci/msys-deps.txt
Normal file
59
ci/msys-deps.txt
Normal file
@@ -0,0 +1,59 @@
|
||||
/bin/gdbus.exe
|
||||
/bin/gspawn-win64-helper-console.exe
|
||||
/bin/libatk-1.0-0.dll
|
||||
/bin/libatkmm-1.6-1.dll
|
||||
/bin/libbrotlicommon.dll
|
||||
/bin/libbrotlidec.dll
|
||||
/bin/libbz2-1.dll
|
||||
/bin/libcairo-2.dll
|
||||
/bin/libcairo-gobject-2.dll
|
||||
/bin/libcairomm-1.0-1.dll
|
||||
/bin/libcrypto-1_1-x64.dll
|
||||
/bin/libcurl-4.dll
|
||||
/bin/libdatrie-1.dll
|
||||
/bin/libdeflate.dll
|
||||
/bin/libepoxy-0.dll
|
||||
/bin/libexpat-1.dll
|
||||
/bin/libffi-8.dll
|
||||
/bin/libfontconfig-1.dll
|
||||
/bin/libfreetype-6.dll
|
||||
/bin/libfribidi-0.dll
|
||||
/bin/libgcc_s_seh-1.dll
|
||||
/bin/libgdk-3-0.dll
|
||||
/bin/libgdk_pixbuf-2.0-0.dll
|
||||
/bin/libgdkmm-3.0-1.dll
|
||||
/bin/libgio-2.0-0.dll
|
||||
/bin/libgiomm-2.4-1.dll
|
||||
/bin/libglib-2.0-0.dll
|
||||
/bin/libglibmm-2.4-1.dll
|
||||
/bin/libgmodule-2.0-0.dll
|
||||
/bin/libgobject-2.0-0.dll
|
||||
/bin/libgraphite2.dll
|
||||
/bin/libgtk-3-0.dll
|
||||
/bin/libgtkmm-3.0-1.dll
|
||||
/bin/libhandy-1-0.dll
|
||||
/bin/libharfbuzz-0.dll
|
||||
/bin/libiconv-2.dll
|
||||
/bin/libidn2-0.dll
|
||||
/bin/libintl-8.dll
|
||||
/bin/libnghttp2-14.dll
|
||||
/bin/libpango-1.0-0.dll
|
||||
/bin/libpangocairo-1.0-0.dll
|
||||
/bin/libpangoft2-1.0-0.dll
|
||||
/bin/libpangomm-1.4-1.dll
|
||||
/bin/libpangowin32-1.0-0.dll
|
||||
/bin/libpcre2-8-0.dll
|
||||
/bin/libpixman-1-0.dll
|
||||
/bin/libpng16-16.dll
|
||||
/bin/libpsl-5.dll
|
||||
/bin/libsigc-2.0-0.dll
|
||||
/bin/libsqlite3-0.dll
|
||||
/bin/libssh2-1.dll
|
||||
/bin/libssl-1_1-x64.dll
|
||||
/bin/libstdc++-6.dll
|
||||
/bin/libthai-0.dll
|
||||
/bin/libunistring-2.dll
|
||||
/bin/libwinpthread-1.dll
|
||||
/bin/libzstd.dll
|
||||
/bin/zlib1.dll
|
||||
/../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
|
||||
1
ci/vcpkg
1
ci/vcpkg
Submodule ci/vcpkg deleted from 50ea8c0ab7
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;
|
||||
}
|
||||
|
||||
389
src/abaddon.cpp
389
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,27 +68,147 @@ Abaddon &Abaddon::Get() {
|
||||
return instance;
|
||||
}
|
||||
|
||||
#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???
|
||||
|
||||
#ifdef WITH_LIBHANDY
|
||||
switch (event->button.button) {
|
||||
case BUTTON_BACK:
|
||||
main_window->GoBack();
|
||||
break;
|
||||
case BUTTON_FORWARD:
|
||||
main_window->GoForward();
|
||||
break;
|
||||
}
|
||||
#endif
|
||||
|
||||
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;
|
||||
|
||||
constexpr static guint EXCLUDE_STATES = GDK_CONTROL_MASK | GDK_SHIFT_MASK;
|
||||
|
||||
if (!(event->key.state & EXCLUDE_STATES) && event->key.keyval == GDK_KEY_Alt_L) {
|
||||
if (Abaddon::Get().GetSettings().AltMenu) {
|
||||
main_window->ToggleMenuVisibility();
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef WITH_LIBHANDY
|
||||
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;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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([this](const Glib::RefPtr<const Gtk::CssSection> §ion, const Glib::Error &error) {
|
||||
Gtk::MessageDialog dlg(*m_main_window, "css failed parsing (" + error.what() + ")", false, Gtk::MESSAGE_ERROR, Gtk::BUTTONS_OK, true);
|
||||
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);
|
||||
dlg.set_position(Gtk::WIN_POS_CENTER);
|
||||
dlg.run();
|
||||
});
|
||||
|
||||
m_css_low_provider = Gtk::CssProvider::create();
|
||||
m_css_low_provider->signal_parsing_error().connect([this](const Glib::RefPtr<const Gtk::CssSection> §ion, const Glib::Error &error) {
|
||||
Gtk::MessageDialog dlg(*m_main_window, "low-priority css failed parsing (" + error.what() + ")", false, Gtk::MESSAGE_ERROR, Gtk::BUTTONS_OK, true);
|
||||
m_css_low_provider->signal_parsing_error().connect([](const Glib::RefPtr<const Gtk::CssSection> §ion, const Glib::Error &error) {
|
||||
Gtk::MessageDialog dlg("low-priority css failed parsing (" + error.what() + ")", false, Gtk::MESSAGE_ERROR, Gtk::BUTTONS_OK, true);
|
||||
dlg.set_position(Gtk::WIN_POS_CENTER);
|
||||
dlg.run();
|
||||
});
|
||||
|
||||
#ifdef _WIN32
|
||||
bool png_found = false;
|
||||
bool gif_found = false;
|
||||
for (const auto &fmt : Gdk::Pixbuf::get_formats()) {
|
||||
if (fmt.get_name() == "png")
|
||||
png_found = true;
|
||||
else if (fmt.get_name() == "gif")
|
||||
gif_found = true;
|
||||
}
|
||||
|
||||
if (!png_found) {
|
||||
Gtk::MessageDialog dlg("The PNG pixbufloader wasn't detected. Abaddon may not work as a result.", false, Gtk::MESSAGE_ERROR, Gtk::BUTTONS_OK, true);
|
||||
dlg.set_position(Gtk::WIN_POS_CENTER);
|
||||
dlg.run();
|
||||
}
|
||||
|
||||
if (!gif_found) {
|
||||
Gtk::MessageDialog dlg("The GIF pixbufloader wasn't detected. Animations may not display as a result.", false, Gtk::MESSAGE_ERROR, Gtk::BUTTONS_OK, true);
|
||||
dlg.set_position(Gtk::WIN_POS_CENTER);
|
||||
dlg.run();
|
||||
}
|
||||
#endif
|
||||
|
||||
m_main_window = std::make_unique<MainWindow>();
|
||||
m_main_window->set_title(APP_TITLE);
|
||||
m_main_window->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);
|
||||
@@ -109,13 +238,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));
|
||||
|
||||
@@ -128,10 +256,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);
|
||||
}
|
||||
|
||||
@@ -142,7 +289,7 @@ void Abaddon::OnShutdown() {
|
||||
|
||||
void Abaddon::LoadFromSettings() {
|
||||
std::string token = GetSettings().DiscordToken;
|
||||
if (token.size()) {
|
||||
if (!token.empty()) {
|
||||
m_discord_token = token;
|
||||
m_discord.UpdateToken(m_discord_token);
|
||||
}
|
||||
@@ -150,11 +297,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 {
|
||||
@@ -267,9 +416,10 @@ 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.size() > 0);
|
||||
m_user_menu_roles->set_visible(!roles.empty());
|
||||
for (const auto &role : roles) {
|
||||
auto *item = Gtk::manage(new Gtk::MenuItem(role.Name));
|
||||
if (role.Color != 0) {
|
||||
@@ -289,7 +439,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);
|
||||
@@ -297,7 +447,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();
|
||||
@@ -311,6 +461,48 @@ 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();
|
||||
}
|
||||
|
||||
// autoconnect
|
||||
if (cookie.has_value() && build_number.has_value() && GetSettings().Autoconnect && !GetDiscordToken().empty()) {
|
||||
ActionConnect();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void Abaddon::ShowGuildVerificationGateDialog(Snowflake guild_id) {
|
||||
VerificationGateDialog dlg(*m_main_window, guild_id);
|
||||
if (dlg.run() == Gtk::RESPONSE_OK) {
|
||||
@@ -325,13 +517,27 @@ void Abaddon::ShowGuildVerificationGateDialog(Snowflake guild_id) {
|
||||
}
|
||||
}
|
||||
|
||||
void Abaddon::CheckMessagesForMembers(const ChannelData &chan, const std::vector<Message> &msgs) {
|
||||
if (!chan.GuildID.has_value()) return;
|
||||
|
||||
std::vector<Snowflake> unknown;
|
||||
std::transform(msgs.begin(), msgs.end(),
|
||||
std::back_inserter(unknown),
|
||||
[](const Message &msg) -> Snowflake {
|
||||
return msg.Author.ID;
|
||||
});
|
||||
|
||||
const auto fetch = m_discord.FilterUnknownMembersFrom(*chan.GuildID, unknown.begin(), unknown.end());
|
||||
m_discord.RequestMembers(*chan.GuildID, fetch.begin(), fetch.end());
|
||||
}
|
||||
|
||||
void Abaddon::SetupUserMenu() {
|
||||
m_user_menu = Gtk::manage(new Gtk::Menu);
|
||||
m_user_menu_insert_mention = Gtk::manage(new Gtk::MenuItem("Insert Mention"));
|
||||
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"));
|
||||
@@ -373,6 +579,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)) {
|
||||
@@ -380,7 +589,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);
|
||||
@@ -388,14 +598,22 @@ void Abaddon::SaveState() {
|
||||
}
|
||||
|
||||
void Abaddon::LoadState() {
|
||||
if (!GetSettings().SaveState) return;
|
||||
if (!GetSettings().SaveState) {
|
||||
// call with empty data to purge the temporary table
|
||||
m_main_window->GetChannelList()->UseExpansionState({});
|
||||
return;
|
||||
}
|
||||
|
||||
const auto data = ReadWholeFile(GetStateCachePath("/state.json"));
|
||||
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());
|
||||
}
|
||||
@@ -430,18 +648,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() {
|
||||
@@ -494,24 +703,28 @@ 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);
|
||||
|
||||
if (channel->Type == ChannelType::GUILD_TEXT || channel->Type == ChannelType::GUILD_NEWS)
|
||||
m_main_window->set_title(std::string(APP_TITLE) + " - #" + *channel->Name);
|
||||
else {
|
||||
@@ -525,26 +738,35 @@ 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()) {
|
||||
m_discord.FetchMessagesInChannel(id, [this, id](const std::vector<Message> &msgs) {
|
||||
m_main_window->UpdateChatWindowContents();
|
||||
m_channels_requested.insert(id);
|
||||
});
|
||||
// dont fire requests we know will fail
|
||||
if (can_access) {
|
||||
m_discord.FetchMessagesInChannel(id, [channel, this, id](const std::vector<Message> &msgs) {
|
||||
CheckMessagesForMembers(*channel, msgs);
|
||||
m_main_window->UpdateChatWindowContents();
|
||||
m_channels_requested.insert(id);
|
||||
});
|
||||
}
|
||||
} else {
|
||||
m_main_window->UpdateChatWindowContents();
|
||||
}
|
||||
|
||||
if (channel->IsThread()) {
|
||||
m_discord.SendThreadLazyLoad(id);
|
||||
if (channel->ThreadMetadata->IsArchived)
|
||||
m_main_window->GetChatWindow()->SetTopic("This thread is archived. Sending a message will unarchive it");
|
||||
} else if (channel->Type != ChannelType::DM && channel->Type != ChannelType::GROUP_DM && channel->GuildID.has_value()) {
|
||||
m_discord.SendLazyLoad(id);
|
||||
if (can_access) {
|
||||
if (channel->IsThread()) {
|
||||
m_discord.SendThreadLazyLoad(id);
|
||||
if (channel->ThreadMetadata->IsArchived)
|
||||
m_main_window->GetChatWindow()->SetTopic("This thread is archived. Sending a message will unarchive it");
|
||||
} else if (channel->Type != ChannelType::DM && channel->Type != ChannelType::GROUP_DM && channel->GuildID.has_value()) {
|
||||
m_discord.SendLazyLoad(id);
|
||||
|
||||
if (m_discord.IsVerificationRequired(*channel->GuildID))
|
||||
ShowGuildVerificationGateDialog(*channel->GuildID);
|
||||
if (m_discord.IsVerificationRequired(*channel->GuildID))
|
||||
ShowGuildVerificationGateDialog(*channel->GuildID);
|
||||
}
|
||||
}
|
||||
|
||||
m_main_window->UpdateMenus();
|
||||
m_discord.SetReferringChannel(id);
|
||||
}
|
||||
|
||||
void Abaddon::ActionChatLoadHistory(Snowflake id) {
|
||||
@@ -567,7 +789,11 @@ void Abaddon::ActionChatLoadHistory(Snowflake id) {
|
||||
m_discord.FetchMessagesInChannelBefore(id, before_id, [this, id](const std::vector<Message> &msgs) {
|
||||
m_channels_history_loading.erase(id);
|
||||
|
||||
if (msgs.size() == 0) {
|
||||
const auto channel = m_discord.GetChannel(id);
|
||||
if (channel.has_value())
|
||||
CheckMessagesForMembers(*channel, msgs);
|
||||
|
||||
if (msgs.empty()) {
|
||||
m_channels_history_loaded.insert(id);
|
||||
} else {
|
||||
m_main_window->UpdateChatPrependHistory(msgs);
|
||||
@@ -575,13 +801,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) {
|
||||
@@ -637,7 +863,7 @@ void Abaddon::ActionSetStatus() {
|
||||
const auto status = dlg.GetStatusType();
|
||||
const auto activity_type = dlg.GetActivityType();
|
||||
const auto activity_name = dlg.GetActivityName();
|
||||
if (activity_name == "") {
|
||||
if (activity_name.empty()) {
|
||||
m_discord.UpdateStatus(status, false);
|
||||
} else {
|
||||
ActivityData activity;
|
||||
@@ -690,6 +916,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);
|
||||
@@ -720,9 +955,39 @@ 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 {
|
||||
if (systemLocale != nullptr) {
|
||||
std::locale::global(std::locale(systemLocale));
|
||||
}
|
||||
} catch (...) {
|
||||
try {
|
||||
std::locale::global(std::locale::classic());
|
||||
if (systemLocale != nullptr) {
|
||||
std::setlocale(LC_ALL, systemLocale);
|
||||
}
|
||||
} catch (...) {}
|
||||
}
|
||||
|
||||
#if defined(_WIN32) && defined(_MSC_VER)
|
||||
TCHAR buf[2] { 0 };
|
||||
GetEnvironmentVariableA("GTK_CSD", buf, sizeof(buf));
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
#pragma once
|
||||
#include <gtkmm.h>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
@@ -14,14 +15,15 @@
|
||||
class Abaddon {
|
||||
private:
|
||||
Abaddon();
|
||||
|
||||
public:
|
||||
static Abaddon &Get();
|
||||
|
||||
Abaddon(const Abaddon &) = delete;
|
||||
Abaddon &operator=(const Abaddon &) = delete;
|
||||
Abaddon(Abaddon &&) = delete;
|
||||
Abaddon &operator=(Abaddon &&) = delete;
|
||||
|
||||
public:
|
||||
static Abaddon &Get();
|
||||
|
||||
int StartGTK();
|
||||
void OnShutdown();
|
||||
|
||||
@@ -34,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);
|
||||
@@ -50,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();
|
||||
@@ -91,8 +94,12 @@ 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);
|
||||
|
||||
void SetupUserMenu();
|
||||
void SaveState();
|
||||
void LoadState();
|
||||
@@ -110,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();
|
||||
@@ -117,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;
|
||||
@@ -135,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
|
||||
};
|
||||
|
||||
@@ -10,8 +10,6 @@ CellRendererPixbufAnimation::CellRendererPixbufAnimation()
|
||||
property_ypad() = 2;
|
||||
}
|
||||
|
||||
CellRendererPixbufAnimation::~CellRendererPixbufAnimation() {}
|
||||
|
||||
Glib::PropertyProxy<Glib::RefPtr<Gdk::Pixbuf>> CellRendererPixbufAnimation::property_pixbuf() {
|
||||
return m_property_pixbuf.get_proxy();
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
class CellRendererPixbufAnimation : public Gtk::CellRenderer {
|
||||
public:
|
||||
CellRendererPixbufAnimation();
|
||||
virtual ~CellRendererPixbufAnimation();
|
||||
~CellRendererPixbufAnimation() override = default;
|
||||
|
||||
Glib::PropertyProxy<Glib::RefPtr<Gdk::Pixbuf>> property_pixbuf();
|
||||
Glib::PropertyProxy<Glib::RefPtr<Gdk::PixbufAnimation>> property_pixbuf_animation();
|
||||
|
||||
@@ -1,29 +1,36 @@
|
||||
#include "abaddon.hpp"
|
||||
#include "channels.hpp"
|
||||
#include "imgmanager.hpp"
|
||||
#include "statusindicator.hpp"
|
||||
#include "util.hpp"
|
||||
#include <algorithm>
|
||||
#include <map>
|
||||
#include <unordered_map>
|
||||
#include "abaddon.hpp"
|
||||
#include "imgmanager.hpp"
|
||||
#include "util.hpp"
|
||||
#include "statusindicator.hpp"
|
||||
|
||||
ChannelList::ChannelList()
|
||||
: Glib::ObjectBase(typeid(ChannelList))
|
||||
, Gtk::ScrolledWindow()
|
||||
, m_model(Gtk::TreeStore::create(m_columns))
|
||||
, m_menu_guild_copy_id("_Copy ID", true)
|
||||
, m_menu_guild_settings("View _Settings", true)
|
||||
, m_menu_guild_leave("_Leave", true)
|
||||
, m_menu_guild_mark_as_read("Mark as _Read", true)
|
||||
, m_menu_category_copy_id("_Copy ID", true)
|
||||
, m_menu_channel_copy_id("_Copy ID", true)
|
||||
, m_menu_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)
|
||||
, m_menu_thread_leave("_Leave", true)
|
||||
, m_menu_thread_archive("_Archive", true)
|
||||
, m_menu_thread_unarchive("_Unarchive", true) {
|
||||
, m_menu_thread_unarchive("_Unarchive", true)
|
||||
, m_menu_thread_mark_as_read("Mark as _Read", true) {
|
||||
get_style_context()->add_class("channel-list");
|
||||
|
||||
// todo: move to method
|
||||
const auto cb = [this](const Gtk::TreeModel::Path &path, Gtk::TreeViewColumn *column) {
|
||||
auto row = *m_model->get_iter(path);
|
||||
const auto type = row[m_columns.m_type];
|
||||
@@ -40,7 +47,9 @@ ChannelList::ChannelList()
|
||||
}
|
||||
|
||||
if (type == RenderType::TextChannel || type == RenderType::DM || type == RenderType::Thread) {
|
||||
m_signal_action_channel_item_select.emit(static_cast<Snowflake>(row[m_columns.m_id]));
|
||||
const auto id = static_cast<Snowflake>(row[m_columns.m_id]);
|
||||
m_signal_action_channel_item_select.emit(id);
|
||||
Abaddon::Get().GetDiscordClient().MarkChannelAsRead(id, [](...) {});
|
||||
}
|
||||
};
|
||||
m_view.signal_row_activated().connect(cb, false);
|
||||
@@ -77,6 +86,7 @@ ChannelList::ChannelList()
|
||||
column->add_attribute(renderer->property_icon(), m_columns.m_icon);
|
||||
column->add_attribute(renderer->property_icon_animation(), m_columns.m_icon_anim);
|
||||
column->add_attribute(renderer->property_name(), m_columns.m_name);
|
||||
column->add_attribute(renderer->property_id(), m_columns.m_id);
|
||||
column->add_attribute(renderer->property_expanded(), m_columns.m_expanded);
|
||||
column->add_attribute(renderer->property_nsfw(), m_columns.m_nsfw);
|
||||
m_view.append_column(*column);
|
||||
@@ -90,20 +100,64 @@ ChannelList::ChannelList()
|
||||
m_menu_guild_leave.signal_activate().connect([this] {
|
||||
m_signal_action_guild_leave.emit(static_cast<Snowflake>((*m_model->get_iter(m_path_for_menu))[m_columns.m_id]));
|
||||
});
|
||||
m_menu_guild.append(m_menu_guild_copy_id);
|
||||
m_menu_guild_mark_as_read.signal_activate().connect([this] {
|
||||
Abaddon::Get().GetDiscordClient().MarkGuildAsRead(static_cast<Snowflake>((*m_model->get_iter(m_path_for_menu))[m_columns.m_id]), [](...) {});
|
||||
});
|
||||
m_menu_guild_toggle_mute.signal_activate().connect([this] {
|
||||
const auto id = static_cast<Snowflake>((*m_model->get_iter(m_path_for_menu))[m_columns.m_id]);
|
||||
auto &discord = Abaddon::Get().GetDiscordClient();
|
||||
if (discord.IsGuildMuted(id))
|
||||
discord.UnmuteGuild(id, NOOP_CALLBACK);
|
||||
else
|
||||
discord.MuteGuild(id, NOOP_CALLBACK);
|
||||
});
|
||||
m_menu_guild.append(m_menu_guild_mark_as_read);
|
||||
m_menu_guild.append(m_menu_guild_settings);
|
||||
m_menu_guild.append(m_menu_guild_leave);
|
||||
m_menu_guild.append(m_menu_guild_toggle_mute);
|
||||
m_menu_guild.append(m_menu_guild_copy_id);
|
||||
m_menu_guild.show_all();
|
||||
|
||||
m_menu_category_copy_id.signal_activate().connect([this] {
|
||||
Gtk::Clipboard::get()->set_text(std::to_string((*m_model->get_iter(m_path_for_menu))[m_columns.m_id]));
|
||||
});
|
||||
m_menu_category_toggle_mute.signal_activate().connect([this] {
|
||||
const auto id = static_cast<Snowflake>((*m_model->get_iter(m_path_for_menu))[m_columns.m_id]);
|
||||
auto &discord = Abaddon::Get().GetDiscordClient();
|
||||
if (discord.IsChannelMuted(id))
|
||||
discord.UnmuteChannel(id, NOOP_CALLBACK);
|
||||
else
|
||||
discord.MuteChannel(id, NOOP_CALLBACK);
|
||||
});
|
||||
m_menu_category.append(m_menu_category_toggle_mute);
|
||||
m_menu_category.append(m_menu_category_copy_id);
|
||||
m_menu_category.show_all();
|
||||
|
||||
m_menu_channel_copy_id.signal_activate().connect([this] {
|
||||
Gtk::Clipboard::get()->set_text(std::to_string((*m_model->get_iter(m_path_for_menu))[m_columns.m_id]));
|
||||
});
|
||||
m_menu_channel_mark_as_read.signal_activate().connect([this] {
|
||||
Abaddon::Get().GetDiscordClient().MarkChannelAsRead(static_cast<Snowflake>((*m_model->get_iter(m_path_for_menu))[m_columns.m_id]), [](...) {});
|
||||
});
|
||||
m_menu_channel_toggle_mute.signal_activate().connect([this] {
|
||||
const auto id = static_cast<Snowflake>((*m_model->get_iter(m_path_for_menu))[m_columns.m_id]);
|
||||
auto &discord = Abaddon::Get().GetDiscordClient();
|
||||
if (discord.IsChannelMuted(id))
|
||||
discord.UnmuteChannel(id, NOOP_CALLBACK);
|
||||
else
|
||||
discord.MuteChannel(id, NOOP_CALLBACK);
|
||||
});
|
||||
|
||||
#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);
|
||||
m_menu_channel.show_all();
|
||||
|
||||
@@ -121,8 +175,24 @@ ChannelList::ChannelList()
|
||||
else if (Abaddon::Get().ShowConfirm("Are you sure you want to leave this group DM?"))
|
||||
Abaddon::Get().GetDiscordClient().CloseDM(id);
|
||||
});
|
||||
m_menu_dm.append(m_menu_dm_copy_id);
|
||||
m_menu_dm_toggle_mute.signal_activate().connect([this] {
|
||||
const auto id = static_cast<Snowflake>((*m_model->get_iter(m_path_for_menu))[m_columns.m_id]);
|
||||
auto &discord = Abaddon::Get().GetDiscordClient();
|
||||
if (discord.IsChannelMuted(id))
|
||||
discord.UnmuteChannel(id, NOOP_CALLBACK);
|
||||
else
|
||||
discord.MuteChannel(id, NOOP_CALLBACK);
|
||||
});
|
||||
#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);
|
||||
m_menu_dm.show_all();
|
||||
|
||||
m_menu_thread_copy_id.signal_activate().connect([this] {
|
||||
@@ -138,14 +208,25 @@ ChannelList::ChannelList()
|
||||
m_menu_thread_unarchive.signal_activate().connect([this] {
|
||||
Abaddon::Get().GetDiscordClient().UnArchiveThread(static_cast<Snowflake>((*m_model->get_iter(m_path_for_menu))[m_columns.m_id]), [](...) {});
|
||||
});
|
||||
m_menu_thread.append(m_menu_thread_copy_id);
|
||||
m_menu_thread_mark_as_read.signal_activate().connect([this] {
|
||||
Abaddon::Get().GetDiscordClient().MarkChannelAsRead(static_cast<Snowflake>((*m_model->get_iter(m_path_for_menu))[m_columns.m_id]), NOOP_CALLBACK);
|
||||
});
|
||||
m_menu_thread_toggle_mute.signal_activate().connect([this] {
|
||||
const auto id = static_cast<Snowflake>((*m_model->get_iter(m_path_for_menu))[m_columns.m_id]);
|
||||
auto &discord = Abaddon::Get().GetDiscordClient();
|
||||
if (discord.IsChannelMuted(id))
|
||||
discord.UnmuteThread(id, NOOP_CALLBACK);
|
||||
else
|
||||
discord.MuteThread(id, NOOP_CALLBACK);
|
||||
});
|
||||
m_menu_thread.append(m_menu_thread_mark_as_read);
|
||||
m_menu_thread.append(m_menu_thread_toggle_mute);
|
||||
m_menu_thread.append(m_menu_thread_leave);
|
||||
m_menu_thread.append(m_menu_thread_archive);
|
||||
m_menu_thread.append(m_menu_thread_unarchive);
|
||||
m_menu_thread.append(m_menu_thread_copy_id);
|
||||
m_menu_thread.show_all();
|
||||
|
||||
m_menu_thread.signal_popped_up().connect(sigc::mem_fun(*this, &ChannelList::OnThreadSubmenuPopup));
|
||||
|
||||
auto &discord = Abaddon::Get().GetDiscordClient();
|
||||
discord.signal_message_create().connect(sigc::mem_fun(*this, &ChannelList::OnMessageCreate));
|
||||
discord.signal_guild_create().connect(sigc::mem_fun(*this, &ChannelList::UpdateNewGuild));
|
||||
@@ -159,6 +240,19 @@ ChannelList::ChannelList()
|
||||
discord.signal_added_to_thread().connect(sigc::mem_fun(*this, &ChannelList::OnThreadJoined));
|
||||
discord.signal_removed_from_thread().connect(sigc::mem_fun(*this, &ChannelList::OnThreadRemoved));
|
||||
discord.signal_guild_update().connect(sigc::mem_fun(*this, &ChannelList::UpdateGuild));
|
||||
discord.signal_message_ack().connect(sigc::mem_fun(*this, &ChannelList::OnMessageAck));
|
||||
discord.signal_channel_muted().connect(sigc::mem_fun(*this, &ChannelList::OnChannelMute));
|
||||
discord.signal_channel_unmuted().connect(sigc::mem_fun(*this, &ChannelList::OnChannelUnmute));
|
||||
discord.signal_guild_muted().connect(sigc::mem_fun(*this, &ChannelList::OnGuildMute));
|
||||
discord.signal_guild_unmuted().connect(sigc::mem_fun(*this, &ChannelList::OnGuildUnmute));
|
||||
}
|
||||
|
||||
void ChannelList::UsePanedHack(Gtk::Paned &paned) {
|
||||
paned.property_position().signal_changed().connect(sigc::mem_fun(*this, &ChannelList::OnPanedPositionChanged));
|
||||
}
|
||||
|
||||
void ChannelList::OnPanedPositionChanged() {
|
||||
m_view.queue_draw();
|
||||
}
|
||||
|
||||
void ChannelList::UpdateListing() {
|
||||
@@ -211,7 +305,7 @@ void ChannelList::UpdateChannel(Snowflake id) {
|
||||
auto channel = Abaddon::Get().GetDiscordClient().GetChannel(id);
|
||||
if (!iter || !channel.has_value()) return;
|
||||
if (channel->Type == ChannelType::GUILD_CATEGORY) return UpdateChannelCategory(*channel);
|
||||
if (!IsTextChannel(channel->Type)) return;
|
||||
if (!channel->IsText()) return;
|
||||
|
||||
// refresh stuff that might have changed
|
||||
const bool is_orphan_TMP = !channel->ParentID.has_value();
|
||||
@@ -231,7 +325,6 @@ void ChannelList::UpdateChannel(Snowflake id) {
|
||||
}
|
||||
|
||||
void ChannelList::UpdateCreateChannel(const ChannelData &channel) {
|
||||
;
|
||||
if (channel.Type == ChannelType::GUILD_CATEGORY) return (void)UpdateCreateChannelCategory(channel);
|
||||
if (channel.Type == ChannelType::DM || channel.Type == ChannelType::GROUP_DM) return UpdateCreateDMChannel(channel);
|
||||
if (channel.Type != ChannelType::GUILD_TEXT && channel.Type != ChannelType::GUILD_NEWS) return;
|
||||
@@ -320,7 +413,7 @@ void ChannelList::OnThreadListSync(const ThreadListSyncData &data) {
|
||||
queue.pop();
|
||||
if ((*item)[m_columns.m_type] == RenderType::Thread)
|
||||
threads.push_back(static_cast<Snowflake>((*item)[m_columns.m_id]));
|
||||
for (auto child : item->children())
|
||||
for (const auto &child : item->children())
|
||||
queue.push(child);
|
||||
}
|
||||
|
||||
@@ -347,9 +440,35 @@ void ChannelList::DeleteThreadRow(Snowflake id) {
|
||||
m_model->erase(iter);
|
||||
}
|
||||
|
||||
void ChannelList::OnChannelMute(Snowflake id) {
|
||||
if (auto iter = GetIteratorForChannelFromID(id))
|
||||
m_model->row_changed(m_model->get_path(iter), iter);
|
||||
}
|
||||
|
||||
void ChannelList::OnChannelUnmute(Snowflake id) {
|
||||
if (auto iter = GetIteratorForChannelFromID(id))
|
||||
m_model->row_changed(m_model->get_path(iter), iter);
|
||||
}
|
||||
|
||||
void ChannelList::OnGuildMute(Snowflake id) {
|
||||
if (auto iter = GetIteratorForGuildFromID(id))
|
||||
m_model->row_changed(m_model->get_path(iter), iter);
|
||||
}
|
||||
|
||||
void ChannelList::OnGuildUnmute(Snowflake id) {
|
||||
if (auto iter = GetIteratorForGuildFromID(id))
|
||||
m_model->row_changed(m_model->get_path(iter), iter);
|
||||
}
|
||||
|
||||
// create a temporary channel row for non-joined threads
|
||||
// and delete them when the active channel switches off of them if still not joined
|
||||
void ChannelList::SetActiveChannel(Snowflake id) {
|
||||
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, [](...) {});
|
||||
|
||||
m_active_channel = id;
|
||||
|
||||
if (m_temporary_thread_row) {
|
||||
const auto thread_id = static_cast<Snowflake>((*m_temporary_thread_row)[m_columns.m_id]);
|
||||
const auto thread = Abaddon::Get().GetDiscordClient().GetChannel(thread_id);
|
||||
@@ -360,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);
|
||||
@@ -378,11 +498,11 @@ void ChannelList::UseExpansionState(const ExpansionStateRoot &root) {
|
||||
auto recurse = [this](auto &self, const ExpansionStateRoot &root) -> void {
|
||||
// and these are only channels
|
||||
for (const auto &[id, state] : root.Children) {
|
||||
if (const auto iter = GetIteratorForChannelFromID(id)) {
|
||||
if (const auto iter = m_tmp_channel_map.find(id); iter != m_tmp_channel_map.end()) {
|
||||
if (state.IsExpanded)
|
||||
m_view.expand_row(m_model->get_path(iter), false);
|
||||
m_view.expand_row(m_model->get_path(iter->second), false);
|
||||
else
|
||||
m_view.collapse_row(m_model->get_path(iter));
|
||||
m_view.collapse_row(m_model->get_path(iter->second));
|
||||
}
|
||||
|
||||
self(self, state.Children);
|
||||
@@ -400,6 +520,8 @@ void ChannelList::UseExpansionState(const ExpansionStateRoot &root) {
|
||||
|
||||
recurse(recurse, state.Children);
|
||||
}
|
||||
|
||||
m_tmp_channel_map.clear();
|
||||
}
|
||||
|
||||
ExpansionStateRoot ChannelList::GetExpansionState() const {
|
||||
@@ -473,14 +595,14 @@ Gtk::TreeModel::iterator ChannelList::AddGuild(const GuildData &guild) {
|
||||
if (thread.has_value())
|
||||
threads[*thread->ParentID].push_back(*thread);
|
||||
}
|
||||
const auto add_threads = [&](const ChannelData &channel, Gtk::TreeRow row) {
|
||||
const auto add_threads = [&](const ChannelData &channel, const Gtk::TreeRow &row) {
|
||||
row[m_columns.m_expanded] = true;
|
||||
|
||||
const auto it = threads.find(channel.ID);
|
||||
if (it == threads.end()) return;
|
||||
|
||||
for (const auto &thread : it->second)
|
||||
CreateThreadRow(row.children(), thread);
|
||||
m_tmp_channel_map[thread.ID] = CreateThreadRow(row.children(), thread);
|
||||
};
|
||||
|
||||
for (const auto &channel : orphan_channels) {
|
||||
@@ -491,6 +613,7 @@ Gtk::TreeModel::iterator ChannelList::AddGuild(const GuildData &guild) {
|
||||
channel_row[m_columns.m_sort] = *channel.Position + OrphanChannelSortOffset;
|
||||
channel_row[m_columns.m_nsfw] = channel.NSFW();
|
||||
add_threads(channel, channel_row);
|
||||
m_tmp_channel_map[channel.ID] = channel_row;
|
||||
}
|
||||
|
||||
for (const auto &[category_id, channels] : categories) {
|
||||
@@ -502,6 +625,7 @@ Gtk::TreeModel::iterator ChannelList::AddGuild(const GuildData &guild) {
|
||||
cat_row[m_columns.m_name] = Glib::Markup::escape_text(*category->Name);
|
||||
cat_row[m_columns.m_sort] = *category->Position;
|
||||
cat_row[m_columns.m_expanded] = true;
|
||||
m_tmp_channel_map[category_id] = cat_row;
|
||||
// m_view.expand_row wont work because it might not have channels
|
||||
|
||||
for (const auto &channel : channels) {
|
||||
@@ -512,6 +636,7 @@ Gtk::TreeModel::iterator ChannelList::AddGuild(const GuildData &guild) {
|
||||
channel_row[m_columns.m_sort] = *channel.Position;
|
||||
channel_row[m_columns.m_nsfw] = channel.NSFW();
|
||||
add_threads(channel, channel_row);
|
||||
m_tmp_channel_map[channel.ID] = channel_row;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -538,7 +663,7 @@ Gtk::TreeModel::iterator ChannelList::CreateThreadRow(const Gtk::TreeNodeChildre
|
||||
thread_row[m_columns.m_type] = RenderType::Thread;
|
||||
thread_row[m_columns.m_id] = channel.ID;
|
||||
thread_row[m_columns.m_name] = "- " + Glib::Markup::escape_text(*channel.Name);
|
||||
thread_row[m_columns.m_sort] = channel.ID;
|
||||
thread_row[m_columns.m_sort] = static_cast<int64_t>(channel.ID);
|
||||
thread_row[m_columns.m_nsfw] = false;
|
||||
|
||||
return thread_iter;
|
||||
@@ -582,7 +707,7 @@ bool ChannelList::IsTextChannel(ChannelType type) {
|
||||
}
|
||||
|
||||
// this should be unncessary but something is behaving strange so its just in case
|
||||
void ChannelList::OnRowCollapsed(const Gtk::TreeModel::iterator &iter, const Gtk::TreeModel::Path &path) {
|
||||
void ChannelList::OnRowCollapsed(const Gtk::TreeModel::iterator &iter, const Gtk::TreeModel::Path &path) const {
|
||||
(*iter)[m_columns.m_expanded] = false;
|
||||
}
|
||||
|
||||
@@ -627,14 +752,14 @@ void ChannelList::AddPrivateChannels() {
|
||||
|
||||
std::optional<UserData> top_recipient;
|
||||
const auto recipients = dm->GetDMRecipients();
|
||||
if (recipients.size() > 0)
|
||||
if (!recipients.empty())
|
||||
top_recipient = recipients[0];
|
||||
|
||||
auto iter = m_model->append(header_row->children());
|
||||
auto row = *iter;
|
||||
row[m_columns.m_type] = RenderType::DM;
|
||||
row[m_columns.m_id] = dm_id;
|
||||
row[m_columns.m_sort] = -(dm->LastMessageID.has_value() ? *dm->LastMessageID : dm_id);
|
||||
row[m_columns.m_sort] = static_cast<int64_t>(-(dm->LastMessageID.has_value() ? *dm->LastMessageID : dm_id));
|
||||
row[m_columns.m_icon] = img.GetPlaceholder(DMIconSize);
|
||||
|
||||
if (dm->Type == ChannelType::DM && top_recipient.has_value())
|
||||
@@ -642,7 +767,13 @@ void ChannelList::AddPrivateChannels() {
|
||||
else if (dm->Type == ChannelType::GROUP_DM)
|
||||
row[m_columns.m_name] = std::to_string(recipients.size()) + " members";
|
||||
|
||||
if (top_recipient.has_value()) {
|
||||
if (dm->HasIcon()) {
|
||||
const auto cb = [this, iter](const Glib::RefPtr<Gdk::Pixbuf> &pb) {
|
||||
if (iter)
|
||||
(*iter)[m_columns.m_icon] = pb->scale_simple(DMIconSize, DMIconSize, Gdk::INTERP_BILINEAR);
|
||||
};
|
||||
img.LoadFromURL(dm->GetIconURL(), sigc::track_obj(cb, *this));
|
||||
} else if (top_recipient.has_value()) {
|
||||
const auto cb = [this, iter](const Glib::RefPtr<Gdk::Pixbuf> &pb) {
|
||||
if (iter)
|
||||
(*iter)[m_columns.m_icon] = pb->scale_simple(DMIconSize, DMIconSize, Gdk::INTERP_BILINEAR);
|
||||
@@ -658,14 +789,14 @@ void ChannelList::UpdateCreateDMChannel(const ChannelData &dm) {
|
||||
|
||||
std::optional<UserData> top_recipient;
|
||||
const auto recipients = dm.GetDMRecipients();
|
||||
if (recipients.size() > 0)
|
||||
if (!recipients.empty())
|
||||
top_recipient = recipients[0];
|
||||
|
||||
auto iter = m_model->append(header_row->children());
|
||||
auto row = *iter;
|
||||
row[m_columns.m_type] = RenderType::DM;
|
||||
row[m_columns.m_id] = dm.ID;
|
||||
row[m_columns.m_sort] = -(dm.LastMessageID.has_value() ? *dm.LastMessageID : dm.ID);
|
||||
row[m_columns.m_sort] = static_cast<int64_t>(-(dm.LastMessageID.has_value() ? *dm.LastMessageID : dm.ID));
|
||||
row[m_columns.m_icon] = img.GetPlaceholder(DMIconSize);
|
||||
|
||||
if (dm.Type == ChannelType::DM && top_recipient.has_value())
|
||||
@@ -682,30 +813,51 @@ void ChannelList::UpdateCreateDMChannel(const ChannelData &dm) {
|
||||
}
|
||||
}
|
||||
|
||||
void ChannelList::OnMessageAck(const MessageAckData &data) {
|
||||
// trick renderer into redrawing
|
||||
m_model->row_changed(Gtk::TreeModel::Path("0"), m_model->get_iter("0")); // 0 is always path for dm header
|
||||
auto iter = GetIteratorForChannelFromID(data.ChannelID);
|
||||
if (iter) m_model->row_changed(m_model->get_path(iter), iter);
|
||||
auto channel = Abaddon::Get().GetDiscordClient().GetChannel(data.ChannelID);
|
||||
if (channel.has_value() && channel->GuildID.has_value()) {
|
||||
iter = GetIteratorForGuildFromID(*channel->GuildID);
|
||||
if (iter) m_model->row_changed(m_model->get_path(iter), iter);
|
||||
}
|
||||
}
|
||||
|
||||
void ChannelList::OnMessageCreate(const Message &msg) {
|
||||
auto iter = GetIteratorForChannelFromID(msg.ChannelID);
|
||||
if (iter) m_model->row_changed(m_model->get_path(iter), iter); // redraw
|
||||
const auto channel = Abaddon::Get().GetDiscordClient().GetChannel(msg.ChannelID);
|
||||
if (!channel.has_value()) return;
|
||||
if (channel->Type != ChannelType::DM && channel->Type != ChannelType::GROUP_DM) return;
|
||||
auto iter = GetIteratorForChannelFromID(msg.ChannelID);
|
||||
if (iter)
|
||||
(*iter)[m_columns.m_sort] = -msg.ID;
|
||||
if (channel->Type == ChannelType::DM || channel->Type == ChannelType::GROUP_DM) {
|
||||
if (iter)
|
||||
(*iter)[m_columns.m_sort] = static_cast<int64_t>(-msg.ID);
|
||||
}
|
||||
if (channel->GuildID.has_value())
|
||||
if ((iter = GetIteratorForGuildFromID(*channel->GuildID)))
|
||||
m_model->row_changed(m_model->get_path(iter), iter);
|
||||
}
|
||||
|
||||
bool ChannelList::OnButtonPressEvent(GdkEventButton *ev) {
|
||||
if (ev->button == GDK_BUTTON_SECONDARY && ev->type == GDK_BUTTON_PRESS) {
|
||||
if (m_view.get_path_at_pos(ev->x, ev->y, m_path_for_menu)) {
|
||||
if (m_view.get_path_at_pos(static_cast<int>(ev->x), static_cast<int>(ev->y), m_path_for_menu)) {
|
||||
auto row = (*m_model->get_iter(m_path_for_menu));
|
||||
switch (static_cast<RenderType>(row[m_columns.m_type])) {
|
||||
case RenderType::Guild:
|
||||
OnGuildSubmenuPopup();
|
||||
m_menu_guild.popup_at_pointer(reinterpret_cast<GdkEvent *>(ev));
|
||||
break;
|
||||
case RenderType::Category:
|
||||
OnCategorySubmenuPopup();
|
||||
m_menu_category.popup_at_pointer(reinterpret_cast<GdkEvent *>(ev));
|
||||
break;
|
||||
case RenderType::TextChannel:
|
||||
OnChannelSubmenuPopup();
|
||||
m_menu_channel.popup_at_pointer(reinterpret_cast<GdkEvent *>(ev));
|
||||
break;
|
||||
case RenderType::DM: {
|
||||
OnDMSubmenuPopup();
|
||||
const auto channel = Abaddon::Get().GetDiscordClient().GetChannel(static_cast<Snowflake>(row[m_columns.m_id]));
|
||||
if (channel.has_value()) {
|
||||
m_menu_dm_close.set_label(channel->Type == ChannelType::DM ? "Close" : "Leave");
|
||||
@@ -715,6 +867,7 @@ bool ChannelList::OnButtonPressEvent(GdkEventButton *ev) {
|
||||
m_menu_dm.popup_at_pointer(reinterpret_cast<GdkEvent *>(ev));
|
||||
} break;
|
||||
case RenderType::Thread: {
|
||||
OnThreadSubmenuPopup();
|
||||
m_menu_thread.popup_at_pointer(reinterpret_cast<GdkEvent *>(ev));
|
||||
break;
|
||||
} break;
|
||||
@@ -754,14 +907,71 @@ void ChannelList::MoveRow(const Gtk::TreeModel::iterator &iter, const Gtk::TreeM
|
||||
m_model->erase(iter);
|
||||
}
|
||||
|
||||
void ChannelList::OnThreadSubmenuPopup(const Gdk::Rectangle *flipped_rect, const Gdk::Rectangle *final_rect, bool flipped_x, bool flipped_y) {
|
||||
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]);
|
||||
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() {
|
||||
const auto iter = m_model->get_iter(m_path_for_menu);
|
||||
if (!iter) return;
|
||||
const auto id = static_cast<Snowflake>((*iter)[m_columns.m_id]);
|
||||
if (Abaddon::Get().GetDiscordClient().IsChannelMuted(id))
|
||||
m_menu_category_toggle_mute.set_label("Unmute");
|
||||
else
|
||||
m_menu_category_toggle_mute.set_label("Mute");
|
||||
}
|
||||
|
||||
void ChannelList::OnChannelSubmenuPopup() {
|
||||
const auto iter = m_model->get_iter(m_path_for_menu);
|
||||
if (!iter) return;
|
||||
const auto id = static_cast<Snowflake>((*iter)[m_columns.m_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");
|
||||
}
|
||||
|
||||
void ChannelList::OnDMSubmenuPopup() {
|
||||
auto iter = m_model->get_iter(m_path_for_menu);
|
||||
if (!iter) return;
|
||||
const auto id = static_cast<Snowflake>((*iter)[m_columns.m_id]);
|
||||
if (Abaddon::Get().GetDiscordClient().IsChannelMuted(id))
|
||||
m_menu_dm_toggle_mute.set_label("Unmute");
|
||||
else
|
||||
m_menu_dm_toggle_mute.set_label("Mute");
|
||||
}
|
||||
|
||||
void ChannelList::OnThreadSubmenuPopup() {
|
||||
m_menu_thread_archive.set_visible(false);
|
||||
m_menu_thread_unarchive.set_visible(false);
|
||||
|
||||
auto &discord = Abaddon::Get().GetDiscordClient();
|
||||
auto iter = m_model->get_iter(m_path_for_menu);
|
||||
if (!iter) return;
|
||||
auto channel = discord.GetChannel(static_cast<Snowflake>((*iter)[m_columns.m_id]));
|
||||
const auto id = static_cast<Snowflake>((*iter)[m_columns.m_id]);
|
||||
|
||||
if (discord.IsChannelMuted(id))
|
||||
m_menu_thread_toggle_mute.set_label("Unmute");
|
||||
else
|
||||
m_menu_thread_toggle_mute.set_label("Mute");
|
||||
|
||||
auto channel = discord.GetChannel(id);
|
||||
if (!channel.has_value() || !channel->ThreadMetadata.has_value()) return;
|
||||
if (!discord.HasGuildPermission(discord.GetUserData().ID, *channel->GuildID, Permission::MANAGE_THREADS)) return;
|
||||
|
||||
@@ -781,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);
|
||||
@@ -791,454 +1007,3 @@ ChannelList::ModelColumns::ModelColumns() {
|
||||
add(m_nsfw);
|
||||
add(m_expanded);
|
||||
}
|
||||
|
||||
CellRendererChannels::CellRendererChannels()
|
||||
: Glib::ObjectBase(typeid(CellRendererChannels))
|
||||
, Gtk::CellRenderer()
|
||||
, m_property_type(*this, "render-type")
|
||||
, m_property_name(*this, "name")
|
||||
, m_property_pixbuf(*this, "pixbuf")
|
||||
, m_property_pixbuf_animation(*this, "pixbuf-animation")
|
||||
, m_property_expanded(*this, "expanded")
|
||||
, m_property_nsfw(*this, "nsfw") {
|
||||
property_mode() = Gtk::CELL_RENDERER_MODE_ACTIVATABLE;
|
||||
property_xpad() = 2;
|
||||
property_ypad() = 2;
|
||||
m_property_name.get_proxy().signal_changed().connect([this] {
|
||||
m_renderer_text.property_markup() = m_property_name;
|
||||
});
|
||||
}
|
||||
|
||||
CellRendererChannels::~CellRendererChannels() {
|
||||
}
|
||||
|
||||
Glib::PropertyProxy<RenderType> CellRendererChannels::property_type() {
|
||||
return m_property_type.get_proxy();
|
||||
}
|
||||
|
||||
Glib::PropertyProxy<Glib::ustring> CellRendererChannels::property_name() {
|
||||
return m_property_name.get_proxy();
|
||||
}
|
||||
|
||||
Glib::PropertyProxy<Glib::RefPtr<Gdk::Pixbuf>> CellRendererChannels::property_icon() {
|
||||
return m_property_pixbuf.get_proxy();
|
||||
}
|
||||
|
||||
Glib::PropertyProxy<Glib::RefPtr<Gdk::PixbufAnimation>> CellRendererChannels::property_icon_animation() {
|
||||
return m_property_pixbuf_animation.get_proxy();
|
||||
}
|
||||
|
||||
Glib::PropertyProxy<bool> CellRendererChannels::property_expanded() {
|
||||
return m_property_expanded.get_proxy();
|
||||
}
|
||||
|
||||
Glib::PropertyProxy<bool> CellRendererChannels::property_nsfw() {
|
||||
return m_property_nsfw.get_proxy();
|
||||
}
|
||||
|
||||
void CellRendererChannels::get_preferred_width_vfunc(Gtk::Widget &widget, int &minimum_width, int &natural_width) const {
|
||||
switch (m_property_type.get_value()) {
|
||||
case RenderType::Guild:
|
||||
return get_preferred_width_vfunc_guild(widget, minimum_width, natural_width);
|
||||
case RenderType::Category:
|
||||
return get_preferred_width_vfunc_category(widget, minimum_width, natural_width);
|
||||
case RenderType::TextChannel:
|
||||
return get_preferred_width_vfunc_channel(widget, minimum_width, natural_width);
|
||||
case RenderType::Thread:
|
||||
return get_preferred_width_vfunc_thread(widget, minimum_width, natural_width);
|
||||
case RenderType::DMHeader:
|
||||
return get_preferred_width_vfunc_dmheader(widget, minimum_width, natural_width);
|
||||
case RenderType::DM:
|
||||
return get_preferred_width_vfunc_dm(widget, minimum_width, natural_width);
|
||||
}
|
||||
}
|
||||
|
||||
void CellRendererChannels::get_preferred_width_for_height_vfunc(Gtk::Widget &widget, int height, int &minimum_width, int &natural_width) const {
|
||||
switch (m_property_type.get_value()) {
|
||||
case RenderType::Guild:
|
||||
return get_preferred_width_for_height_vfunc_guild(widget, height, minimum_width, natural_width);
|
||||
case RenderType::Category:
|
||||
return get_preferred_width_for_height_vfunc_category(widget, height, minimum_width, natural_width);
|
||||
case RenderType::TextChannel:
|
||||
return get_preferred_width_for_height_vfunc_channel(widget, height, minimum_width, natural_width);
|
||||
case RenderType::Thread:
|
||||
return get_preferred_width_for_height_vfunc_thread(widget, height, minimum_width, natural_width);
|
||||
case RenderType::DMHeader:
|
||||
return get_preferred_width_for_height_vfunc_dmheader(widget, height, minimum_width, natural_width);
|
||||
case RenderType::DM:
|
||||
return get_preferred_width_for_height_vfunc_dm(widget, height, minimum_width, natural_width);
|
||||
}
|
||||
}
|
||||
|
||||
void CellRendererChannels::get_preferred_height_vfunc(Gtk::Widget &widget, int &minimum_height, int &natural_height) const {
|
||||
switch (m_property_type.get_value()) {
|
||||
case RenderType::Guild:
|
||||
return get_preferred_height_vfunc_guild(widget, minimum_height, natural_height);
|
||||
case RenderType::Category:
|
||||
return get_preferred_height_vfunc_category(widget, minimum_height, natural_height);
|
||||
case RenderType::TextChannel:
|
||||
return get_preferred_height_vfunc_channel(widget, minimum_height, natural_height);
|
||||
case RenderType::Thread:
|
||||
return get_preferred_height_vfunc_thread(widget, minimum_height, natural_height);
|
||||
case RenderType::DMHeader:
|
||||
return get_preferred_height_vfunc_dmheader(widget, minimum_height, natural_height);
|
||||
case RenderType::DM:
|
||||
return get_preferred_height_vfunc_dm(widget, minimum_height, natural_height);
|
||||
}
|
||||
}
|
||||
|
||||
void CellRendererChannels::get_preferred_height_for_width_vfunc(Gtk::Widget &widget, int width, int &minimum_height, int &natural_height) const {
|
||||
switch (m_property_type.get_value()) {
|
||||
case RenderType::Guild:
|
||||
return get_preferred_height_for_width_vfunc_guild(widget, width, minimum_height, natural_height);
|
||||
case RenderType::Category:
|
||||
return get_preferred_height_for_width_vfunc_category(widget, width, minimum_height, natural_height);
|
||||
case RenderType::TextChannel:
|
||||
return get_preferred_height_for_width_vfunc_channel(widget, width, minimum_height, natural_height);
|
||||
case RenderType::Thread:
|
||||
return get_preferred_height_for_width_vfunc_thread(widget, width, minimum_height, natural_height);
|
||||
case RenderType::DMHeader:
|
||||
return get_preferred_height_for_width_vfunc_dmheader(widget, width, minimum_height, natural_height);
|
||||
case RenderType::DM:
|
||||
return get_preferred_height_for_width_vfunc_dm(widget, width, minimum_height, natural_height);
|
||||
}
|
||||
}
|
||||
|
||||
void CellRendererChannels::render_vfunc(const Cairo::RefPtr<Cairo::Context> &cr, Gtk::Widget &widget, const Gdk::Rectangle &background_area, const Gdk::Rectangle &cell_area, Gtk::CellRendererState flags) {
|
||||
switch (m_property_type.get_value()) {
|
||||
case RenderType::Guild:
|
||||
return render_vfunc_guild(cr, widget, background_area, cell_area, flags);
|
||||
case RenderType::Category:
|
||||
return render_vfunc_category(cr, widget, background_area, cell_area, flags);
|
||||
case RenderType::TextChannel:
|
||||
return render_vfunc_channel(cr, widget, background_area, cell_area, flags);
|
||||
case RenderType::Thread:
|
||||
return render_vfunc_thread(cr, widget, background_area, cell_area, flags);
|
||||
case RenderType::DMHeader:
|
||||
return render_vfunc_dmheader(cr, widget, background_area, cell_area, flags);
|
||||
case RenderType::DM:
|
||||
return render_vfunc_dm(cr, widget, background_area, cell_area, flags);
|
||||
}
|
||||
}
|
||||
|
||||
// guild functions
|
||||
|
||||
void CellRendererChannels::get_preferred_width_vfunc_guild(Gtk::Widget &widget, int &minimum_width, int &natural_width) const {
|
||||
int pixbuf_width = 0;
|
||||
|
||||
if (auto pixbuf = m_property_pixbuf_animation.get_value())
|
||||
pixbuf_width = pixbuf->get_width();
|
||||
else if (auto pixbuf = m_property_pixbuf.get_value())
|
||||
pixbuf_width = pixbuf->get_width();
|
||||
|
||||
int text_min, text_nat;
|
||||
m_renderer_text.get_preferred_width(widget, text_min, text_nat);
|
||||
|
||||
int xpad, ypad;
|
||||
get_padding(xpad, ypad);
|
||||
minimum_width = std::max(text_min, pixbuf_width) + xpad * 2;
|
||||
natural_width = std::max(text_nat, pixbuf_width) + xpad * 2;
|
||||
}
|
||||
|
||||
void CellRendererChannels::get_preferred_width_for_height_vfunc_guild(Gtk::Widget &widget, int height, int &minimum_width, int &natural_width) const {
|
||||
get_preferred_width_vfunc_guild(widget, minimum_width, natural_width);
|
||||
}
|
||||
|
||||
void CellRendererChannels::get_preferred_height_vfunc_guild(Gtk::Widget &widget, int &minimum_height, int &natural_height) const {
|
||||
int pixbuf_height = 0;
|
||||
if (auto pixbuf = m_property_pixbuf_animation.get_value())
|
||||
pixbuf_height = pixbuf->get_height();
|
||||
else if (auto pixbuf = m_property_pixbuf.get_value())
|
||||
pixbuf_height = pixbuf->get_height();
|
||||
|
||||
int text_min, text_nat;
|
||||
m_renderer_text.get_preferred_height(widget, text_min, text_nat);
|
||||
|
||||
int xpad, ypad;
|
||||
get_padding(xpad, ypad);
|
||||
minimum_height = std::max(text_min, pixbuf_height) + ypad * 2;
|
||||
natural_height = std::max(text_nat, pixbuf_height) + ypad * 2;
|
||||
}
|
||||
|
||||
void CellRendererChannels::get_preferred_height_for_width_vfunc_guild(Gtk::Widget &widget, int width, int &minimum_height, int &natural_height) const {
|
||||
get_preferred_height_vfunc_guild(widget, minimum_height, natural_height);
|
||||
}
|
||||
|
||||
void CellRendererChannels::render_vfunc_guild(const Cairo::RefPtr<Cairo::Context> &cr, Gtk::Widget &widget, const Gdk::Rectangle &background_area, const Gdk::Rectangle &cell_area, Gtk::CellRendererState flags) {
|
||||
Gtk::Requisition text_minimum, text_natural;
|
||||
m_renderer_text.get_preferred_size(widget, text_minimum, text_natural);
|
||||
|
||||
Gtk::Requisition minimum, natural;
|
||||
get_preferred_size(widget, minimum, natural);
|
||||
|
||||
int pixbuf_w, pixbuf_h = 0;
|
||||
if (auto pixbuf = m_property_pixbuf_animation.get_value()) {
|
||||
pixbuf_w = pixbuf->get_width();
|
||||
pixbuf_h = pixbuf->get_height();
|
||||
} else if (auto pixbuf = m_property_pixbuf.get_value()) {
|
||||
pixbuf_w = pixbuf->get_width();
|
||||
pixbuf_h = pixbuf->get_height();
|
||||
}
|
||||
|
||||
const double icon_w = pixbuf_w;
|
||||
const double icon_h = pixbuf_h;
|
||||
const double icon_x = background_area.get_x();
|
||||
const double icon_y = background_area.get_y() + background_area.get_height() / 2.0 - icon_h / 2.0;
|
||||
|
||||
const double text_x = icon_x + icon_w + 5.0;
|
||||
const double text_y = background_area.get_y() + background_area.get_height() / 2.0 - text_natural.height / 2.0;
|
||||
const double text_w = text_natural.width;
|
||||
const double text_h = text_natural.height;
|
||||
|
||||
Gdk::Rectangle text_cell_area(text_x, text_y, text_w, text_h);
|
||||
|
||||
m_renderer_text.render(cr, widget, background_area, text_cell_area, flags);
|
||||
|
||||
const bool hover_only = Abaddon::Get().GetSettings().AnimatedGuildHoverOnly;
|
||||
const bool is_hovered = flags & Gtk::CELL_RENDERER_PRELIT;
|
||||
auto anim = m_property_pixbuf_animation.get_value();
|
||||
|
||||
// kinda gross
|
||||
if (anim) {
|
||||
auto map_iter = m_pixbuf_anim_iters.find(anim);
|
||||
if (map_iter == m_pixbuf_anim_iters.end())
|
||||
m_pixbuf_anim_iters[anim] = anim->get_iter(nullptr);
|
||||
auto pb_iter = m_pixbuf_anim_iters.at(anim);
|
||||
|
||||
const auto cb = [this, &widget, anim, icon_x, icon_y, icon_w, icon_h] {
|
||||
if (m_pixbuf_anim_iters.at(anim)->advance())
|
||||
widget.queue_draw_area(icon_x, icon_y, icon_w, icon_h);
|
||||
};
|
||||
|
||||
if ((hover_only && is_hovered) || !hover_only)
|
||||
Glib::signal_timeout().connect_once(sigc::track_obj(cb, widget), pb_iter->get_delay_time());
|
||||
if (hover_only && !is_hovered)
|
||||
m_pixbuf_anim_iters[anim] = anim->get_iter(nullptr);
|
||||
|
||||
Gdk::Cairo::set_source_pixbuf(cr, pb_iter->get_pixbuf(), icon_x, icon_y);
|
||||
cr->rectangle(icon_x, icon_y, icon_w, icon_h);
|
||||
cr->fill();
|
||||
} else if (auto pixbuf = m_property_pixbuf.get_value()) {
|
||||
Gdk::Cairo::set_source_pixbuf(cr, pixbuf, icon_x, icon_y);
|
||||
cr->rectangle(icon_x, icon_y, icon_w, icon_h);
|
||||
cr->fill();
|
||||
}
|
||||
}
|
||||
|
||||
// category
|
||||
|
||||
void CellRendererChannels::get_preferred_width_vfunc_category(Gtk::Widget &widget, int &minimum_width, int &natural_width) const {
|
||||
m_renderer_text.get_preferred_width(widget, minimum_width, natural_width);
|
||||
}
|
||||
|
||||
void CellRendererChannels::get_preferred_width_for_height_vfunc_category(Gtk::Widget &widget, int height, int &minimum_width, int &natural_width) const {
|
||||
m_renderer_text.get_preferred_width_for_height(widget, height, minimum_width, natural_width);
|
||||
}
|
||||
|
||||
void CellRendererChannels::get_preferred_height_vfunc_category(Gtk::Widget &widget, int &minimum_height, int &natural_height) const {
|
||||
m_renderer_text.get_preferred_height(widget, minimum_height, natural_height);
|
||||
}
|
||||
|
||||
void CellRendererChannels::get_preferred_height_for_width_vfunc_category(Gtk::Widget &widget, int width, int &minimum_height, int &natural_height) const {
|
||||
m_renderer_text.get_preferred_height_for_width(widget, width, minimum_height, natural_height);
|
||||
}
|
||||
|
||||
void CellRendererChannels::render_vfunc_category(const Cairo::RefPtr<Cairo::Context> &cr, Gtk::Widget &widget, const Gdk::Rectangle &background_area, const Gdk::Rectangle &cell_area, Gtk::CellRendererState flags) {
|
||||
// todo: figure out how Gtk::Arrow is rendered because i like it better :^)
|
||||
constexpr static int len = 5;
|
||||
int x1, y1, x2, y2, x3, y3;
|
||||
if (property_expanded()) {
|
||||
x1 = background_area.get_x() + 7;
|
||||
y1 = background_area.get_y() + background_area.get_height() / 2 - len;
|
||||
x2 = background_area.get_x() + 7 + len;
|
||||
y2 = background_area.get_y() + background_area.get_height() / 2 + len;
|
||||
x3 = background_area.get_x() + 7 + len * 2;
|
||||
y3 = background_area.get_y() + background_area.get_height() / 2 - len;
|
||||
} else {
|
||||
x1 = background_area.get_x() + 7;
|
||||
y1 = background_area.get_y() + background_area.get_height() / 2 - len;
|
||||
x2 = background_area.get_x() + 7 + len * 2;
|
||||
y2 = background_area.get_y() + background_area.get_height() / 2;
|
||||
x3 = background_area.get_x() + 7;
|
||||
y3 = background_area.get_y() + background_area.get_height() / 2 + len;
|
||||
}
|
||||
cr->move_to(x1, y1);
|
||||
cr->line_to(x2, y2);
|
||||
cr->line_to(x3, y3);
|
||||
const auto expander_color = Gdk::RGBA(Abaddon::Get().GetSettings().ChannelsExpanderColor);
|
||||
cr->set_source_rgb(expander_color.get_red(), expander_color.get_green(), expander_color.get_blue());
|
||||
cr->stroke();
|
||||
|
||||
Gtk::Requisition text_minimum, text_natural;
|
||||
m_renderer_text.get_preferred_size(widget, text_minimum, text_natural);
|
||||
|
||||
const int text_x = background_area.get_x() + 22;
|
||||
const int text_y = background_area.get_y() + background_area.get_height() / 2 - text_natural.height / 2;
|
||||
const int text_w = text_natural.width;
|
||||
const int text_h = text_natural.height;
|
||||
|
||||
Gdk::Rectangle text_cell_area(text_x, text_y, text_w, text_h);
|
||||
|
||||
m_renderer_text.render(cr, widget, background_area, text_cell_area, flags);
|
||||
}
|
||||
|
||||
// text channel
|
||||
|
||||
void CellRendererChannels::get_preferred_width_vfunc_channel(Gtk::Widget &widget, int &minimum_width, int &natural_width) const {
|
||||
m_renderer_text.get_preferred_width(widget, minimum_width, natural_width);
|
||||
}
|
||||
|
||||
void CellRendererChannels::get_preferred_width_for_height_vfunc_channel(Gtk::Widget &widget, int height, int &minimum_width, int &natural_width) const {
|
||||
m_renderer_text.get_preferred_width_for_height(widget, height, minimum_width, natural_width);
|
||||
}
|
||||
|
||||
void CellRendererChannels::get_preferred_height_vfunc_channel(Gtk::Widget &widget, int &minimum_height, int &natural_height) const {
|
||||
m_renderer_text.get_preferred_height(widget, minimum_height, natural_height);
|
||||
}
|
||||
|
||||
void CellRendererChannels::get_preferred_height_for_width_vfunc_channel(Gtk::Widget &widget, int width, int &minimum_height, int &natural_height) const {
|
||||
m_renderer_text.get_preferred_height_for_width(widget, width, minimum_height, natural_height);
|
||||
}
|
||||
|
||||
void CellRendererChannels::render_vfunc_channel(const Cairo::RefPtr<Cairo::Context> &cr, Gtk::Widget &widget, const Gdk::Rectangle &background_area, const Gdk::Rectangle &cell_area, Gtk::CellRendererState flags) {
|
||||
Gtk::Requisition minimum_size, natural_size;
|
||||
m_renderer_text.get_preferred_size(widget, minimum_size, natural_size);
|
||||
|
||||
const int text_x = background_area.get_x() + 21;
|
||||
const int text_y = background_area.get_y() + background_area.get_height() / 2 - natural_size.height / 2;
|
||||
const int text_w = natural_size.width;
|
||||
const int text_h = natural_size.height;
|
||||
|
||||
Gdk::Rectangle text_cell_area(text_x, text_y, text_w, text_h);
|
||||
|
||||
const auto nsfw_color = Gdk::RGBA(Abaddon::Get().GetSettings().NSFWChannelColor);
|
||||
if (m_property_nsfw.get_value())
|
||||
m_renderer_text.property_foreground_rgba() = nsfw_color;
|
||||
m_renderer_text.render(cr, widget, background_area, text_cell_area, flags);
|
||||
// setting property_foreground_rgba() sets this to true which makes non-nsfw cells use the property too which is bad
|
||||
// so unset it
|
||||
m_renderer_text.property_foreground_set() = false;
|
||||
}
|
||||
|
||||
// thread
|
||||
|
||||
void CellRendererChannels::get_preferred_width_vfunc_thread(Gtk::Widget &widget, int &minimum_width, int &natural_width) const {
|
||||
m_renderer_text.get_preferred_width(widget, minimum_width, natural_width);
|
||||
}
|
||||
|
||||
void CellRendererChannels::get_preferred_width_for_height_vfunc_thread(Gtk::Widget &widget, int height, int &minimum_width, int &natural_width) const {
|
||||
get_preferred_width_vfunc_thread(widget, minimum_width, natural_width);
|
||||
}
|
||||
|
||||
void CellRendererChannels::get_preferred_height_vfunc_thread(Gtk::Widget &widget, int &minimum_height, int &natural_height) const {
|
||||
m_renderer_text.get_preferred_height(widget, minimum_height, natural_height);
|
||||
}
|
||||
|
||||
void CellRendererChannels::get_preferred_height_for_width_vfunc_thread(Gtk::Widget &widget, int width, int &minimum_height, int &natural_height) const {
|
||||
get_preferred_height_vfunc_thread(widget, minimum_height, natural_height);
|
||||
}
|
||||
|
||||
void CellRendererChannels::render_vfunc_thread(const Cairo::RefPtr<Cairo::Context> &cr, Gtk::Widget &widget, const Gdk::Rectangle &background_area, const Gdk::Rectangle &cell_area, Gtk::CellRendererState flags) {
|
||||
Gtk::Requisition minimum_size, natural_size;
|
||||
m_renderer_text.get_preferred_size(widget, minimum_size, natural_size);
|
||||
|
||||
const int text_x = background_area.get_x() + 26;
|
||||
const int text_y = background_area.get_y() + background_area.get_height() / 2 - natural_size.height / 2;
|
||||
const int text_w = natural_size.width;
|
||||
const int text_h = natural_size.height;
|
||||
|
||||
Gdk::Rectangle text_cell_area(text_x, text_y, text_w, text_h);
|
||||
m_renderer_text.render(cr, widget, background_area, text_cell_area, flags);
|
||||
}
|
||||
|
||||
// dm header
|
||||
|
||||
void CellRendererChannels::get_preferred_width_vfunc_dmheader(Gtk::Widget &widget, int &minimum_width, int &natural_width) const {
|
||||
m_renderer_text.get_preferred_width(widget, minimum_width, natural_width);
|
||||
}
|
||||
|
||||
void CellRendererChannels::get_preferred_width_for_height_vfunc_dmheader(Gtk::Widget &widget, int height, int &minimum_width, int &natural_width) const {
|
||||
m_renderer_text.get_preferred_width_for_height(widget, height, minimum_width, natural_width);
|
||||
}
|
||||
|
||||
void CellRendererChannels::get_preferred_height_vfunc_dmheader(Gtk::Widget &widget, int &minimum_height, int &natural_height) const {
|
||||
m_renderer_text.get_preferred_height(widget, minimum_height, natural_height);
|
||||
}
|
||||
|
||||
void CellRendererChannels::get_preferred_height_for_width_vfunc_dmheader(Gtk::Widget &widget, int width, int &minimum_height, int &natural_height) const {
|
||||
m_renderer_text.get_preferred_height_for_width(widget, width, minimum_height, natural_height);
|
||||
}
|
||||
|
||||
void CellRendererChannels::render_vfunc_dmheader(const Cairo::RefPtr<Cairo::Context> &cr, Gtk::Widget &widget, const Gdk::Rectangle &background_area, const Gdk::Rectangle &cell_area, Gtk::CellRendererState flags) {
|
||||
// gdk::rectangle more like gdk::stupid
|
||||
Gdk::Rectangle text_cell_area(
|
||||
cell_area.get_x() + 9, cell_area.get_y(), // maybe theres a better way to align this ?
|
||||
cell_area.get_width(), cell_area.get_height());
|
||||
m_renderer_text.render(cr, widget, background_area, text_cell_area, flags);
|
||||
}
|
||||
|
||||
// dm (basically the same thing as guild)
|
||||
|
||||
void CellRendererChannels::get_preferred_width_vfunc_dm(Gtk::Widget &widget, int &minimum_width, int &natural_width) const {
|
||||
int pixbuf_width = 0;
|
||||
if (auto pixbuf = m_property_pixbuf.get_value())
|
||||
pixbuf_width = pixbuf->get_width();
|
||||
|
||||
int text_min, text_nat;
|
||||
m_renderer_text.get_preferred_width(widget, text_min, text_nat);
|
||||
|
||||
int xpad, ypad;
|
||||
get_padding(xpad, ypad);
|
||||
minimum_width = std::max(text_min, pixbuf_width) + xpad * 2;
|
||||
natural_width = std::max(text_nat, pixbuf_width) + xpad * 2;
|
||||
}
|
||||
|
||||
void CellRendererChannels::get_preferred_width_for_height_vfunc_dm(Gtk::Widget &widget, int height, int &minimum_width, int &natural_width) const {
|
||||
get_preferred_width_vfunc_guild(widget, minimum_width, natural_width);
|
||||
}
|
||||
|
||||
void CellRendererChannels::get_preferred_height_vfunc_dm(Gtk::Widget &widget, int &minimum_height, int &natural_height) const {
|
||||
int pixbuf_height = 0;
|
||||
if (auto pixbuf = m_property_pixbuf.get_value())
|
||||
pixbuf_height = pixbuf->get_height();
|
||||
|
||||
int text_min, text_nat;
|
||||
m_renderer_text.get_preferred_height(widget, text_min, text_nat);
|
||||
|
||||
int xpad, ypad;
|
||||
get_padding(xpad, ypad);
|
||||
minimum_height = std::max(text_min, pixbuf_height) + ypad * 2;
|
||||
natural_height = std::max(text_nat, pixbuf_height) + ypad * 2;
|
||||
}
|
||||
|
||||
void CellRendererChannels::get_preferred_height_for_width_vfunc_dm(Gtk::Widget &widget, int width, int &minimum_height, int &natural_height) const {
|
||||
get_preferred_height_vfunc_guild(widget, minimum_height, natural_height);
|
||||
}
|
||||
|
||||
void CellRendererChannels::render_vfunc_dm(const Cairo::RefPtr<Cairo::Context> &cr, Gtk::Widget &widget, const Gdk::Rectangle &background_area, const Gdk::Rectangle &cell_area, Gtk::CellRendererState flags) {
|
||||
Gtk::Requisition text_minimum, text_natural;
|
||||
m_renderer_text.get_preferred_size(widget, text_minimum, text_natural);
|
||||
|
||||
Gtk::Requisition minimum, natural;
|
||||
get_preferred_size(widget, minimum, natural);
|
||||
|
||||
auto pixbuf = m_property_pixbuf.get_value();
|
||||
|
||||
const double icon_w = pixbuf->get_width();
|
||||
const double icon_h = pixbuf->get_height();
|
||||
const double icon_x = background_area.get_x() + 2;
|
||||
const double icon_y = background_area.get_y() + background_area.get_height() / 2.0 - icon_h / 2.0;
|
||||
|
||||
const double text_x = icon_x + icon_w + 5.0;
|
||||
const double text_y = background_area.get_y() + background_area.get_height() / 2.0 - text_natural.height / 2.0;
|
||||
const double text_w = text_natural.width;
|
||||
const double text_h = text_natural.height;
|
||||
|
||||
Gdk::Rectangle text_cell_area(text_x, text_y, text_w, text_h);
|
||||
|
||||
m_renderer_text.render(cr, widget, background_area, text_cell_area, flags);
|
||||
|
||||
Gdk::Cairo::set_source_pixbuf(cr, m_property_pixbuf.get_value(), icon_x, icon_y);
|
||||
cr->rectangle(icon_x, icon_y, icon_w, icon_h);
|
||||
cr->fill();
|
||||
}
|
||||
|
||||
@@ -8,138 +8,28 @@
|
||||
#include <sigc++/sigc++.h>
|
||||
#include "discord/discord.hpp"
|
||||
#include "state.hpp"
|
||||
#include "channelscellrenderer.hpp"
|
||||
|
||||
constexpr static int GuildIconSize = 24;
|
||||
constexpr static int DMIconSize = 20;
|
||||
constexpr static int OrphanChannelSortOffset = -100; // forces orphan channels to the top of the list
|
||||
|
||||
enum class RenderType : uint8_t {
|
||||
Guild,
|
||||
Category,
|
||||
TextChannel,
|
||||
Thread,
|
||||
|
||||
DMHeader,
|
||||
DM,
|
||||
};
|
||||
|
||||
class CellRendererChannels : public Gtk::CellRenderer {
|
||||
public:
|
||||
CellRendererChannels();
|
||||
virtual ~CellRendererChannels();
|
||||
|
||||
Glib::PropertyProxy<RenderType> property_type();
|
||||
Glib::PropertyProxy<Glib::ustring> property_name();
|
||||
Glib::PropertyProxy<Glib::RefPtr<Gdk::Pixbuf>> property_icon();
|
||||
Glib::PropertyProxy<Glib::RefPtr<Gdk::PixbufAnimation>> property_icon_animation();
|
||||
Glib::PropertyProxy<bool> property_expanded();
|
||||
Glib::PropertyProxy<bool> property_nsfw();
|
||||
|
||||
protected:
|
||||
void get_preferred_width_vfunc(Gtk::Widget &widget, int &minimum_width, int &natural_width) const override;
|
||||
void get_preferred_width_for_height_vfunc(Gtk::Widget &widget, int height, int &minimum_width, int &natural_width) const override;
|
||||
void get_preferred_height_vfunc(Gtk::Widget &widget, int &minimum_height, int &natural_height) const override;
|
||||
void get_preferred_height_for_width_vfunc(Gtk::Widget &widget, int width, int &minimum_height, int &natural_height) const override;
|
||||
void render_vfunc(const Cairo::RefPtr<Cairo::Context> &cr,
|
||||
Gtk::Widget &widget,
|
||||
const Gdk::Rectangle &background_area,
|
||||
const Gdk::Rectangle &cell_area,
|
||||
Gtk::CellRendererState flags) override;
|
||||
|
||||
// guild functions
|
||||
void get_preferred_width_vfunc_guild(Gtk::Widget &widget, int &minimum_width, int &natural_width) const;
|
||||
void get_preferred_width_for_height_vfunc_guild(Gtk::Widget &widget, int height, int &minimum_width, int &natural_width) const;
|
||||
void get_preferred_height_vfunc_guild(Gtk::Widget &widget, int &minimum_height, int &natural_height) const;
|
||||
void get_preferred_height_for_width_vfunc_guild(Gtk::Widget &widget, int width, int &minimum_height, int &natural_height) const;
|
||||
void render_vfunc_guild(const Cairo::RefPtr<Cairo::Context> &cr,
|
||||
Gtk::Widget &widget,
|
||||
const Gdk::Rectangle &background_area,
|
||||
const Gdk::Rectangle &cell_area,
|
||||
Gtk::CellRendererState flags);
|
||||
|
||||
// category
|
||||
void get_preferred_width_vfunc_category(Gtk::Widget &widget, int &minimum_width, int &natural_width) const;
|
||||
void get_preferred_width_for_height_vfunc_category(Gtk::Widget &widget, int height, int &minimum_width, int &natural_width) const;
|
||||
void get_preferred_height_vfunc_category(Gtk::Widget &widget, int &minimum_height, int &natural_height) const;
|
||||
void get_preferred_height_for_width_vfunc_category(Gtk::Widget &widget, int width, int &minimum_height, int &natural_height) const;
|
||||
void render_vfunc_category(const Cairo::RefPtr<Cairo::Context> &cr,
|
||||
Gtk::Widget &widget,
|
||||
const Gdk::Rectangle &background_area,
|
||||
const Gdk::Rectangle &cell_area,
|
||||
Gtk::CellRendererState flags);
|
||||
|
||||
// text channel
|
||||
void get_preferred_width_vfunc_channel(Gtk::Widget &widget, int &minimum_width, int &natural_width) const;
|
||||
void get_preferred_width_for_height_vfunc_channel(Gtk::Widget &widget, int height, int &minimum_width, int &natural_width) const;
|
||||
void get_preferred_height_vfunc_channel(Gtk::Widget &widget, int &minimum_height, int &natural_height) const;
|
||||
void get_preferred_height_for_width_vfunc_channel(Gtk::Widget &widget, int width, int &minimum_height, int &natural_height) const;
|
||||
void render_vfunc_channel(const Cairo::RefPtr<Cairo::Context> &cr,
|
||||
Gtk::Widget &widget,
|
||||
const Gdk::Rectangle &background_area,
|
||||
const Gdk::Rectangle &cell_area,
|
||||
Gtk::CellRendererState flags);
|
||||
|
||||
// thread
|
||||
void get_preferred_width_vfunc_thread(Gtk::Widget &widget, int &minimum_width, int &natural_width) const;
|
||||
void get_preferred_width_for_height_vfunc_thread(Gtk::Widget &widget, int height, int &minimum_width, int &natural_width) const;
|
||||
void get_preferred_height_vfunc_thread(Gtk::Widget &widget, int &minimum_height, int &natural_height) const;
|
||||
void get_preferred_height_for_width_vfunc_thread(Gtk::Widget &widget, int width, int &minimum_height, int &natural_height) const;
|
||||
void render_vfunc_thread(const Cairo::RefPtr<Cairo::Context> &cr,
|
||||
Gtk::Widget &widget,
|
||||
const Gdk::Rectangle &background_area,
|
||||
const Gdk::Rectangle &cell_area,
|
||||
Gtk::CellRendererState flags);
|
||||
|
||||
// dm header
|
||||
void get_preferred_width_vfunc_dmheader(Gtk::Widget &widget, int &minimum_width, int &natural_width) const;
|
||||
void get_preferred_width_for_height_vfunc_dmheader(Gtk::Widget &widget, int height, int &minimum_width, int &natural_width) const;
|
||||
void get_preferred_height_vfunc_dmheader(Gtk::Widget &widget, int &minimum_height, int &natural_height) const;
|
||||
void get_preferred_height_for_width_vfunc_dmheader(Gtk::Widget &widget, int width, int &minimum_height, int &natural_height) const;
|
||||
void render_vfunc_dmheader(const Cairo::RefPtr<Cairo::Context> &cr,
|
||||
Gtk::Widget &widget,
|
||||
const Gdk::Rectangle &background_area,
|
||||
const Gdk::Rectangle &cell_area,
|
||||
Gtk::CellRendererState flags);
|
||||
|
||||
// dm
|
||||
void get_preferred_width_vfunc_dm(Gtk::Widget &widget, int &minimum_width, int &natural_width) const;
|
||||
void get_preferred_width_for_height_vfunc_dm(Gtk::Widget &widget, int height, int &minimum_width, int &natural_width) const;
|
||||
void get_preferred_height_vfunc_dm(Gtk::Widget &widget, int &minimum_height, int &natural_height) const;
|
||||
void get_preferred_height_for_width_vfunc_dm(Gtk::Widget &widget, int width, int &minimum_height, int &natural_height) const;
|
||||
void render_vfunc_dm(const Cairo::RefPtr<Cairo::Context> &cr,
|
||||
Gtk::Widget &widget,
|
||||
const Gdk::Rectangle &background_area,
|
||||
const Gdk::Rectangle &cell_area,
|
||||
Gtk::CellRendererState flags);
|
||||
|
||||
private:
|
||||
Gtk::CellRendererText m_renderer_text;
|
||||
|
||||
Glib::Property<RenderType> m_property_type; // all
|
||||
Glib::Property<Glib::ustring> m_property_name; // all
|
||||
Glib::Property<Glib::RefPtr<Gdk::Pixbuf>> m_property_pixbuf; // guild, dm
|
||||
Glib::Property<Glib::RefPtr<Gdk::PixbufAnimation>> m_property_pixbuf_animation; // guild
|
||||
Glib::Property<bool> m_property_expanded; // category
|
||||
Glib::Property<bool> m_property_nsfw; // channel
|
||||
|
||||
// same pitfalls as in https://github.com/uowuo/abaddon/blob/60404783bd4ce9be26233fe66fc3a74475d9eaa3/components/cellrendererpixbufanimation.hpp#L32-L39
|
||||
// this will manifest though since guild icons can change
|
||||
// an animation or two wont be the end of the world though
|
||||
std::map<Glib::RefPtr<Gdk::PixbufAnimation>, Glib::RefPtr<Gdk::PixbufAnimationIter>> m_pixbuf_anim_iters;
|
||||
};
|
||||
|
||||
class ChannelList : public Gtk::ScrolledWindow {
|
||||
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);
|
||||
ExpansionStateRoot GetExpansionState() const;
|
||||
|
||||
void UsePanedHack(Gtk::Paned &paned);
|
||||
|
||||
protected:
|
||||
void OnPanedPositionChanged();
|
||||
|
||||
void UpdateNewGuild(const GuildData &guild);
|
||||
void UpdateRemoveGuild(Snowflake id);
|
||||
void UpdateRemoveChannel(Snowflake id);
|
||||
@@ -147,6 +37,10 @@ protected:
|
||||
void UpdateCreateChannel(const ChannelData &channel);
|
||||
void UpdateGuild(Snowflake id);
|
||||
void DeleteThreadRow(Snowflake id);
|
||||
void OnChannelMute(Snowflake id);
|
||||
void OnChannelUnmute(Snowflake id);
|
||||
void OnGuildMute(Snowflake id);
|
||||
void OnGuildUnmute(Snowflake id);
|
||||
|
||||
void OnThreadJoined(Snowflake id);
|
||||
void OnThreadRemoved(Snowflake id);
|
||||
@@ -190,7 +84,7 @@ protected:
|
||||
|
||||
bool IsTextChannel(ChannelType type);
|
||||
|
||||
void OnRowCollapsed(const Gtk::TreeModel::iterator &iter, const Gtk::TreeModel::Path &path);
|
||||
void OnRowCollapsed(const Gtk::TreeModel::iterator &iter, const Gtk::TreeModel::Path &path) const;
|
||||
void OnRowExpanded(const Gtk::TreeModel::iterator &iter, const Gtk::TreeModel::Path &path);
|
||||
bool SelectionFunc(const Glib::RefPtr<Gtk::TreeModel> &model, const Gtk::TreeModel::Path &path, bool is_currently_selected);
|
||||
bool OnButtonPressEvent(GdkEventButton *ev);
|
||||
@@ -203,6 +97,8 @@ protected:
|
||||
void AddPrivateChannels();
|
||||
void UpdateCreateDMChannel(const ChannelData &channel);
|
||||
|
||||
void OnMessageAck(const MessageAckData &data);
|
||||
|
||||
void OnMessageCreate(const Message &msg);
|
||||
Gtk::TreeModel::Path m_path_for_menu;
|
||||
|
||||
@@ -213,38 +109,73 @@ protected:
|
||||
Gtk::MenuItem m_menu_guild_copy_id;
|
||||
Gtk::MenuItem m_menu_guild_settings;
|
||||
Gtk::MenuItem m_menu_guild_leave;
|
||||
Gtk::MenuItem m_menu_guild_mark_as_read;
|
||||
Gtk::MenuItem m_menu_guild_toggle_mute;
|
||||
|
||||
Gtk::Menu m_menu_category;
|
||||
Gtk::MenuItem m_menu_category_copy_id;
|
||||
Gtk::MenuItem m_menu_category_toggle_mute;
|
||||
|
||||
Gtk::Menu m_menu_channel;
|
||||
Gtk::MenuItem m_menu_channel_copy_id;
|
||||
Gtk::MenuItem m_menu_channel_mark_as_read;
|
||||
Gtk::MenuItem m_menu_channel_toggle_mute;
|
||||
|
||||
#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;
|
||||
Gtk::MenuItem m_menu_thread_archive;
|
||||
Gtk::MenuItem m_menu_thread_unarchive;
|
||||
Gtk::MenuItem m_menu_thread_mark_as_read;
|
||||
Gtk::MenuItem m_menu_thread_toggle_mute;
|
||||
|
||||
void OnThreadSubmenuPopup(const Gdk::Rectangle *flipped_rect, const Gdk::Rectangle *final_rect, bool flipped_x, bool flipped_y);
|
||||
void OnGuildSubmenuPopup();
|
||||
void OnCategorySubmenuPopup();
|
||||
void OnChannelSubmenuPopup();
|
||||
void OnDMSubmenuPopup();
|
||||
void OnThreadSubmenuPopup();
|
||||
|
||||
bool m_updating_listing = false;
|
||||
|
||||
Snowflake m_active_channel;
|
||||
|
||||
// (GetIteratorForChannelFromID is rather slow)
|
||||
// only temporary since i dont want to worry about maintaining this map
|
||||
std::unordered_map<Snowflake, Gtk::TreeModel::iterator> m_tmp_channel_map;
|
||||
|
||||
public:
|
||||
typedef sigc::signal<void, Snowflake> type_signal_action_channel_item_select;
|
||||
typedef sigc::signal<void, Snowflake> type_signal_action_guild_leave;
|
||||
typedef sigc::signal<void, Snowflake> type_signal_action_guild_settings;
|
||||
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
|
||||
};
|
||||
|
||||
673
src/components/channelscellrenderer.cpp
Normal file
673
src/components/channelscellrenderer.cpp
Normal file
@@ -0,0 +1,673 @@
|
||||
#include "abaddon.hpp"
|
||||
#include "channelscellrenderer.hpp"
|
||||
#include <gtkmm.h>
|
||||
|
||||
constexpr static int MentionsRightPad = 7;
|
||||
#ifndef M_PI
|
||||
constexpr static double M_PI = 3.14159265358979;
|
||||
#endif
|
||||
constexpr static double M_PI_H = M_PI / 2.0;
|
||||
constexpr static double M_PI_3_2 = M_PI * 3.0 / 2.0;
|
||||
|
||||
CellRendererChannels::CellRendererChannels()
|
||||
: Glib::ObjectBase(typeid(CellRendererChannels))
|
||||
, Gtk::CellRenderer()
|
||||
, m_property_type(*this, "render-type")
|
||||
, m_property_id(*this, "id")
|
||||
, m_property_name(*this, "name")
|
||||
, m_property_pixbuf(*this, "pixbuf")
|
||||
, m_property_pixbuf_animation(*this, "pixbuf-animation")
|
||||
, m_property_expanded(*this, "expanded")
|
||||
, m_property_nsfw(*this, "nsfw") {
|
||||
property_mode() = Gtk::CELL_RENDERER_MODE_ACTIVATABLE;
|
||||
property_xpad() = 2;
|
||||
property_ypad() = 2;
|
||||
m_property_name.get_proxy().signal_changed().connect([this] {
|
||||
m_renderer_text.property_markup() = m_property_name;
|
||||
});
|
||||
}
|
||||
|
||||
Glib::PropertyProxy<RenderType> CellRendererChannels::property_type() {
|
||||
return m_property_type.get_proxy();
|
||||
}
|
||||
|
||||
Glib::PropertyProxy<uint64_t> CellRendererChannels::property_id() {
|
||||
return m_property_id.get_proxy();
|
||||
}
|
||||
|
||||
Glib::PropertyProxy<Glib::ustring> CellRendererChannels::property_name() {
|
||||
return m_property_name.get_proxy();
|
||||
}
|
||||
|
||||
Glib::PropertyProxy<Glib::RefPtr<Gdk::Pixbuf>> CellRendererChannels::property_icon() {
|
||||
return m_property_pixbuf.get_proxy();
|
||||
}
|
||||
|
||||
Glib::PropertyProxy<Glib::RefPtr<Gdk::PixbufAnimation>> CellRendererChannels::property_icon_animation() {
|
||||
return m_property_pixbuf_animation.get_proxy();
|
||||
}
|
||||
|
||||
Glib::PropertyProxy<bool> CellRendererChannels::property_expanded() {
|
||||
return m_property_expanded.get_proxy();
|
||||
}
|
||||
|
||||
Glib::PropertyProxy<bool> CellRendererChannels::property_nsfw() {
|
||||
return m_property_nsfw.get_proxy();
|
||||
}
|
||||
|
||||
void CellRendererChannels::get_preferred_width_vfunc(Gtk::Widget &widget, int &minimum_width, int &natural_width) const {
|
||||
switch (m_property_type.get_value()) {
|
||||
case RenderType::Guild:
|
||||
return get_preferred_width_vfunc_guild(widget, minimum_width, natural_width);
|
||||
case RenderType::Category:
|
||||
return get_preferred_width_vfunc_category(widget, minimum_width, natural_width);
|
||||
case RenderType::TextChannel:
|
||||
return get_preferred_width_vfunc_channel(widget, minimum_width, natural_width);
|
||||
case RenderType::Thread:
|
||||
return get_preferred_width_vfunc_thread(widget, minimum_width, natural_width);
|
||||
case RenderType::DMHeader:
|
||||
return get_preferred_width_vfunc_dmheader(widget, minimum_width, natural_width);
|
||||
case RenderType::DM:
|
||||
return get_preferred_width_vfunc_dm(widget, minimum_width, natural_width);
|
||||
}
|
||||
}
|
||||
|
||||
void CellRendererChannels::get_preferred_width_for_height_vfunc(Gtk::Widget &widget, int height, int &minimum_width, int &natural_width) const {
|
||||
switch (m_property_type.get_value()) {
|
||||
case RenderType::Guild:
|
||||
return get_preferred_width_for_height_vfunc_guild(widget, height, minimum_width, natural_width);
|
||||
case RenderType::Category:
|
||||
return get_preferred_width_for_height_vfunc_category(widget, height, minimum_width, natural_width);
|
||||
case RenderType::TextChannel:
|
||||
return get_preferred_width_for_height_vfunc_channel(widget, height, minimum_width, natural_width);
|
||||
case RenderType::Thread:
|
||||
return get_preferred_width_for_height_vfunc_thread(widget, height, minimum_width, natural_width);
|
||||
case RenderType::DMHeader:
|
||||
return get_preferred_width_for_height_vfunc_dmheader(widget, height, minimum_width, natural_width);
|
||||
case RenderType::DM:
|
||||
return get_preferred_width_for_height_vfunc_dm(widget, height, minimum_width, natural_width);
|
||||
}
|
||||
}
|
||||
|
||||
void CellRendererChannels::get_preferred_height_vfunc(Gtk::Widget &widget, int &minimum_height, int &natural_height) const {
|
||||
switch (m_property_type.get_value()) {
|
||||
case RenderType::Guild:
|
||||
return get_preferred_height_vfunc_guild(widget, minimum_height, natural_height);
|
||||
case RenderType::Category:
|
||||
return get_preferred_height_vfunc_category(widget, minimum_height, natural_height);
|
||||
case RenderType::TextChannel:
|
||||
return get_preferred_height_vfunc_channel(widget, minimum_height, natural_height);
|
||||
case RenderType::Thread:
|
||||
return get_preferred_height_vfunc_thread(widget, minimum_height, natural_height);
|
||||
case RenderType::DMHeader:
|
||||
return get_preferred_height_vfunc_dmheader(widget, minimum_height, natural_height);
|
||||
case RenderType::DM:
|
||||
return get_preferred_height_vfunc_dm(widget, minimum_height, natural_height);
|
||||
}
|
||||
}
|
||||
|
||||
void CellRendererChannels::get_preferred_height_for_width_vfunc(Gtk::Widget &widget, int width, int &minimum_height, int &natural_height) const {
|
||||
switch (m_property_type.get_value()) {
|
||||
case RenderType::Guild:
|
||||
return get_preferred_height_for_width_vfunc_guild(widget, width, minimum_height, natural_height);
|
||||
case RenderType::Category:
|
||||
return get_preferred_height_for_width_vfunc_category(widget, width, minimum_height, natural_height);
|
||||
case RenderType::TextChannel:
|
||||
return get_preferred_height_for_width_vfunc_channel(widget, width, minimum_height, natural_height);
|
||||
case RenderType::Thread:
|
||||
return get_preferred_height_for_width_vfunc_thread(widget, width, minimum_height, natural_height);
|
||||
case RenderType::DMHeader:
|
||||
return get_preferred_height_for_width_vfunc_dmheader(widget, width, minimum_height, natural_height);
|
||||
case RenderType::DM:
|
||||
return get_preferred_height_for_width_vfunc_dm(widget, width, minimum_height, natural_height);
|
||||
}
|
||||
}
|
||||
|
||||
void CellRendererChannels::render_vfunc(const Cairo::RefPtr<Cairo::Context> &cr, Gtk::Widget &widget, const Gdk::Rectangle &background_area, const Gdk::Rectangle &cell_area, Gtk::CellRendererState flags) {
|
||||
switch (m_property_type.get_value()) {
|
||||
case RenderType::Guild:
|
||||
return render_vfunc_guild(cr, widget, background_area, cell_area, flags);
|
||||
case RenderType::Category:
|
||||
return render_vfunc_category(cr, widget, background_area, cell_area, flags);
|
||||
case RenderType::TextChannel:
|
||||
return render_vfunc_channel(cr, widget, background_area, cell_area, flags);
|
||||
case RenderType::Thread:
|
||||
return render_vfunc_thread(cr, widget, background_area, cell_area, flags);
|
||||
case RenderType::DMHeader:
|
||||
return render_vfunc_dmheader(cr, widget, background_area, cell_area, flags);
|
||||
case RenderType::DM:
|
||||
return render_vfunc_dm(cr, widget, background_area, cell_area, flags);
|
||||
}
|
||||
}
|
||||
|
||||
// guild functions
|
||||
|
||||
void CellRendererChannels::get_preferred_width_vfunc_guild(Gtk::Widget &widget, int &minimum_width, int &natural_width) const {
|
||||
int pixbuf_width = 0;
|
||||
|
||||
if (auto pixbuf = m_property_pixbuf_animation.get_value())
|
||||
pixbuf_width = pixbuf->get_width();
|
||||
else if (auto pixbuf = m_property_pixbuf.get_value())
|
||||
pixbuf_width = pixbuf->get_width();
|
||||
|
||||
int text_min, text_nat;
|
||||
m_renderer_text.get_preferred_width(widget, text_min, text_nat);
|
||||
|
||||
int xpad, ypad;
|
||||
get_padding(xpad, ypad);
|
||||
minimum_width = std::max(text_min, pixbuf_width) + xpad * 2;
|
||||
natural_width = std::max(text_nat, pixbuf_width) + xpad * 2;
|
||||
}
|
||||
|
||||
void CellRendererChannels::get_preferred_width_for_height_vfunc_guild(Gtk::Widget &widget, int height, int &minimum_width, int &natural_width) const {
|
||||
get_preferred_width_vfunc_guild(widget, minimum_width, natural_width);
|
||||
}
|
||||
|
||||
void CellRendererChannels::get_preferred_height_vfunc_guild(Gtk::Widget &widget, int &minimum_height, int &natural_height) const {
|
||||
int pixbuf_height = 0;
|
||||
if (auto pixbuf = m_property_pixbuf_animation.get_value())
|
||||
pixbuf_height = pixbuf->get_height();
|
||||
else if (auto pixbuf = m_property_pixbuf.get_value())
|
||||
pixbuf_height = pixbuf->get_height();
|
||||
|
||||
int text_min, text_nat;
|
||||
m_renderer_text.get_preferred_height(widget, text_min, text_nat);
|
||||
|
||||
int xpad, ypad;
|
||||
get_padding(xpad, ypad);
|
||||
minimum_height = std::max(text_min, pixbuf_height) + ypad * 2;
|
||||
natural_height = std::max(text_nat, pixbuf_height) + ypad * 2;
|
||||
}
|
||||
|
||||
void CellRendererChannels::get_preferred_height_for_width_vfunc_guild(Gtk::Widget &widget, int width, int &minimum_height, int &natural_height) const {
|
||||
get_preferred_height_vfunc_guild(widget, minimum_height, natural_height);
|
||||
}
|
||||
|
||||
void CellRendererChannels::render_vfunc_guild(const Cairo::RefPtr<Cairo::Context> &cr, Gtk::Widget &widget, const Gdk::Rectangle &background_area, const Gdk::Rectangle &cell_area, Gtk::CellRendererState flags) {
|
||||
Gtk::Requisition text_minimum, text_natural;
|
||||
m_renderer_text.get_preferred_size(widget, text_minimum, text_natural);
|
||||
|
||||
Gtk::Requisition minimum, natural;
|
||||
get_preferred_size(widget, minimum, natural);
|
||||
|
||||
int pixbuf_w, pixbuf_h = 0;
|
||||
if (auto pixbuf = m_property_pixbuf_animation.get_value()) {
|
||||
pixbuf_w = pixbuf->get_width();
|
||||
pixbuf_h = pixbuf->get_height();
|
||||
} else if (auto pixbuf = m_property_pixbuf.get_value()) {
|
||||
pixbuf_w = pixbuf->get_width();
|
||||
pixbuf_h = pixbuf->get_height();
|
||||
}
|
||||
|
||||
const double icon_w = pixbuf_w;
|
||||
const double icon_h = pixbuf_h;
|
||||
const double icon_x = background_area.get_x() + 3;
|
||||
const double icon_y = background_area.get_y() + background_area.get_height() / 2.0 - icon_h / 2.0;
|
||||
|
||||
const double text_x = icon_x + icon_w + 5.0;
|
||||
const double text_y = background_area.get_y() + background_area.get_height() / 2.0 - text_natural.height / 2.0;
|
||||
const double text_w = text_natural.width;
|
||||
const double text_h = text_natural.height;
|
||||
|
||||
Gdk::Rectangle text_cell_area(static_cast<int>(text_x),
|
||||
static_cast<int>(text_y),
|
||||
static_cast<int>(text_w),
|
||||
static_cast<int>(text_h));
|
||||
|
||||
static const auto color = Gdk::RGBA(Abaddon::Get().GetSettings().ChannelColor);
|
||||
m_renderer_text.property_foreground_rgba() = color;
|
||||
m_renderer_text.render(cr, widget, background_area, text_cell_area, flags);
|
||||
|
||||
const bool hover_only = Abaddon::Get().GetSettings().AnimatedGuildHoverOnly;
|
||||
const bool is_hovered = flags & Gtk::CELL_RENDERER_PRELIT;
|
||||
auto anim = m_property_pixbuf_animation.get_value();
|
||||
|
||||
// kinda gross
|
||||
if (anim) {
|
||||
auto map_iter = m_pixbuf_anim_iters.find(anim);
|
||||
if (map_iter == m_pixbuf_anim_iters.end())
|
||||
m_pixbuf_anim_iters[anim] = anim->get_iter(nullptr);
|
||||
auto pb_iter = m_pixbuf_anim_iters.at(anim);
|
||||
|
||||
const auto cb = [this, &widget, anim, icon_x, icon_y, icon_w, icon_h] {
|
||||
if (m_pixbuf_anim_iters.at(anim)->advance())
|
||||
widget.queue_draw_area(
|
||||
static_cast<int>(icon_x),
|
||||
static_cast<int>(icon_y),
|
||||
static_cast<int>(icon_w),
|
||||
static_cast<int>(icon_h));
|
||||
};
|
||||
|
||||
if ((hover_only && is_hovered) || !hover_only)
|
||||
Glib::signal_timeout().connect_once(sigc::track_obj(cb, widget), pb_iter->get_delay_time());
|
||||
if (hover_only && !is_hovered)
|
||||
m_pixbuf_anim_iters[anim] = anim->get_iter(nullptr);
|
||||
|
||||
Gdk::Cairo::set_source_pixbuf(cr, pb_iter->get_pixbuf(), icon_x, icon_y);
|
||||
cr->rectangle(icon_x, icon_y, icon_w, icon_h);
|
||||
cr->fill();
|
||||
} else if (auto pixbuf = m_property_pixbuf.get_value()) {
|
||||
Gdk::Cairo::set_source_pixbuf(cr, pixbuf, icon_x, icon_y);
|
||||
cr->rectangle(icon_x, icon_y, icon_w, icon_h);
|
||||
cr->fill();
|
||||
}
|
||||
|
||||
// unread
|
||||
if (!Abaddon::Get().GetSettings().Unreads) return;
|
||||
|
||||
const auto id = m_property_id.get_value();
|
||||
|
||||
auto &discord = Abaddon::Get().GetDiscordClient();
|
||||
int total_mentions;
|
||||
const auto has_unread = discord.GetUnreadStateForGuild(id, total_mentions);
|
||||
|
||||
if (has_unread && !discord.IsGuildMuted(id)) {
|
||||
static const auto color = Gdk::RGBA(Abaddon::Get().GetSettings().UnreadIndicatorColor);
|
||||
cr->set_source_rgb(color.get_red(), color.get_green(), color.get_blue());
|
||||
const auto x = background_area.get_x();
|
||||
const auto y = background_area.get_y();
|
||||
const auto w = background_area.get_width();
|
||||
const auto h = background_area.get_height();
|
||||
cr->rectangle(x, y + h / 2.0 - 24.0 / 2.0, 3.0, 24.0);
|
||||
cr->fill();
|
||||
}
|
||||
|
||||
if (total_mentions < 1) return;
|
||||
auto *paned = dynamic_cast<Gtk::Paned *>(widget.get_ancestor(Gtk::Paned::get_type()));
|
||||
if (paned != nullptr) {
|
||||
const auto edge = std::min(paned->get_position(), background_area.get_width());
|
||||
|
||||
unread_render_mentions(cr, widget, total_mentions, edge, background_area);
|
||||
}
|
||||
}
|
||||
|
||||
// category
|
||||
|
||||
void CellRendererChannels::get_preferred_width_vfunc_category(Gtk::Widget &widget, int &minimum_width, int &natural_width) const {
|
||||
m_renderer_text.get_preferred_width(widget, minimum_width, natural_width);
|
||||
}
|
||||
|
||||
void CellRendererChannels::get_preferred_width_for_height_vfunc_category(Gtk::Widget &widget, int height, int &minimum_width, int &natural_width) const {
|
||||
m_renderer_text.get_preferred_width_for_height(widget, height, minimum_width, natural_width);
|
||||
}
|
||||
|
||||
void CellRendererChannels::get_preferred_height_vfunc_category(Gtk::Widget &widget, int &minimum_height, int &natural_height) const {
|
||||
m_renderer_text.get_preferred_height(widget, minimum_height, natural_height);
|
||||
}
|
||||
|
||||
void CellRendererChannels::get_preferred_height_for_width_vfunc_category(Gtk::Widget &widget, int width, int &minimum_height, int &natural_height) const {
|
||||
m_renderer_text.get_preferred_height_for_width(widget, width, minimum_height, natural_height);
|
||||
}
|
||||
|
||||
void CellRendererChannels::render_vfunc_category(const Cairo::RefPtr<Cairo::Context> &cr, Gtk::Widget &widget, const Gdk::Rectangle &background_area, const Gdk::Rectangle &cell_area, Gtk::CellRendererState flags) {
|
||||
// todo: figure out how Gtk::Arrow is rendered because i like it better :^)
|
||||
constexpr static int len = 5;
|
||||
int x1, y1, x2, y2, x3, y3;
|
||||
if (property_expanded()) {
|
||||
x1 = background_area.get_x() + 7;
|
||||
y1 = background_area.get_y() + background_area.get_height() / 2 - len;
|
||||
x2 = background_area.get_x() + 7 + len;
|
||||
y2 = background_area.get_y() + background_area.get_height() / 2 + len;
|
||||
x3 = background_area.get_x() + 7 + len * 2;
|
||||
y3 = background_area.get_y() + background_area.get_height() / 2 - len;
|
||||
} else {
|
||||
x1 = background_area.get_x() + 7;
|
||||
y1 = background_area.get_y() + background_area.get_height() / 2 - len;
|
||||
x2 = background_area.get_x() + 7 + len * 2;
|
||||
y2 = background_area.get_y() + background_area.get_height() / 2;
|
||||
x3 = background_area.get_x() + 7;
|
||||
y3 = background_area.get_y() + background_area.get_height() / 2 + len;
|
||||
}
|
||||
cr->move_to(x1, y1);
|
||||
cr->line_to(x2, y2);
|
||||
cr->line_to(x3, y3);
|
||||
const auto expander_color = Gdk::RGBA(Abaddon::Get().GetSettings().ChannelsExpanderColor);
|
||||
cr->set_source_rgb(expander_color.get_red(), expander_color.get_green(), expander_color.get_blue());
|
||||
cr->stroke();
|
||||
|
||||
Gtk::Requisition text_minimum, text_natural;
|
||||
m_renderer_text.get_preferred_size(widget, text_minimum, text_natural);
|
||||
|
||||
const int text_x = background_area.get_x() + 22;
|
||||
const int text_y = background_area.get_y() + background_area.get_height() / 2 - text_natural.height / 2;
|
||||
const int text_w = text_natural.width;
|
||||
const int text_h = text_natural.height;
|
||||
|
||||
Gdk::Rectangle text_cell_area(text_x, text_y, text_w, text_h);
|
||||
|
||||
static const auto color = Gdk::RGBA(Abaddon::Get().GetSettings().ChannelColor);
|
||||
if (Abaddon::Get().GetDiscordClient().IsChannelMuted(m_property_id.get_value())) {
|
||||
auto muted = color;
|
||||
muted.set_red(muted.get_red() * 0.5);
|
||||
muted.set_green(muted.get_green() * 0.5);
|
||||
muted.set_blue(muted.get_blue() * 0.5);
|
||||
m_renderer_text.property_foreground_rgba() = muted;
|
||||
} else {
|
||||
m_renderer_text.property_foreground_rgba() = color;
|
||||
}
|
||||
m_renderer_text.render(cr, widget, background_area, text_cell_area, flags);
|
||||
m_renderer_text.property_foreground_set() = false;
|
||||
}
|
||||
|
||||
// text channel
|
||||
|
||||
void CellRendererChannels::get_preferred_width_vfunc_channel(Gtk::Widget &widget, int &minimum_width, int &natural_width) const {
|
||||
m_renderer_text.get_preferred_width(widget, minimum_width, natural_width);
|
||||
}
|
||||
|
||||
void CellRendererChannels::get_preferred_width_for_height_vfunc_channel(Gtk::Widget &widget, int height, int &minimum_width, int &natural_width) const {
|
||||
m_renderer_text.get_preferred_width_for_height(widget, height, minimum_width, natural_width);
|
||||
}
|
||||
|
||||
void CellRendererChannels::get_preferred_height_vfunc_channel(Gtk::Widget &widget, int &minimum_height, int &natural_height) const {
|
||||
m_renderer_text.get_preferred_height(widget, minimum_height, natural_height);
|
||||
}
|
||||
|
||||
void CellRendererChannels::get_preferred_height_for_width_vfunc_channel(Gtk::Widget &widget, int width, int &minimum_height, int &natural_height) const {
|
||||
m_renderer_text.get_preferred_height_for_width(widget, width, minimum_height, natural_height);
|
||||
}
|
||||
|
||||
void CellRendererChannels::render_vfunc_channel(const Cairo::RefPtr<Cairo::Context> &cr, Gtk::Widget &widget, const Gdk::Rectangle &background_area, const Gdk::Rectangle &cell_area, Gtk::CellRendererState flags) {
|
||||
Gtk::Requisition minimum_size, natural_size;
|
||||
m_renderer_text.get_preferred_size(widget, minimum_size, natural_size);
|
||||
|
||||
const int text_x = background_area.get_x() + 21;
|
||||
const int text_y = background_area.get_y() + background_area.get_height() / 2 - natural_size.height / 2;
|
||||
const int text_w = natural_size.width;
|
||||
const int text_h = natural_size.height;
|
||||
|
||||
Gdk::Rectangle text_cell_area(text_x, text_y, text_w, text_h);
|
||||
|
||||
auto &discord = Abaddon::Get().GetDiscordClient();
|
||||
const auto id = m_property_id.get_value();
|
||||
const bool is_muted = discord.IsChannelMuted(id);
|
||||
|
||||
static const auto sfw_unmuted = Gdk::RGBA(Abaddon::Get().GetSettings().ChannelColor);
|
||||
|
||||
m_renderer_text.property_sensitive() = false;
|
||||
static const auto nsfw_color = Gdk::RGBA(Abaddon::Get().GetSettings().NSFWChannelColor);
|
||||
if (m_property_nsfw.get_value())
|
||||
m_renderer_text.property_foreground_rgba() = nsfw_color;
|
||||
else
|
||||
m_renderer_text.property_foreground_rgba() = sfw_unmuted;
|
||||
if (is_muted) {
|
||||
auto col = m_renderer_text.property_foreground_rgba().get_value();
|
||||
col.set_red(col.get_red() * 0.5);
|
||||
col.set_green(col.get_green() * 0.5);
|
||||
col.set_blue(col.get_blue() * 0.5);
|
||||
m_renderer_text.property_foreground_rgba() = col;
|
||||
}
|
||||
m_renderer_text.render(cr, widget, background_area, text_cell_area, flags);
|
||||
// unset foreground to default so properties dont bleed
|
||||
m_renderer_text.property_foreground_set() = false;
|
||||
|
||||
// unread
|
||||
if (!Abaddon::Get().GetSettings().Unreads) return;
|
||||
|
||||
const auto unread_state = discord.GetUnreadStateForChannel(id);
|
||||
if (unread_state < 0) return;
|
||||
|
||||
if (!is_muted) {
|
||||
static const auto color = Gdk::RGBA(Abaddon::Get().GetSettings().UnreadIndicatorColor);
|
||||
cr->set_source_rgb(color.get_red(), color.get_green(), color.get_blue());
|
||||
const auto x = background_area.get_x();
|
||||
const auto y = background_area.get_y();
|
||||
const auto w = background_area.get_width();
|
||||
const auto h = background_area.get_height();
|
||||
cr->rectangle(x, y, 3, h);
|
||||
cr->fill();
|
||||
}
|
||||
|
||||
if (unread_state < 1) return;
|
||||
auto *paned = dynamic_cast<Gtk::Paned *>(widget.get_ancestor(Gtk::Paned::get_type()));
|
||||
if (paned != nullptr) {
|
||||
const auto edge = std::min(paned->get_position(), cell_area.get_width());
|
||||
|
||||
unread_render_mentions(cr, widget, unread_state, edge, cell_area);
|
||||
}
|
||||
}
|
||||
|
||||
// thread
|
||||
|
||||
void CellRendererChannels::get_preferred_width_vfunc_thread(Gtk::Widget &widget, int &minimum_width, int &natural_width) const {
|
||||
m_renderer_text.get_preferred_width(widget, minimum_width, natural_width);
|
||||
}
|
||||
|
||||
void CellRendererChannels::get_preferred_width_for_height_vfunc_thread(Gtk::Widget &widget, int height, int &minimum_width, int &natural_width) const {
|
||||
get_preferred_width_vfunc_thread(widget, minimum_width, natural_width);
|
||||
}
|
||||
|
||||
void CellRendererChannels::get_preferred_height_vfunc_thread(Gtk::Widget &widget, int &minimum_height, int &natural_height) const {
|
||||
m_renderer_text.get_preferred_height(widget, minimum_height, natural_height);
|
||||
}
|
||||
|
||||
void CellRendererChannels::get_preferred_height_for_width_vfunc_thread(Gtk::Widget &widget, int width, int &minimum_height, int &natural_height) const {
|
||||
get_preferred_height_vfunc_thread(widget, minimum_height, natural_height);
|
||||
}
|
||||
|
||||
void CellRendererChannels::render_vfunc_thread(const Cairo::RefPtr<Cairo::Context> &cr, Gtk::Widget &widget, const Gdk::Rectangle &background_area, const Gdk::Rectangle &cell_area, Gtk::CellRendererState flags) {
|
||||
Gtk::Requisition minimum_size, natural_size;
|
||||
m_renderer_text.get_preferred_size(widget, minimum_size, natural_size);
|
||||
|
||||
const int text_x = background_area.get_x() + 26;
|
||||
const int text_y = background_area.get_y() + background_area.get_height() / 2 - natural_size.height / 2;
|
||||
const int text_w = natural_size.width;
|
||||
const int text_h = natural_size.height;
|
||||
|
||||
Gdk::Rectangle text_cell_area(text_x, text_y, text_w, text_h);
|
||||
|
||||
auto &discord = Abaddon::Get().GetDiscordClient();
|
||||
const auto id = m_property_id.get_value();
|
||||
const bool is_muted = discord.IsChannelMuted(id);
|
||||
|
||||
static const auto color = Gdk::RGBA(Abaddon::Get().GetSettings().ChannelColor);
|
||||
if (Abaddon::Get().GetDiscordClient().IsChannelMuted(m_property_id.get_value())) {
|
||||
auto muted = color;
|
||||
muted.set_red(muted.get_red() * 0.5);
|
||||
muted.set_green(muted.get_green() * 0.5);
|
||||
muted.set_blue(muted.get_blue() * 0.5);
|
||||
m_renderer_text.property_foreground_rgba() = muted;
|
||||
} else {
|
||||
m_renderer_text.property_foreground_rgba() = color;
|
||||
}
|
||||
m_renderer_text.render(cr, widget, background_area, text_cell_area, flags);
|
||||
m_renderer_text.property_foreground_set() = false;
|
||||
|
||||
// unread
|
||||
if (!Abaddon::Get().GetSettings().Unreads) return;
|
||||
|
||||
const auto unread_state = discord.GetUnreadStateForChannel(id);
|
||||
if (unread_state < 0) return;
|
||||
|
||||
if (!is_muted) {
|
||||
static const auto color = Gdk::RGBA(Abaddon::Get().GetSettings().UnreadIndicatorColor);
|
||||
cr->set_source_rgb(color.get_red(), color.get_green(), color.get_blue());
|
||||
const auto x = background_area.get_x();
|
||||
const auto y = background_area.get_y();
|
||||
const auto w = background_area.get_width();
|
||||
const auto h = background_area.get_height();
|
||||
cr->rectangle(x, y, 3, h);
|
||||
cr->fill();
|
||||
}
|
||||
|
||||
if (unread_state < 1) return;
|
||||
auto *paned = dynamic_cast<Gtk::Paned *>(widget.get_ancestor(Gtk::Paned::get_type()));
|
||||
if (paned != nullptr) {
|
||||
const auto edge = std::min(paned->get_position(), cell_area.get_width());
|
||||
|
||||
unread_render_mentions(cr, widget, unread_state, edge, cell_area);
|
||||
}
|
||||
}
|
||||
|
||||
// dm header
|
||||
|
||||
void CellRendererChannels::get_preferred_width_vfunc_dmheader(Gtk::Widget &widget, int &minimum_width, int &natural_width) const {
|
||||
m_renderer_text.get_preferred_width(widget, minimum_width, natural_width);
|
||||
}
|
||||
|
||||
void CellRendererChannels::get_preferred_width_for_height_vfunc_dmheader(Gtk::Widget &widget, int height, int &minimum_width, int &natural_width) const {
|
||||
m_renderer_text.get_preferred_width_for_height(widget, height, minimum_width, natural_width);
|
||||
}
|
||||
|
||||
void CellRendererChannels::get_preferred_height_vfunc_dmheader(Gtk::Widget &widget, int &minimum_height, int &natural_height) const {
|
||||
m_renderer_text.get_preferred_height(widget, minimum_height, natural_height);
|
||||
}
|
||||
|
||||
void CellRendererChannels::get_preferred_height_for_width_vfunc_dmheader(Gtk::Widget &widget, int width, int &minimum_height, int &natural_height) const {
|
||||
m_renderer_text.get_preferred_height_for_width(widget, width, minimum_height, natural_height);
|
||||
}
|
||||
|
||||
void CellRendererChannels::render_vfunc_dmheader(const Cairo::RefPtr<Cairo::Context> &cr, Gtk::Widget &widget, const Gdk::Rectangle &background_area, const Gdk::Rectangle &cell_area, Gtk::CellRendererState flags) {
|
||||
// gdk::rectangle more like gdk::stupid
|
||||
Gdk::Rectangle text_cell_area(
|
||||
cell_area.get_x() + 9, cell_area.get_y(), // maybe theres a better way to align this ?
|
||||
cell_area.get_width(), cell_area.get_height());
|
||||
m_renderer_text.render(cr, widget, background_area, text_cell_area, flags);
|
||||
|
||||
if (!Abaddon::Get().GetSettings().Unreads) return;
|
||||
|
||||
auto *paned = dynamic_cast<Gtk::Paned *>(widget.get_ancestor(Gtk::Paned::get_type()));
|
||||
if (paned != nullptr) {
|
||||
const auto edge = std::min(paned->get_position(), background_area.get_width());
|
||||
if (const auto unread = Abaddon::Get().GetDiscordClient().GetUnreadDMsCount(); unread > 0)
|
||||
unread_render_mentions(cr, widget, unread, edge, background_area);
|
||||
}
|
||||
}
|
||||
|
||||
// dm (basically the same thing as guild)
|
||||
|
||||
void CellRendererChannels::get_preferred_width_vfunc_dm(Gtk::Widget &widget, int &minimum_width, int &natural_width) const {
|
||||
int pixbuf_width = 0;
|
||||
if (auto pixbuf = m_property_pixbuf.get_value())
|
||||
pixbuf_width = pixbuf->get_width();
|
||||
|
||||
int text_min, text_nat;
|
||||
m_renderer_text.get_preferred_width(widget, text_min, text_nat);
|
||||
|
||||
int xpad, ypad;
|
||||
get_padding(xpad, ypad);
|
||||
minimum_width = std::max(text_min, pixbuf_width) + xpad * 2;
|
||||
natural_width = std::max(text_nat, pixbuf_width) + xpad * 2;
|
||||
}
|
||||
|
||||
void CellRendererChannels::get_preferred_width_for_height_vfunc_dm(Gtk::Widget &widget, int height, int &minimum_width, int &natural_width) const {
|
||||
get_preferred_width_vfunc_guild(widget, minimum_width, natural_width);
|
||||
}
|
||||
|
||||
void CellRendererChannels::get_preferred_height_vfunc_dm(Gtk::Widget &widget, int &minimum_height, int &natural_height) const {
|
||||
int pixbuf_height = 0;
|
||||
if (auto pixbuf = m_property_pixbuf.get_value())
|
||||
pixbuf_height = pixbuf->get_height();
|
||||
|
||||
int text_min, text_nat;
|
||||
m_renderer_text.get_preferred_height(widget, text_min, text_nat);
|
||||
|
||||
int xpad, ypad;
|
||||
get_padding(xpad, ypad);
|
||||
minimum_height = std::max(text_min, pixbuf_height) + ypad * 2;
|
||||
natural_height = std::max(text_nat, pixbuf_height) + ypad * 2;
|
||||
}
|
||||
|
||||
void CellRendererChannels::get_preferred_height_for_width_vfunc_dm(Gtk::Widget &widget, int width, int &minimum_height, int &natural_height) const {
|
||||
get_preferred_height_vfunc_guild(widget, minimum_height, natural_height);
|
||||
}
|
||||
|
||||
void CellRendererChannels::render_vfunc_dm(const Cairo::RefPtr<Cairo::Context> &cr, Gtk::Widget &widget, const Gdk::Rectangle &background_area, const Gdk::Rectangle &cell_area, Gtk::CellRendererState flags) {
|
||||
Gtk::Requisition text_minimum, text_natural;
|
||||
m_renderer_text.get_preferred_size(widget, text_minimum, text_natural);
|
||||
|
||||
Gtk::Requisition minimum, natural;
|
||||
get_preferred_size(widget, minimum, natural);
|
||||
|
||||
auto pixbuf = m_property_pixbuf.get_value();
|
||||
|
||||
const double icon_w = pixbuf->get_width();
|
||||
const double icon_h = pixbuf->get_height();
|
||||
const double icon_x = background_area.get_x() + 3;
|
||||
const double icon_y = background_area.get_y() + background_area.get_height() / 2.0 - icon_h / 2.0;
|
||||
|
||||
const double text_x = icon_x + icon_w + 6.0;
|
||||
const double text_y = background_area.get_y() + background_area.get_height() / 2.0 - text_natural.height / 2.0;
|
||||
const double text_w = text_natural.width;
|
||||
const double text_h = text_natural.height;
|
||||
|
||||
Gdk::Rectangle text_cell_area(static_cast<int>(text_x),
|
||||
static_cast<int>(text_y),
|
||||
static_cast<int>(text_w),
|
||||
static_cast<int>(text_h));
|
||||
|
||||
auto &discord = Abaddon::Get().GetDiscordClient();
|
||||
const auto id = m_property_id.get_value();
|
||||
const bool is_muted = discord.IsChannelMuted(id);
|
||||
|
||||
static const auto color = Gdk::RGBA(Abaddon::Get().GetSettings().ChannelColor);
|
||||
if (Abaddon::Get().GetDiscordClient().IsChannelMuted(m_property_id.get_value())) {
|
||||
auto muted = color;
|
||||
muted.set_red(muted.get_red() * 0.5);
|
||||
muted.set_green(muted.get_green() * 0.5);
|
||||
muted.set_blue(muted.get_blue() * 0.5);
|
||||
m_renderer_text.property_foreground_rgba() = muted;
|
||||
} else {
|
||||
m_renderer_text.property_foreground_rgba() = color;
|
||||
}
|
||||
m_renderer_text.render(cr, widget, background_area, text_cell_area, flags);
|
||||
m_renderer_text.property_foreground_set() = false;
|
||||
|
||||
Gdk::Cairo::set_source_pixbuf(cr, m_property_pixbuf.get_value(), icon_x, icon_y);
|
||||
cr->rectangle(icon_x, icon_y, icon_w, icon_h);
|
||||
cr->fill();
|
||||
|
||||
// unread
|
||||
if (!Abaddon::Get().GetSettings().Unreads) return;
|
||||
|
||||
const auto unread_state = discord.GetUnreadStateForChannel(id);
|
||||
if (unread_state < 0) return;
|
||||
|
||||
if (!is_muted) {
|
||||
static const auto color = Gdk::RGBA(Abaddon::Get().GetSettings().UnreadIndicatorColor);
|
||||
cr->set_source_rgb(color.get_red(), color.get_green(), color.get_blue());
|
||||
const auto x = background_area.get_x();
|
||||
const auto y = background_area.get_y();
|
||||
const auto w = background_area.get_width();
|
||||
const auto h = background_area.get_height();
|
||||
cr->rectangle(x, y, 3, h);
|
||||
cr->fill();
|
||||
}
|
||||
}
|
||||
|
||||
void CellRendererChannels::cairo_path_rounded_rect(const Cairo::RefPtr<Cairo::Context> &cr, double x, double y, double w, double h, double r) {
|
||||
const double degrees = M_PI / 180.0;
|
||||
|
||||
cr->begin_new_sub_path();
|
||||
cr->arc(x + w - r, y + r, r, -M_PI_H, 0);
|
||||
cr->arc(x + w - r, y + h - r, r, 0, M_PI_H);
|
||||
cr->arc(x + r, y + h - r, r, M_PI_H, M_PI);
|
||||
cr->arc(x + r, y + r, r, M_PI, M_PI_3_2);
|
||||
cr->close_path();
|
||||
}
|
||||
|
||||
void CellRendererChannels::unread_render_mentions(const Cairo::RefPtr<Cairo::Context> &cr, Gtk::Widget &widget, int mentions, int edge, const Gdk::Rectangle &cell_area) {
|
||||
Pango::FontDescription font;
|
||||
font.set_family("sans 14");
|
||||
// font.set_weight(Pango::WEIGHT_BOLD);
|
||||
|
||||
auto layout = widget.create_pango_layout(std::to_string(mentions));
|
||||
layout->set_font_description(font);
|
||||
layout->set_alignment(Pango::ALIGN_RIGHT);
|
||||
|
||||
int width, height;
|
||||
layout->get_pixel_size(width, height);
|
||||
{
|
||||
static const auto bg = Gdk::RGBA(Abaddon::Get().GetSettings().MentionBadgeColor);
|
||||
static const auto text = Gdk::RGBA(Abaddon::Get().GetSettings().MentionBadgeTextColor);
|
||||
|
||||
const auto x = cell_area.get_x() + edge - width - MentionsRightPad;
|
||||
const auto y = cell_area.get_y() + cell_area.get_height() / 2.0 - height / 2.0 - 1;
|
||||
cairo_path_rounded_rect(cr, x - 4, y + 2, width + 8, height, 5);
|
||||
cr->set_source_rgb(bg.get_red(), bg.get_green(), bg.get_blue());
|
||||
cr->fill();
|
||||
cr->set_source_rgb(text.get_red(), text.get_green(), text.get_blue());
|
||||
cr->move_to(x, y);
|
||||
layout->show_in_cairo_context(cr);
|
||||
}
|
||||
}
|
||||
126
src/components/channelscellrenderer.hpp
Normal file
126
src/components/channelscellrenderer.hpp
Normal file
@@ -0,0 +1,126 @@
|
||||
#pragma once
|
||||
#include <gtkmm/cellrenderertext.h>
|
||||
#include <gdkmm/pixbufanimation.h>
|
||||
#include <glibmm/property.h>
|
||||
#include <map>
|
||||
#include "discord/snowflake.hpp"
|
||||
|
||||
enum class RenderType : uint8_t {
|
||||
Guild,
|
||||
Category,
|
||||
TextChannel,
|
||||
Thread,
|
||||
|
||||
DMHeader,
|
||||
DM,
|
||||
};
|
||||
|
||||
class CellRendererChannels : public Gtk::CellRenderer {
|
||||
public:
|
||||
CellRendererChannels();
|
||||
~CellRendererChannels() override = default;
|
||||
|
||||
Glib::PropertyProxy<RenderType> property_type();
|
||||
Glib::PropertyProxy<uint64_t> property_id();
|
||||
Glib::PropertyProxy<Glib::ustring> property_name();
|
||||
Glib::PropertyProxy<Glib::RefPtr<Gdk::Pixbuf>> property_icon();
|
||||
Glib::PropertyProxy<Glib::RefPtr<Gdk::PixbufAnimation>> property_icon_animation();
|
||||
Glib::PropertyProxy<bool> property_expanded();
|
||||
Glib::PropertyProxy<bool> property_nsfw();
|
||||
|
||||
protected:
|
||||
void get_preferred_width_vfunc(Gtk::Widget &widget, int &minimum_width, int &natural_width) const override;
|
||||
void get_preferred_width_for_height_vfunc(Gtk::Widget &widget, int height, int &minimum_width, int &natural_width) const override;
|
||||
void get_preferred_height_vfunc(Gtk::Widget &widget, int &minimum_height, int &natural_height) const override;
|
||||
void get_preferred_height_for_width_vfunc(Gtk::Widget &widget, int width, int &minimum_height, int &natural_height) const override;
|
||||
void render_vfunc(const Cairo::RefPtr<Cairo::Context> &cr,
|
||||
Gtk::Widget &widget,
|
||||
const Gdk::Rectangle &background_area,
|
||||
const Gdk::Rectangle &cell_area,
|
||||
Gtk::CellRendererState flags) override;
|
||||
|
||||
// guild functions
|
||||
void get_preferred_width_vfunc_guild(Gtk::Widget &widget, int &minimum_width, int &natural_width) const;
|
||||
void get_preferred_width_for_height_vfunc_guild(Gtk::Widget &widget, int height, int &minimum_width, int &natural_width) const;
|
||||
void get_preferred_height_vfunc_guild(Gtk::Widget &widget, int &minimum_height, int &natural_height) const;
|
||||
void get_preferred_height_for_width_vfunc_guild(Gtk::Widget &widget, int width, int &minimum_height, int &natural_height) const;
|
||||
void render_vfunc_guild(const Cairo::RefPtr<Cairo::Context> &cr,
|
||||
Gtk::Widget &widget,
|
||||
const Gdk::Rectangle &background_area,
|
||||
const Gdk::Rectangle &cell_area,
|
||||
Gtk::CellRendererState flags);
|
||||
|
||||
// category
|
||||
void get_preferred_width_vfunc_category(Gtk::Widget &widget, int &minimum_width, int &natural_width) const;
|
||||
void get_preferred_width_for_height_vfunc_category(Gtk::Widget &widget, int height, int &minimum_width, int &natural_width) const;
|
||||
void get_preferred_height_vfunc_category(Gtk::Widget &widget, int &minimum_height, int &natural_height) const;
|
||||
void get_preferred_height_for_width_vfunc_category(Gtk::Widget &widget, int width, int &minimum_height, int &natural_height) const;
|
||||
void render_vfunc_category(const Cairo::RefPtr<Cairo::Context> &cr,
|
||||
Gtk::Widget &widget,
|
||||
const Gdk::Rectangle &background_area,
|
||||
const Gdk::Rectangle &cell_area,
|
||||
Gtk::CellRendererState flags);
|
||||
|
||||
// text channel
|
||||
void get_preferred_width_vfunc_channel(Gtk::Widget &widget, int &minimum_width, int &natural_width) const;
|
||||
void get_preferred_width_for_height_vfunc_channel(Gtk::Widget &widget, int height, int &minimum_width, int &natural_width) const;
|
||||
void get_preferred_height_vfunc_channel(Gtk::Widget &widget, int &minimum_height, int &natural_height) const;
|
||||
void get_preferred_height_for_width_vfunc_channel(Gtk::Widget &widget, int width, int &minimum_height, int &natural_height) const;
|
||||
void render_vfunc_channel(const Cairo::RefPtr<Cairo::Context> &cr,
|
||||
Gtk::Widget &widget,
|
||||
const Gdk::Rectangle &background_area,
|
||||
const Gdk::Rectangle &cell_area,
|
||||
Gtk::CellRendererState flags);
|
||||
|
||||
// thread
|
||||
void get_preferred_width_vfunc_thread(Gtk::Widget &widget, int &minimum_width, int &natural_width) const;
|
||||
void get_preferred_width_for_height_vfunc_thread(Gtk::Widget &widget, int height, int &minimum_width, int &natural_width) const;
|
||||
void get_preferred_height_vfunc_thread(Gtk::Widget &widget, int &minimum_height, int &natural_height) const;
|
||||
void get_preferred_height_for_width_vfunc_thread(Gtk::Widget &widget, int width, int &minimum_height, int &natural_height) const;
|
||||
void render_vfunc_thread(const Cairo::RefPtr<Cairo::Context> &cr,
|
||||
Gtk::Widget &widget,
|
||||
const Gdk::Rectangle &background_area,
|
||||
const Gdk::Rectangle &cell_area,
|
||||
Gtk::CellRendererState flags);
|
||||
|
||||
// dm header
|
||||
void get_preferred_width_vfunc_dmheader(Gtk::Widget &widget, int &minimum_width, int &natural_width) const;
|
||||
void get_preferred_width_for_height_vfunc_dmheader(Gtk::Widget &widget, int height, int &minimum_width, int &natural_width) const;
|
||||
void get_preferred_height_vfunc_dmheader(Gtk::Widget &widget, int &minimum_height, int &natural_height) const;
|
||||
void get_preferred_height_for_width_vfunc_dmheader(Gtk::Widget &widget, int width, int &minimum_height, int &natural_height) const;
|
||||
void render_vfunc_dmheader(const Cairo::RefPtr<Cairo::Context> &cr,
|
||||
Gtk::Widget &widget,
|
||||
const Gdk::Rectangle &background_area,
|
||||
const Gdk::Rectangle &cell_area,
|
||||
Gtk::CellRendererState flags);
|
||||
|
||||
// dm
|
||||
void get_preferred_width_vfunc_dm(Gtk::Widget &widget, int &minimum_width, int &natural_width) const;
|
||||
void get_preferred_width_for_height_vfunc_dm(Gtk::Widget &widget, int height, int &minimum_width, int &natural_width) const;
|
||||
void get_preferred_height_vfunc_dm(Gtk::Widget &widget, int &minimum_height, int &natural_height) const;
|
||||
void get_preferred_height_for_width_vfunc_dm(Gtk::Widget &widget, int width, int &minimum_height, int &natural_height) const;
|
||||
void render_vfunc_dm(const Cairo::RefPtr<Cairo::Context> &cr,
|
||||
Gtk::Widget &widget,
|
||||
const Gdk::Rectangle &background_area,
|
||||
const Gdk::Rectangle &cell_area,
|
||||
Gtk::CellRendererState flags);
|
||||
|
||||
static void cairo_path_rounded_rect(const Cairo::RefPtr<Cairo::Context> &cr, double x, double y, double w, double h, double r);
|
||||
static void unread_render_mentions(const Cairo::RefPtr<Cairo::Context> &cr, Gtk::Widget &widget, int mentions, int edge, const Gdk::Rectangle &cell_area);
|
||||
|
||||
private:
|
||||
Gtk::CellRendererText m_renderer_text;
|
||||
|
||||
Glib::Property<RenderType> m_property_type; // all
|
||||
Glib::Property<Glib::ustring> m_property_name; // all
|
||||
Glib::Property<uint64_t> m_property_id;
|
||||
Glib::Property<Glib::RefPtr<Gdk::Pixbuf>> m_property_pixbuf; // guild, dm
|
||||
Glib::Property<Glib::RefPtr<Gdk::PixbufAnimation>> m_property_pixbuf_animation; // guild
|
||||
Glib::Property<bool> m_property_expanded; // category
|
||||
Glib::Property<bool> m_property_nsfw; // channel
|
||||
|
||||
// same pitfalls as in https://github.com/uowuo/abaddon/blob/60404783bd4ce9be26233fe66fc3a74475d9eaa3/components/cellrendererpixbufanimation.hpp#L32-L39
|
||||
// this will manifest though since guild icons can change
|
||||
// an animation or two wont be the end of the world though
|
||||
std::map<Glib::RefPtr<Gdk::PixbufAnimation>, Glib::RefPtr<Gdk::PixbufAnimationIter>> m_pixbuf_anim_iters;
|
||||
};
|
||||
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();
|
||||
|
||||
@@ -25,16 +25,16 @@ ChatInputIndicator::ChatInputIndicator()
|
||||
if (!std::filesystem::exists(path)) return;
|
||||
auto gif_data = ReadWholeFile(path);
|
||||
auto loader = Gdk::PixbufLoader::create();
|
||||
loader->signal_size_prepared().connect([&](int inw, int inh) {
|
||||
int w, h;
|
||||
GetImageDimensions(inw, inh, w, h, 20, 10);
|
||||
loader->set_size(w, h);
|
||||
});
|
||||
loader->write(gif_data.data(), gif_data.size());
|
||||
try {
|
||||
loader->signal_size_prepared().connect([&](int inw, int inh) {
|
||||
int w, h;
|
||||
GetImageDimensions(inw, inh, w, h, 20, 10);
|
||||
loader->set_size(w, h);
|
||||
});
|
||||
loader->write(gif_data.data(), gif_data.size());
|
||||
loader->close();
|
||||
m_img.property_pixbuf_animation() = loader->get_animation();
|
||||
} catch (const std::exception &) {}
|
||||
} catch (...) {}
|
||||
}
|
||||
|
||||
void ChatInputIndicator::AddUser(Snowflake channel_id, const UserData &user, int timeout) {
|
||||
@@ -84,14 +84,14 @@ void ChatInputIndicator::OnMessageCreate(const Message &message) {
|
||||
|
||||
void ChatInputIndicator::SetTypingString(const Glib::ustring &str) {
|
||||
m_label.set_text(str);
|
||||
if (str == "")
|
||||
if (str.empty())
|
||||
m_img.hide();
|
||||
else if (m_img.property_pixbuf_animation().get_value())
|
||||
m_img.show();
|
||||
}
|
||||
|
||||
void ChatInputIndicator::ComputeTypingString() {
|
||||
if (m_custom_markup != "") {
|
||||
if (!m_custom_markup.empty()) {
|
||||
m_label.set_markup(m_custom_markup);
|
||||
m_img.hide();
|
||||
return;
|
||||
@@ -104,7 +104,7 @@ void ChatInputIndicator::ComputeTypingString() {
|
||||
if (user.has_value())
|
||||
typers.push_back(*user);
|
||||
}
|
||||
if (typers.size() == 0) {
|
||||
if (typers.empty()) {
|
||||
SetTypingString("");
|
||||
} else if (typers.size() == 1) {
|
||||
SetTypingString(typers[0].Username + " is typing...");
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#include "chatmessage.hpp"
|
||||
#include "chatlist.hpp"
|
||||
#include "abaddon.hpp"
|
||||
#include "chatmessage.hpp"
|
||||
#include "constants.hpp"
|
||||
|
||||
ChatList::ChatList() {
|
||||
@@ -8,17 +8,11 @@ ChatList::ChatList() {
|
||||
|
||||
set_can_focus(false);
|
||||
set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_ALWAYS);
|
||||
signal_edge_reached().connect(sigc::mem_fun(*this, &ChatList::OnScrollEdgeOvershot));
|
||||
|
||||
auto v = get_vadjustment();
|
||||
v->signal_value_changed().connect([this, v] {
|
||||
m_should_scroll_to_bottom = v->get_upper() - v->get_page_size() <= v->get_value();
|
||||
});
|
||||
get_vadjustment()->signal_value_changed().connect(sigc::mem_fun(*this, &ChatList::OnVAdjustmentValueChanged));
|
||||
get_vadjustment()->property_upper().signal_changed().connect(sigc::mem_fun(*this, &ChatList::OnVAdjustmentUpperChanged));
|
||||
|
||||
m_list.signal_size_allocate().connect([this](Gtk::Allocation &) {
|
||||
if (m_should_scroll_to_bottom)
|
||||
ScrollToBottom();
|
||||
});
|
||||
m_list.signal_size_allocate().connect(sigc::mem_fun(*this, &ChatList::OnListSizeAllocate));
|
||||
|
||||
m_list.set_focus_hadjustment(get_hadjustment());
|
||||
m_list.set_focus_vadjustment(get_vadjustment());
|
||||
@@ -30,56 +24,7 @@ ChatList::ChatList() {
|
||||
|
||||
m_list.show();
|
||||
|
||||
m_menu_copy_id = Gtk::manage(new Gtk::MenuItem("Copy ID"));
|
||||
m_menu_copy_id->signal_activate().connect([this] {
|
||||
Gtk::Clipboard::get()->set_text(std::to_string(m_menu_selected_message));
|
||||
});
|
||||
m_menu_copy_id->show();
|
||||
m_menu.append(*m_menu_copy_id);
|
||||
|
||||
m_menu_delete_message = Gtk::manage(new Gtk::MenuItem("Delete Message"));
|
||||
m_menu_delete_message->signal_activate().connect([this] {
|
||||
Abaddon::Get().GetDiscordClient().DeleteMessage(m_active_channel, m_menu_selected_message);
|
||||
});
|
||||
m_menu_delete_message->show();
|
||||
m_menu.append(*m_menu_delete_message);
|
||||
|
||||
m_menu_edit_message = Gtk::manage(new Gtk::MenuItem("Edit Message"));
|
||||
m_menu_edit_message->signal_activate().connect([this] {
|
||||
m_signal_action_message_edit.emit(m_active_channel, m_menu_selected_message);
|
||||
});
|
||||
m_menu_edit_message->show();
|
||||
m_menu.append(*m_menu_edit_message);
|
||||
|
||||
m_menu_copy_content = Gtk::manage(new Gtk::MenuItem("Copy Content"));
|
||||
m_menu_copy_content->signal_activate().connect([this] {
|
||||
const auto msg = Abaddon::Get().GetDiscordClient().GetMessage(m_menu_selected_message);
|
||||
if (msg.has_value())
|
||||
Gtk::Clipboard::get()->set_text(msg->Content);
|
||||
});
|
||||
m_menu_copy_content->show();
|
||||
m_menu.append(*m_menu_copy_content);
|
||||
|
||||
m_menu_reply_to = Gtk::manage(new Gtk::MenuItem("Reply To"));
|
||||
m_menu_reply_to->signal_activate().connect([this] {
|
||||
m_signal_action_reply_to.emit(m_menu_selected_message);
|
||||
});
|
||||
m_menu_reply_to->show();
|
||||
m_menu.append(*m_menu_reply_to);
|
||||
|
||||
m_menu_unpin = Gtk::manage(new Gtk::MenuItem("Unpin"));
|
||||
m_menu_unpin->signal_activate().connect([this] {
|
||||
Abaddon::Get().GetDiscordClient().Unpin(m_active_channel, m_menu_selected_message, [](...) {});
|
||||
});
|
||||
m_menu.append(*m_menu_unpin);
|
||||
|
||||
m_menu_pin = Gtk::manage(new Gtk::MenuItem("Pin"));
|
||||
m_menu_pin->signal_activate().connect([this] {
|
||||
Abaddon::Get().GetDiscordClient().Pin(m_active_channel, m_menu_selected_message, [](...) {});
|
||||
});
|
||||
m_menu.append(*m_menu_pin);
|
||||
|
||||
m_menu.show();
|
||||
SetupMenu();
|
||||
}
|
||||
|
||||
void ChatList::Clear() {
|
||||
@@ -89,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) {
|
||||
@@ -98,6 +46,7 @@ void ChatList::SetActiveChannel(Snowflake id) {
|
||||
void ChatList::ProcessNewMessage(const Message &data, bool prepend) {
|
||||
auto &discord = Abaddon::Get().GetDiscordClient();
|
||||
if (!discord.IsStarted()) return;
|
||||
if (!prepend) m_ignore_next_upper = true;
|
||||
|
||||
// delete preview message when gateway sends it back
|
||||
if (!data.IsPending && data.Nonce.has_value() && data.Author.ID == discord.GetUserData().ID) {
|
||||
@@ -243,7 +192,7 @@ void ChatList::RefetchMessage(Snowflake id) {
|
||||
}
|
||||
|
||||
Snowflake ChatList::GetOldestListedMessage() {
|
||||
if (m_id_to_widget.size() > 0)
|
||||
if (!m_id_to_widget.empty())
|
||||
return m_id_to_widget.begin()->first;
|
||||
else
|
||||
return Snowflake::Invalid;
|
||||
@@ -306,14 +255,85 @@ void ChatList::ActuallyRemoveMessage(Snowflake id) {
|
||||
RemoveMessageAndHeader(it->second);
|
||||
}
|
||||
|
||||
void ChatList::OnScrollEdgeOvershot(Gtk::PositionType pos) {
|
||||
if (pos == Gtk::POS_TOP)
|
||||
m_signal_action_chat_load_history.emit(m_active_channel);
|
||||
void ChatList::SetupMenu() {
|
||||
m_menu_copy_id = Gtk::manage(new Gtk::MenuItem("Copy ID"));
|
||||
m_menu_copy_id->signal_activate().connect([this] {
|
||||
Gtk::Clipboard::get()->set_text(std::to_string(m_menu_selected_message));
|
||||
});
|
||||
m_menu_copy_id->show();
|
||||
m_menu.append(*m_menu_copy_id);
|
||||
|
||||
m_menu_delete_message = Gtk::manage(new Gtk::MenuItem("Delete Message"));
|
||||
m_menu_delete_message->signal_activate().connect([this] {
|
||||
Abaddon::Get().GetDiscordClient().DeleteMessage(m_active_channel, m_menu_selected_message);
|
||||
});
|
||||
m_menu_delete_message->show();
|
||||
m_menu.append(*m_menu_delete_message);
|
||||
|
||||
m_menu_edit_message = Gtk::manage(new Gtk::MenuItem("Edit Message"));
|
||||
m_menu_edit_message->signal_activate().connect([this] {
|
||||
m_signal_action_message_edit.emit(m_active_channel, m_menu_selected_message);
|
||||
});
|
||||
m_menu_edit_message->show();
|
||||
m_menu.append(*m_menu_edit_message);
|
||||
|
||||
m_menu_copy_content = Gtk::manage(new Gtk::MenuItem("Copy Content"));
|
||||
m_menu_copy_content->signal_activate().connect([this] {
|
||||
const auto msg = Abaddon::Get().GetDiscordClient().GetMessage(m_menu_selected_message);
|
||||
if (msg.has_value())
|
||||
Gtk::Clipboard::get()->set_text(msg->Content);
|
||||
});
|
||||
m_menu_copy_content->show();
|
||||
m_menu.append(*m_menu_copy_content);
|
||||
|
||||
m_menu_reply_to = Gtk::manage(new Gtk::MenuItem("Reply To"));
|
||||
m_menu_reply_to->signal_activate().connect([this] {
|
||||
m_signal_action_reply_to.emit(m_menu_selected_message);
|
||||
});
|
||||
m_menu_reply_to->show();
|
||||
m_menu.append(*m_menu_reply_to);
|
||||
|
||||
m_menu_unpin = Gtk::manage(new Gtk::MenuItem("Unpin"));
|
||||
m_menu_unpin->signal_activate().connect([this] {
|
||||
Abaddon::Get().GetDiscordClient().Unpin(m_active_channel, m_menu_selected_message, [](...) {});
|
||||
});
|
||||
m_menu.append(*m_menu_unpin);
|
||||
|
||||
m_menu_pin = Gtk::manage(new Gtk::MenuItem("Pin"));
|
||||
m_menu_pin->signal_activate().connect([this] {
|
||||
Abaddon::Get().GetDiscordClient().Pin(m_active_channel, m_menu_selected_message, [](...) {});
|
||||
});
|
||||
m_menu.append(*m_menu_pin);
|
||||
|
||||
m_menu.show();
|
||||
}
|
||||
|
||||
void ChatList::ScrollToBottom() {
|
||||
auto x = get_vadjustment();
|
||||
x->set_value(x->get_upper());
|
||||
auto v = get_vadjustment();
|
||||
v->set_value(v->get_upper());
|
||||
}
|
||||
|
||||
void ChatList::OnVAdjustmentValueChanged() {
|
||||
auto v = get_vadjustment();
|
||||
if (m_history_timer.elapsed() > 1 && v->get_value() < 500) {
|
||||
m_history_timer.start();
|
||||
m_signal_action_chat_load_history.emit(m_active_channel);
|
||||
}
|
||||
m_should_scroll_to_bottom = v->get_upper() - v->get_page_size() <= v->get_value();
|
||||
}
|
||||
|
||||
void ChatList::OnVAdjustmentUpperChanged() {
|
||||
auto v = get_vadjustment();
|
||||
if (!m_ignore_next_upper && !m_should_scroll_to_bottom && m_old_upper > -1.0) {
|
||||
const auto inc = v->get_upper() - m_old_upper;
|
||||
v->set_value(v->get_value() + inc);
|
||||
}
|
||||
m_ignore_next_upper = false;
|
||||
m_old_upper = v->get_upper();
|
||||
}
|
||||
|
||||
void ChatList::OnListSizeAllocate(Gtk::Allocation &allocation) {
|
||||
if (m_should_scroll_to_bottom) ScrollToBottom();
|
||||
}
|
||||
|
||||
void ChatList::RemoveMessageAndHeader(Gtk::Widget *widget) {
|
||||
@@ -335,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;
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
#include <gtkmm.h>
|
||||
#include <map>
|
||||
#include <vector>
|
||||
#include "discord/message.hpp"
|
||||
#include "discord/snowflake.hpp"
|
||||
|
||||
class ChatList : public Gtk::ScrolledWindow {
|
||||
@@ -25,8 +26,11 @@ public:
|
||||
void ActuallyRemoveMessage(Snowflake id); // perhaps not the best method name
|
||||
|
||||
private:
|
||||
void OnScrollEdgeOvershot(Gtk::PositionType pos);
|
||||
void SetupMenu();
|
||||
void ScrollToBottom();
|
||||
void OnVAdjustmentValueChanged();
|
||||
void OnVAdjustmentUpperChanged();
|
||||
void OnListSizeAllocate(Gtk::Allocation &allocation);
|
||||
void RemoveMessageAndHeader(Gtk::Widget *widget);
|
||||
|
||||
bool m_use_pinned_menu = false;
|
||||
@@ -47,15 +51,18 @@ private:
|
||||
int m_num_rows = 0;
|
||||
std::map<Snowflake, Gtk::Widget *> m_id_to_widget;
|
||||
|
||||
bool m_ignore_next_upper = false;
|
||||
double m_old_upper = -1.0;
|
||||
bool m_should_scroll_to_bottom = true;
|
||||
Gtk::ListBox m_list;
|
||||
|
||||
bool m_separate_all = false;
|
||||
|
||||
Glib::Timer m_history_timer;
|
||||
|
||||
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>;
|
||||
@@ -65,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();
|
||||
@@ -76,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;
|
||||
@@ -101,15 +106,6 @@ inline void ChatList::SetMessages(Iter begin, Iter end) {
|
||||
|
||||
template<typename Iter>
|
||||
inline void ChatList::PrependMessages(Iter begin, Iter end) {
|
||||
const auto old_upper = get_vadjustment()->get_upper();
|
||||
const auto old_value = get_vadjustment()->get_value();
|
||||
for (Iter it = begin; it != end; it++)
|
||||
ProcessNewMessage(*it, true);
|
||||
// force everything to process before getting new values
|
||||
while (Gtk::Main::events_pending())
|
||||
Gtk::Main::iteration();
|
||||
const auto new_upper = get_vadjustment()->get_upper();
|
||||
if (old_value == 0.0 && (new_upper - old_upper) > 0.0)
|
||||
get_vadjustment()->set_value(new_upper - old_upper);
|
||||
// this isn't ideal
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#include "chatmessage.hpp"
|
||||
#include "abaddon.hpp"
|
||||
#include "util.hpp"
|
||||
#include "chatmessage.hpp"
|
||||
#include "lazyimage.hpp"
|
||||
#include "util.hpp"
|
||||
#include <unordered_map>
|
||||
|
||||
constexpr static int EmojiSize = 24; // settings eventually
|
||||
@@ -30,9 +30,8 @@ ChatMessageItemContainer *ChatMessageItemContainer::FromMessage(const Message &d
|
||||
if (data.Nonce.has_value())
|
||||
container->Nonce = *data.Nonce;
|
||||
|
||||
if (data.Content.size() > 0 || data.Type != MessageType::DEFAULT) {
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -44,18 +43,9 @@ ChatMessageItemContainer *ChatMessageItemContainer::FromMessage(const Message &d
|
||||
}
|
||||
}
|
||||
|
||||
// there should only ever be 1 embed (i think?)
|
||||
if (data.Embeds.size() == 1) {
|
||||
const auto &embed = data.Embeds[0];
|
||||
if (IsEmbedImageOnly(embed)) {
|
||||
auto *widget = container->CreateImageComponent(*embed.Thumbnail->ProxyURL, *embed.Thumbnail->URL, *embed.Thumbnail->Width, *embed.Thumbnail->Height);
|
||||
container->AttachEventHandlers(*widget);
|
||||
container->m_main.add(*widget);
|
||||
} else {
|
||||
container->m_embed_component = container->CreateEmbedComponent(embed);
|
||||
container->AttachEventHandlers(*container->m_embed_component);
|
||||
container->m_main.add(*container->m_embed_component);
|
||||
}
|
||||
if (!data.Embeds.empty()) {
|
||||
container->m_embed_component = container->CreateEmbedsComponent(data.Embeds);
|
||||
container->m_main.add(*container->m_embed_component);
|
||||
}
|
||||
|
||||
// i dont think attachments can be edited
|
||||
@@ -87,7 +77,7 @@ ChatMessageItemContainer *ChatMessageItemContainer::FromMessage(const Message &d
|
||||
container->m_main.add(*widget);
|
||||
}
|
||||
|
||||
if (data.Reactions.has_value() && data.Reactions->size() > 0) {
|
||||
if (data.Reactions.has_value() && !data.Reactions->empty()) {
|
||||
container->m_reactions_component = container->CreateReactionsComponent(data);
|
||||
container->m_main.add(*container->m_reactions_component);
|
||||
}
|
||||
@@ -108,10 +98,10 @@ void ChatMessageItemContainer::UpdateContent() {
|
||||
m_embed_component = nullptr;
|
||||
}
|
||||
|
||||
if (data->Embeds.size() == 1) {
|
||||
m_embed_component = CreateEmbedComponent(data->Embeds[0]);
|
||||
AttachEventHandlers(*m_embed_component);
|
||||
if (!data->Embeds.empty()) {
|
||||
m_embed_component = CreateEmbedsComponent(data->Embeds);
|
||||
m_main.add(*m_embed_component);
|
||||
m_embed_component->show_all();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -122,7 +112,7 @@ void ChatMessageItemContainer::UpdateReactions() {
|
||||
}
|
||||
|
||||
const auto data = Abaddon::Get().GetDiscordClient().GetMessage(ID);
|
||||
if (data->Reactions.has_value() && data->Reactions->size() > 0) {
|
||||
if (data->Reactions.has_value() && !data->Reactions->empty()) {
|
||||
m_reactions_component = CreateReactionsComponent(*data);
|
||||
m_reactions_component->show_all();
|
||||
m_main.add(*m_reactions_component);
|
||||
@@ -158,14 +148,14 @@ void ChatMessageItemContainer::UpdateAttributes() {
|
||||
m_attrib_label->set_markup("<span color='#999999'>[edited]</span>");
|
||||
}
|
||||
|
||||
void ChatMessageItemContainer::AddClickHandler(Gtk::Widget *widget, std::string url) {
|
||||
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
|
||||
}
|
||||
@@ -182,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;
|
||||
@@ -199,6 +191,7 @@ void ChatMessageItemContainer::UpdateTextComponent(Gtk::TextView *tv) {
|
||||
case MessageType::DEFAULT:
|
||||
case MessageType::INLINE_REPLY:
|
||||
b->insert(s, data->Content);
|
||||
HandleRoleMentions(b);
|
||||
HandleUserMentions(b);
|
||||
HandleLinks(*tv);
|
||||
HandleChannelMentions(tv);
|
||||
@@ -231,13 +224,13 @@ void ChatMessageItemContainer::UpdateTextComponent(Gtk::TextView *tv) {
|
||||
}
|
||||
} break;
|
||||
case MessageType::RECIPIENT_ADD: {
|
||||
if (data->Mentions.size() == 0) break;
|
||||
if (data->Mentions.empty()) break;
|
||||
const auto &adder = Abaddon::Get().GetDiscordClient().GetUser(data->Author.ID);
|
||||
const auto &added = data->Mentions[0];
|
||||
b->insert_markup(s, "<i><span color='#999999'><span color='#eeeeee'>" + adder->Username + "</span> added <span color='#eeeeee'>" + added.Username + "</span></span></i>");
|
||||
} break;
|
||||
case MessageType::RECIPIENT_REMOVE: {
|
||||
if (data->Mentions.size() == 0) break;
|
||||
if (data->Mentions.empty()) break;
|
||||
const auto &adder = Abaddon::Get().GetDiscordClient().GetUser(data->Author.ID);
|
||||
const auto &added = data->Mentions[0];
|
||||
if (adder->ID == added.ID)
|
||||
@@ -288,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>");
|
||||
}
|
||||
@@ -298,6 +289,22 @@ void ChatMessageItemContainer::UpdateTextComponent(Gtk::TextView *tv) {
|
||||
}
|
||||
}
|
||||
|
||||
Gtk::Widget *ChatMessageItemContainer::CreateEmbedsComponent(const std::vector<EmbedData> &embeds) {
|
||||
auto *box = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_VERTICAL));
|
||||
for (const auto &embed : embeds) {
|
||||
if (IsEmbedImageOnly(embed)) {
|
||||
auto *widget = CreateImageComponent(*embed.Thumbnail->ProxyURL, *embed.Thumbnail->URL, *embed.Thumbnail->Width, *embed.Thumbnail->Height);
|
||||
widget->show();
|
||||
box->add(*widget);
|
||||
} else {
|
||||
auto *widget = CreateEmbedComponent(embed);
|
||||
widget->show();
|
||||
box->add(*widget);
|
||||
}
|
||||
}
|
||||
return box;
|
||||
}
|
||||
|
||||
Gtk::Widget *ChatMessageItemContainer::CreateEmbedComponent(const EmbedData &embed) {
|
||||
Gtk::EventBox *ev = Gtk::manage(new Gtk::EventBox);
|
||||
ev->set_can_focus(true);
|
||||
@@ -350,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([this, 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;
|
||||
}
|
||||
@@ -378,7 +385,7 @@ Gtk::Widget *ChatMessageItemContainer::CreateEmbedComponent(const EmbedData &emb
|
||||
}
|
||||
|
||||
// todo: handle inline fields
|
||||
if (embed.Fields.has_value() && embed.Fields->size() > 0) {
|
||||
if (embed.Fields.has_value() && !embed.Fields->empty()) {
|
||||
auto *flow = Gtk::manage(new Gtk::FlowBox);
|
||||
flow->set_orientation(Gtk::ORIENTATION_HORIZONTAL);
|
||||
flow->set_min_children_per_line(3);
|
||||
@@ -482,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;
|
||||
}
|
||||
|
||||
@@ -499,29 +516,21 @@ 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;
|
||||
}
|
||||
|
||||
Gtk::Widget *ChatMessageItemContainer::CreateStickerComponentDeprecated(const StickerData &data) {
|
||||
auto *box = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL));
|
||||
auto *imgw = Gtk::manage(new Gtk::Image);
|
||||
box->add(*imgw);
|
||||
auto &img = Abaddon::Get().GetImageManager();
|
||||
|
||||
if (data.FormatType == StickerFormatType::PNG || data.FormatType == StickerFormatType::APNG) {
|
||||
auto cb = [this, imgw](const Glib::RefPtr<Gdk::Pixbuf> &pixbuf) {
|
||||
imgw->property_pixbuf() = pixbuf;
|
||||
};
|
||||
img.LoadFromURL(data.GetURL(), sigc::track_obj(cb, *imgw));
|
||||
}
|
||||
|
||||
AttachEventHandlers(*box);
|
||||
return box;
|
||||
}
|
||||
|
||||
Gtk::Widget *ChatMessageItemContainer::CreateStickersComponent(const std::vector<StickerItem> &data) {
|
||||
auto *box = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_VERTICAL));
|
||||
|
||||
@@ -530,6 +539,7 @@ Gtk::Widget *ChatMessageItemContainer::CreateStickersComponent(const std::vector
|
||||
if (sticker.FormatType != StickerFormatType::PNG && sticker.FormatType != StickerFormatType::APNG) continue;
|
||||
auto *ev = Gtk::manage(new Gtk::EventBox);
|
||||
auto *img = Gtk::manage(new LazyImage(sticker.GetURL(), StickerComponentSize, StickerComponentSize, false));
|
||||
img->set_halign(Gtk::ALIGN_START);
|
||||
img->set_size_request(StickerComponentSize, StickerComponentSize); // should this go in LazyImage ?
|
||||
img->show();
|
||||
ev->show();
|
||||
@@ -539,7 +549,6 @@ Gtk::Widget *ChatMessageItemContainer::CreateStickersComponent(const std::vector
|
||||
|
||||
box->show();
|
||||
|
||||
AttachEventHandlers(*box);
|
||||
return box;
|
||||
}
|
||||
|
||||
@@ -596,7 +605,7 @@ Gtk::Widget *ChatMessageItemContainer::CreateReactionsComponent(const Message &d
|
||||
// image
|
||||
if (is_stock) { // unicode/stock
|
||||
const auto shortcode = emojis.GetShortCodeForPattern(reaction.Emoji.Name);
|
||||
if (shortcode != "")
|
||||
if (!shortcode.empty())
|
||||
ev->set_tooltip_text(shortcode);
|
||||
|
||||
const auto &pb = emojis.GetPixBuf(reaction.Emoji.Name);
|
||||
@@ -646,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
|
||||
@@ -664,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) {
|
||||
@@ -732,7 +747,47 @@ bool ChatMessageItemContainer::IsEmbedImageOnly(const EmbedData &data) {
|
||||
return data.Thumbnail->ProxyURL.has_value() && data.Thumbnail->URL.has_value() && data.Thumbnail->Width.has_value() && data.Thumbnail->Height.has_value();
|
||||
}
|
||||
|
||||
void ChatMessageItemContainer::HandleUserMentions(Glib::RefPtr<Gtk::TextBuffer> buf) {
|
||||
void ChatMessageItemContainer::HandleRoleMentions(const Glib::RefPtr<Gtk::TextBuffer> &buf) {
|
||||
constexpr static const auto mentions_regex = R"(<@&(\d+)>)";
|
||||
|
||||
static auto rgx = Glib::Regex::create(mentions_regex);
|
||||
|
||||
Glib::ustring text = GetText(buf);
|
||||
const auto &discord = Abaddon::Get().GetDiscordClient();
|
||||
|
||||
int startpos = 0;
|
||||
Glib::MatchInfo match;
|
||||
while (rgx->match(text, startpos, match)) {
|
||||
int mstart, mend;
|
||||
if (!match.fetch_pos(0, mstart, mend)) break;
|
||||
const Glib::ustring role_id = match.fetch(1);
|
||||
const auto role = discord.GetRole(role_id);
|
||||
if (!role.has_value()) {
|
||||
startpos = mend;
|
||||
continue;
|
||||
}
|
||||
|
||||
Glib::ustring replacement;
|
||||
if (role->HasColor()) {
|
||||
replacement = "<b><span color=\"#" + IntToCSSColor(role->Color) + "\">@" + role->GetEscapedName() + "</span></b>";
|
||||
} else {
|
||||
replacement = "<b>@" + role->GetEscapedName() + "</b>";
|
||||
}
|
||||
|
||||
const auto chars_start = g_utf8_pointer_to_offset(text.c_str(), text.c_str() + mstart);
|
||||
const auto chars_end = g_utf8_pointer_to_offset(text.c_str(), text.c_str() + mend);
|
||||
const auto start_it = buf->get_iter_at_offset(chars_start);
|
||||
const auto end_it = buf->get_iter_at_offset(chars_end);
|
||||
|
||||
auto it = buf->erase(start_it, end_it);
|
||||
buf->insert_markup(it, replacement);
|
||||
|
||||
text = GetText(buf);
|
||||
startpos = 0;
|
||||
}
|
||||
}
|
||||
|
||||
void ChatMessageItemContainer::HandleUserMentions(const Glib::RefPtr<Gtk::TextBuffer> &buf) const {
|
||||
constexpr static const auto mentions_regex = R"(<@!?(\d+)>)";
|
||||
|
||||
static auto rgx = Glib::Regex::create(mentions_regex);
|
||||
@@ -809,7 +864,7 @@ void ChatMessageItemContainer::HandleCustomEmojis(Gtk::TextView &tv) {
|
||||
const auto mark_start = buf->create_mark(start_it, false);
|
||||
end_it.backward_char();
|
||||
const auto mark_end = buf->create_mark(end_it, false);
|
||||
const auto cb = [this, &tv, buf, mark_start, mark_end](const Glib::RefPtr<Gdk::PixbufAnimation> &pixbuf) {
|
||||
const auto cb = [&tv, buf, mark_start, mark_end](const Glib::RefPtr<Gdk::PixbufAnimation> &pixbuf) {
|
||||
auto start_it = mark_start->get_iter();
|
||||
auto end_it = mark_end->get_iter();
|
||||
end_it.forward_char();
|
||||
@@ -827,14 +882,16 @@ void ChatMessageItemContainer::HandleCustomEmojis(Gtk::TextView &tv) {
|
||||
const auto mark_start = buf->create_mark(start_it, false);
|
||||
end_it.backward_char();
|
||||
const auto mark_end = buf->create_mark(end_it, false);
|
||||
const auto cb = [this, buf, mark_start, mark_end](const Glib::RefPtr<Gdk::Pixbuf> &pixbuf) {
|
||||
const auto cb = [buf, mark_start, mark_end](const Glib::RefPtr<Gdk::Pixbuf> &pixbuf) {
|
||||
auto start_it = mark_start->get_iter();
|
||||
auto end_it = mark_end->get_iter();
|
||||
end_it.forward_char();
|
||||
buf->delete_mark(mark_start);
|
||||
buf->delete_mark(mark_end);
|
||||
auto it = buf->erase(start_it, end_it);
|
||||
buf->insert_pixbuf(it, pixbuf->scale_simple(EmojiSize, EmojiSize, Gdk::INTERP_BILINEAR));
|
||||
int width, height;
|
||||
GetImageDimensions(pixbuf->get_width(), pixbuf->get_height(), width, height, EmojiSize, EmojiSize);
|
||||
buf->insert_pixbuf(it, pixbuf->scale_simple(width, height, Gdk::INTERP_BILINEAR));
|
||||
};
|
||||
img.LoadFromURL(EmojiData::URLFromID(match.fetch(2)), sigc::track_obj(cb, tv));
|
||||
}
|
||||
@@ -848,7 +905,7 @@ void ChatMessageItemContainer::HandleEmojis(Gtk::TextView &tv) {
|
||||
if (Abaddon::Get().GetSettings().ShowCustomEmojis) HandleCustomEmojis(tv);
|
||||
}
|
||||
|
||||
void ChatMessageItemContainer::CleanupEmojis(Glib::RefPtr<Gtk::TextBuffer> buf) {
|
||||
void ChatMessageItemContainer::CleanupEmojis(const Glib::RefPtr<Gtk::TextBuffer> &buf) {
|
||||
static auto rgx = Glib::Regex::create(R"(<a?:([\w\d_]+):(\d+)>)");
|
||||
|
||||
auto text = GetText(buf);
|
||||
@@ -868,9 +925,9 @@ void ChatMessageItemContainer::CleanupEmojis(Glib::RefPtr<Gtk::TextBuffer> buf)
|
||||
|
||||
startpos = mend;
|
||||
const auto it = buf->erase(start_it, end_it);
|
||||
const int alen = text.size();
|
||||
const int alen = static_cast<int>(text.size());
|
||||
text = GetText(buf);
|
||||
const int blen = text.size();
|
||||
const int blen = static_cast<int>(text.size());
|
||||
startpos -= (alen - blen);
|
||||
|
||||
buf->insert(it, new_term);
|
||||
@@ -879,7 +936,7 @@ void ChatMessageItemContainer::CleanupEmojis(Glib::RefPtr<Gtk::TextBuffer> buf)
|
||||
}
|
||||
}
|
||||
|
||||
void ChatMessageItemContainer::HandleChannelMentions(Glib::RefPtr<Gtk::TextBuffer> buf) {
|
||||
void ChatMessageItemContainer::HandleChannelMentions(const Glib::RefPtr<Gtk::TextBuffer> &buf) {
|
||||
static auto rgx = Glib::Regex::create(R"(<#(\d+)>)");
|
||||
|
||||
Glib::ustring text = GetText(buf);
|
||||
@@ -919,7 +976,6 @@ void ChatMessageItemContainer::HandleChannelMentions(Glib::RefPtr<Gtk::TextBuffe
|
||||
}
|
||||
|
||||
void ChatMessageItemContainer::HandleChannelMentions(Gtk::TextView *tv) {
|
||||
tv->signal_button_press_event().connect(sigc::mem_fun(*this, &ChatMessageItemContainer::OnClickChannel), false);
|
||||
HandleChannelMentions(tv->get_buffer());
|
||||
}
|
||||
|
||||
@@ -936,12 +992,12 @@ bool ChatMessageItemContainer::OnClickChannel(GdkEventButton *ev) {
|
||||
return false;
|
||||
|
||||
int x, y;
|
||||
m_text_component->window_to_buffer_coords(Gtk::TEXT_WINDOW_WIDGET, ev->x, ev->y, x, y);
|
||||
m_text_component->window_to_buffer_coords(Gtk::TEXT_WINDOW_WIDGET, static_cast<int>(ev->x), static_cast<int>(ev->y), x, y);
|
||||
Gtk::TextBuffer::iterator iter;
|
||||
m_text_component->get_iter_at_location(iter, x, y);
|
||||
|
||||
const auto tags = iter.get_tags();
|
||||
for (auto tag : tags) {
|
||||
for (const auto &tag : tags) {
|
||||
const auto it = m_channel_tagmap.find(tag);
|
||||
if (it != m_channel_tagmap.end()) {
|
||||
m_signal_action_channel_click.emit(it->second);
|
||||
@@ -953,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);
|
||||
}
|
||||
@@ -960,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);
|
||||
|
||||
@@ -999,12 +1067,12 @@ bool ChatMessageItemContainer::OnLinkClick(GdkEventButton *ev) {
|
||||
return false;
|
||||
|
||||
int x, y;
|
||||
m_text_component->window_to_buffer_coords(Gtk::TEXT_WINDOW_WIDGET, ev->x, ev->y, x, y);
|
||||
m_text_component->window_to_buffer_coords(Gtk::TEXT_WINDOW_WIDGET, static_cast<int>(ev->x), static_cast<int>(ev->y), x, y);
|
||||
Gtk::TextBuffer::iterator iter;
|
||||
m_text_component->get_iter_at_location(iter, x, y);
|
||||
|
||||
const auto tags = iter.get_tags();
|
||||
for (auto tag : tags) {
|
||||
for (const auto &tag : tags) {
|
||||
const auto it = m_link_tagmap.find(tag);
|
||||
if (it != m_link_tagmap.end()) {
|
||||
if (ev->button == GDK_BUTTON_PRIMARY) {
|
||||
@@ -1033,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)
|
||||
@@ -1062,11 +1118,11 @@ ChatMessageHeader::ChatMessageHeader(const Message &data)
|
||||
};
|
||||
img.LoadFromURL(author->GetAvatarURL(data.GuildID), sigc::track_obj(cb, *this));
|
||||
|
||||
if (author->HasAnimatedAvatar()) {
|
||||
if (author->HasAnimatedAvatar(data.GuildID)) {
|
||||
auto cb = [this](const Glib::RefPtr<Gdk::PixbufAnimation> &pb) {
|
||||
m_anim_avatar = pb;
|
||||
};
|
||||
img.LoadAnimationFromURL(author->GetAvatarURL("gif"), AvatarSize, AvatarSize, sigc::track_obj(cb, *this));
|
||||
img.LoadAnimationFromURL(author->GetAvatarURL(data.GuildID, "gif"), AvatarSize, AvatarSize, sigc::track_obj(cb, *this));
|
||||
}
|
||||
|
||||
get_style_context()->add_class("message-container");
|
||||
@@ -1077,16 +1133,15 @@ ChatMessageHeader::ChatMessageHeader(const Message &data)
|
||||
m_avatar.set_valign(Gtk::ALIGN_START);
|
||||
m_avatar.set_margin_right(10);
|
||||
|
||||
m_author.set_markup(data.Author.GetEscapedBoldName());
|
||||
m_author.set_single_line_mode(true);
|
||||
m_author.set_line_wrap(false);
|
||||
m_author.set_ellipsize(Pango::ELLIPSIZE_END);
|
||||
m_author.set_xalign(0.f);
|
||||
m_author.set_xalign(0.0F);
|
||||
m_author.set_can_focus(false);
|
||||
|
||||
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);
|
||||
@@ -1094,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>");
|
||||
@@ -1158,30 +1213,32 @@ ChatMessageHeader::ChatMessageHeader(const Message &data)
|
||||
show_all();
|
||||
|
||||
auto &discord = Abaddon::Get().GetDiscordClient();
|
||||
auto role_update_cb = [this](...) { UpdateNameColor(); };
|
||||
auto role_update_cb = [this](...) { UpdateName(); };
|
||||
discord.signal_role_update().connect(sigc::track_obj(role_update_cb, *this));
|
||||
auto guild_member_update_cb = [this](const auto &, const auto &) { UpdateNameColor(); };
|
||||
auto guild_member_update_cb = [this](const auto &, const auto &) { UpdateName(); };
|
||||
discord.signal_guild_member_update().connect(sigc::track_obj(guild_member_update_cb, *this));
|
||||
UpdateNameColor();
|
||||
UpdateName();
|
||||
AttachUserMenuHandler(m_meta_ev);
|
||||
AttachUserMenuHandler(m_avatar_ev);
|
||||
}
|
||||
|
||||
void ChatMessageHeader::UpdateNameColor() {
|
||||
void ChatMessageHeader::UpdateName() {
|
||||
const auto &discord = Abaddon::Get().GetDiscordClient();
|
||||
const auto user = discord.GetUser(UserID);
|
||||
if (!user.has_value()) return;
|
||||
const auto chan = discord.GetChannel(ChannelID);
|
||||
bool is_guild = chan.has_value() && chan->GuildID.has_value();
|
||||
if (is_guild) {
|
||||
const auto member = discord.GetMember(UserID, *chan->GuildID);
|
||||
const auto role_id = discord.GetMemberHoistedRole(*chan->GuildID, UserID, true);
|
||||
const auto role = discord.GetRole(role_id);
|
||||
const auto name = GetEscapedDisplayName(*user, member);
|
||||
|
||||
std::string md;
|
||||
if (role.has_value())
|
||||
m_author.set_markup("<span weight='bold' color='#" + IntToCSSColor(role->Color) + "'>" + user->GetEscapedName() + "</span>");
|
||||
m_author.set_markup("<span weight='bold' color='#" + IntToCSSColor(role->Color) + "'>" + name + "</span>");
|
||||
else
|
||||
m_author.set_markup("<span weight='bold'>" + user->GetEscapedName() + "</span>");
|
||||
m_author.set_markup("<span weight='bold'>" + name + "</span>");
|
||||
} else
|
||||
m_author.set_markup("<span weight='bold'>" + user->GetEscapedName() + "</span>");
|
||||
}
|
||||
@@ -1205,6 +1262,13 @@ void ChatMessageHeader::AttachUserMenuHandler(Gtk::Widget &widget) {
|
||||
});
|
||||
}
|
||||
|
||||
Glib::ustring ChatMessageHeader::GetEscapedDisplayName(const UserData &user, const std::optional<GuildMember> &member) {
|
||||
if (member.has_value() && !member->Nickname.empty())
|
||||
return Glib::Markup::escape_text(member->Nickname);
|
||||
else
|
||||
return Glib::Markup::escape_text(user.GetEscapedName());
|
||||
}
|
||||
|
||||
bool ChatMessageHeader::on_author_button_press(GdkEventButton *ev) {
|
||||
if (ev->button == GDK_BUTTON_PRIMARY && (ev->state & GDK_SHIFT_MASK)) {
|
||||
m_signal_action_insert_mention.emit();
|
||||
|
||||
@@ -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;
|
||||
@@ -19,13 +19,13 @@ public:
|
||||
void SetFailed();
|
||||
|
||||
protected:
|
||||
void AddClickHandler(Gtk::Widget *widget, std::string);
|
||||
static void AddClickHandler(Gtk::Widget *widget, const std::string &);
|
||||
Gtk::TextView *CreateTextComponent(const Message &data); // Message.Content
|
||||
void UpdateTextComponent(Gtk::TextView *tv);
|
||||
Gtk::Widget *CreateEmbedComponent(const EmbedData &data); // Message.Embeds[0]
|
||||
Gtk::Widget *CreateEmbedsComponent(const std::vector<EmbedData> &embeds);
|
||||
static Gtk::Widget *CreateEmbedComponent(const EmbedData &data); // Message.Embeds[0]
|
||||
Gtk::Widget *CreateImageComponent(const std::string &proxy_url, const std::string &url, int inw, int inh);
|
||||
Gtk::Widget *CreateAttachmentComponent(const AttachmentData &data); // non-image attachments
|
||||
Gtk::Widget *CreateStickerComponentDeprecated(const StickerData &data);
|
||||
Gtk::Widget *CreateStickersComponent(const std::vector<StickerItem> &data);
|
||||
Gtk::Widget *CreateReactionsComponent(const Message &data);
|
||||
Gtk::Widget *CreateReplyComponent(const Message &data);
|
||||
@@ -34,15 +34,17 @@ protected:
|
||||
|
||||
static bool IsEmbedImageOnly(const EmbedData &data);
|
||||
|
||||
void HandleUserMentions(Glib::RefPtr<Gtk::TextBuffer> buf);
|
||||
void HandleStockEmojis(Gtk::TextView &tv);
|
||||
void HandleCustomEmojis(Gtk::TextView &tv);
|
||||
void HandleEmojis(Gtk::TextView &tv);
|
||||
void CleanupEmojis(Glib::RefPtr<Gtk::TextBuffer> buf);
|
||||
static void HandleRoleMentions(const Glib::RefPtr<Gtk::TextBuffer> &buf);
|
||||
void HandleUserMentions(const Glib::RefPtr<Gtk::TextBuffer> &buf) const;
|
||||
static void HandleStockEmojis(Gtk::TextView &tv);
|
||||
static void HandleCustomEmojis(Gtk::TextView &tv);
|
||||
static void HandleEmojis(Gtk::TextView &tv);
|
||||
static void CleanupEmojis(const Glib::RefPtr<Gtk::TextBuffer> &buf);
|
||||
|
||||
void HandleChannelMentions(Glib::RefPtr<Gtk::TextBuffer> buf);
|
||||
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;
|
||||
@@ -56,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;
|
||||
@@ -89,11 +89,12 @@ public:
|
||||
|
||||
ChatMessageHeader(const Message &data);
|
||||
void AddContent(Gtk::Widget *widget, bool prepend);
|
||||
void UpdateNameColor();
|
||||
void UpdateName();
|
||||
std::vector<Gtk::Widget *> GetChildContent();
|
||||
|
||||
protected:
|
||||
void AttachUserMenuHandler(Gtk::Widget &widget);
|
||||
static Glib::ustring GetEscapedDisplayName(const UserData &user, const std::optional<GuildMember> &member);
|
||||
|
||||
bool on_author_button_press(GdkEventButton *ev);
|
||||
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
#include "chatwindow.hpp"
|
||||
#include "chatmessage.hpp"
|
||||
#include "abaddon.hpp"
|
||||
#include "chatinputindicator.hpp"
|
||||
#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);
|
||||
@@ -16,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);
|
||||
@@ -36,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)
|
||||
@@ -45,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,12 +98,20 @@ ChatWindow::ChatWindow() {
|
||||
|
||||
m_meta->add(*m_input_indicator);
|
||||
m_meta->add(*m_rate_limit_indicator);
|
||||
//m_scroll->add(*m_list);
|
||||
// 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();
|
||||
}
|
||||
|
||||
@@ -112,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) {
|
||||
@@ -134,7 +157,7 @@ void ChatWindow::AddNewHistory(const std::vector<Message> &msgs) {
|
||||
m_chat->PrependMessages(msgs.crbegin(), msgs.crend());
|
||||
}
|
||||
|
||||
void ChatWindow::InsertChatInput(std::string text) {
|
||||
void ChatWindow::InsertChatInput(const std::string &text) {
|
||||
m_input->InsertText(text);
|
||||
}
|
||||
|
||||
@@ -151,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.size() == 0)
|
||||
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();
|
||||
|
||||
@@ -186,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
|
||||
@@ -197,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(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);
|
||||
@@ -47,8 +66,8 @@ protected:
|
||||
void OnMessageSendFail(const std::string &nonce, float retry_after);
|
||||
|
||||
Gtk::Box *m_main;
|
||||
//Gtk::ListBox *m_list;
|
||||
//Gtk::ScrolledWindow *m_scroll;
|
||||
// Gtk::ListBox *m_list;
|
||||
// Gtk::ScrolledWindow *m_scroll;
|
||||
|
||||
Gtk::EventBox m_topic; // todo probably make everything else go on the stack
|
||||
Gtk::Label m_topic_text;
|
||||
@@ -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();
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#include <unordered_set>
|
||||
#include <utility>
|
||||
#include "completer.hpp"
|
||||
#include "abaddon.hpp"
|
||||
#include "util.hpp"
|
||||
@@ -46,7 +47,7 @@ bool Completer::ProcessKeyPress(GdkEventKey *e) {
|
||||
|
||||
switch (e->keyval) {
|
||||
case GDK_KEY_Down: {
|
||||
if (m_entries.size() == 0) return true;
|
||||
if (m_entries.empty()) return true;
|
||||
const auto index = static_cast<size_t>(m_list.get_selected_row()->get_index());
|
||||
if (index >= m_entries.size() - 1) return true;
|
||||
m_list.select_row(*m_entries[index + 1]);
|
||||
@@ -54,7 +55,7 @@ bool Completer::ProcessKeyPress(GdkEventKey *e) {
|
||||
}
|
||||
return true;
|
||||
case GDK_KEY_Up: {
|
||||
if (m_entries.size() == 0) return true;
|
||||
if (m_entries.empty()) return true;
|
||||
const auto index = static_cast<size_t>(m_list.get_selected_row()->get_index());
|
||||
if (index == 0) return true;
|
||||
m_list.select_row(*m_entries[index - 1]);
|
||||
@@ -62,7 +63,7 @@ bool Completer::ProcessKeyPress(GdkEventKey *e) {
|
||||
}
|
||||
return true;
|
||||
case GDK_KEY_Return: {
|
||||
if (m_entries.size() == 0) return true;
|
||||
if (m_entries.empty()) return true;
|
||||
DoCompletion(m_list.get_selected_row());
|
||||
}
|
||||
return true;
|
||||
@@ -74,11 +75,11 @@ bool Completer::ProcessKeyPress(GdkEventKey *e) {
|
||||
}
|
||||
|
||||
void Completer::SetGetRecentAuthors(get_recent_authors_cb cb) {
|
||||
m_recent_authors_cb = cb;
|
||||
m_recent_authors_cb = std::move(cb);
|
||||
}
|
||||
|
||||
void Completer::SetGetChannelID(get_channel_id_cb cb) {
|
||||
m_channel_id_cb = cb;
|
||||
m_channel_id_cb = std::move(cb);
|
||||
}
|
||||
|
||||
bool Completer::IsShown() const {
|
||||
@@ -86,7 +87,7 @@ bool Completer::IsShown() const {
|
||||
}
|
||||
|
||||
CompleterEntry *Completer::CreateEntry(const Glib::ustring &completion) {
|
||||
auto entry = Gtk::manage(new CompleterEntry(completion, m_entries.size()));
|
||||
auto entry = Gtk::manage(new CompleterEntry(completion, static_cast<int>(m_entries.size())));
|
||||
m_entries.push_back(entry);
|
||||
entry->show_all();
|
||||
m_list.add(*entry);
|
||||
@@ -152,7 +153,7 @@ void Completer::CompleteEmojis(const Glib::ustring &term) {
|
||||
const auto make_entry = [&](const Glib::ustring &name, const Glib::ustring &completion, const Glib::ustring &url = "", bool animated = false) -> CompleterEntry * {
|
||||
const auto entry = CreateEntry(completion);
|
||||
entry->SetText(name);
|
||||
if (url == "") return entry;
|
||||
if (url.empty()) return entry;
|
||||
if (animated)
|
||||
entry->SetAnimation(url);
|
||||
else
|
||||
@@ -173,8 +174,8 @@ void Completer::CompleteEmojis(const Glib::ustring &term) {
|
||||
const auto emoji = *discord.GetEmoji(tmp.ID);
|
||||
if (emoji.IsAnimated.has_value() && *emoji.IsAnimated) continue;
|
||||
if (emoji.IsAvailable.has_value() && !*emoji.IsAvailable) continue;
|
||||
if (emoji.Roles.has_value() && emoji.Roles->size() > 0) continue;
|
||||
if (term.size() > 0)
|
||||
if (emoji.Roles.has_value() && !emoji.Roles->empty()) continue;
|
||||
if (!term.empty())
|
||||
if (!StringContainsCaseless(emoji.Name, term)) continue;
|
||||
|
||||
if (i++ > MaxCompleterEntries) break;
|
||||
@@ -190,8 +191,8 @@ void Completer::CompleteEmojis(const Glib::ustring &term) {
|
||||
const auto emoji = *discord.GetEmoji(tmp.ID);
|
||||
const bool is_animated = emoji.IsAnimated.has_value() && *emoji.IsAnimated;
|
||||
if (emoji.IsAvailable.has_value() && !*emoji.IsAvailable) continue;
|
||||
if (emoji.Roles.has_value() && emoji.Roles->size() > 0) continue;
|
||||
if (term.size() > 0)
|
||||
if (emoji.Roles.has_value() && !emoji.Roles->empty()) continue;
|
||||
if (!term.empty())
|
||||
if (!StringContainsCaseless(emoji.Name, term)) continue;
|
||||
|
||||
if (i++ > MaxCompleterEntries) goto done;
|
||||
@@ -275,7 +276,7 @@ void Completer::OnTextBufferChanged() {
|
||||
default:
|
||||
break;
|
||||
}
|
||||
if (m_entries.size() > 0) {
|
||||
if (!m_entries.empty()) {
|
||||
m_list.select_row(*m_entries[0]);
|
||||
set_reveal_child(true);
|
||||
} else {
|
||||
@@ -329,8 +330,8 @@ Glib::ustring Completer::GetTerm() {
|
||||
return m_start.get_text(m_end);
|
||||
}
|
||||
|
||||
CompleterEntry::CompleterEntry(const Glib::ustring &completion, int index)
|
||||
: m_completion(completion)
|
||||
CompleterEntry::CompleterEntry(Glib::ustring completion, int index)
|
||||
: m_completion(std::move(completion))
|
||||
, m_index(index)
|
||||
, m_box(Gtk::ORIENTATION_HORIZONTAL) {
|
||||
set_halign(Gtk::ALIGN_START);
|
||||
|
||||
@@ -8,7 +8,7 @@ constexpr static int CompleterImageSize = 24;
|
||||
|
||||
class CompleterEntry : public Gtk::ListBoxRow {
|
||||
public:
|
||||
CompleterEntry(const Glib::ustring &completion, int index);
|
||||
CompleterEntry(Glib::ustring completion, int index);
|
||||
void SetTextColor(int color); // SetText will reset
|
||||
void SetText(const Glib::ustring &text);
|
||||
void SetImage(const Glib::RefPtr<Gdk::Pixbuf> &pb);
|
||||
|
||||
@@ -102,7 +102,7 @@ bool DragListBox::scroll() {
|
||||
}
|
||||
|
||||
void DragListBox::on_drag_data_received(const Glib::RefPtr<Gdk::DragContext> &context, int x, int y, const Gtk::SelectionData &selection_data, guint info, guint time) {
|
||||
int index = 0;
|
||||
int index;
|
||||
if (m_hover_row != nullptr) {
|
||||
if (m_top) {
|
||||
index = m_hover_row->get_index() - 1;
|
||||
@@ -130,7 +130,7 @@ void DragListBox::on_drag_data_received(const Glib::RefPtr<Gdk::DragContext> &co
|
||||
void DragListBox::add_draggable(Gtk::ListBoxRow *widget) {
|
||||
widget->drag_source_set(m_entries, Gdk::BUTTON1_MASK, Gdk::ACTION_MOVE);
|
||||
widget->signal_drag_begin().connect(sigc::bind<0>(sigc::mem_fun(*this, &DragListBox::row_drag_begin), widget));
|
||||
widget->signal_drag_data_get().connect([this, widget](const Glib::RefPtr<Gdk::DragContext> &context, Gtk::SelectionData &selection_data, guint info, guint time) {
|
||||
widget->signal_drag_data_get().connect([widget](const Glib::RefPtr<Gdk::DragContext> &context, Gtk::SelectionData &selection_data, guint info, guint time) {
|
||||
selection_data.set("GTK_LIST_BOX_ROW", 32, reinterpret_cast<const guint8 *>(&widget), sizeof(&widget));
|
||||
});
|
||||
add(*widget);
|
||||
|
||||
@@ -133,7 +133,7 @@ void FriendsList::OnActionRemove(Snowflake id) {
|
||||
break;
|
||||
}
|
||||
if (Abaddon::Get().ShowConfirm(str, window)) {
|
||||
const auto cb = [this, window](DiscordError code) {
|
||||
const auto cb = [window](DiscordError code) {
|
||||
if (code == DiscordError::NONE) return;
|
||||
Gtk::MessageDialog dlg(*window, "Failed to remove user", false, Gtk::MESSAGE_ERROR, Gtk::BUTTONS_OK, true);
|
||||
dlg.set_position(Gtk::WIN_POS_CENTER);
|
||||
@@ -215,7 +215,7 @@ void FriendsListAddComponent::Submit() {
|
||||
if (hashpos == Glib::ustring::npos) return;
|
||||
const auto username = text.substr(0, hashpos);
|
||||
const auto discriminator = text.substr(hashpos + 1);
|
||||
if (username.size() == 0 || discriminator.size() != 4) return;
|
||||
if (username.empty() || discriminator.size() != 4) return;
|
||||
if (discriminator.find_first_not_of("0123456789") != Glib::ustring::npos) return;
|
||||
|
||||
m_requesting = true;
|
||||
@@ -229,7 +229,9 @@ void FriendsListAddComponent::Submit() {
|
||||
m_label.set_text("Failed: "s + GetDiscordErrorDisplayString(code));
|
||||
}
|
||||
};
|
||||
Abaddon::Get().GetDiscordClient().SendFriendRequest(username, std::stoul(discriminator), sigc::track_obj(cb, *this));
|
||||
Abaddon::Get().GetDiscordClient().SendFriendRequest(username,
|
||||
static_cast<int>(std::stoul(discriminator)),
|
||||
sigc::track_obj(cb, *this));
|
||||
}
|
||||
|
||||
bool FriendsListAddComponent::OnKeyPress(GdkEventKey *e) {
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
#include "lazyimage.hpp"
|
||||
|
||||
#include <utility>
|
||||
#include "abaddon.hpp"
|
||||
|
||||
LazyImage::LazyImage(int w, int h, bool use_placeholder)
|
||||
@@ -9,8 +11,8 @@ LazyImage::LazyImage(int w, int h, bool use_placeholder)
|
||||
signal_draw().connect(sigc::mem_fun(*this, &LazyImage::OnDraw));
|
||||
}
|
||||
|
||||
LazyImage::LazyImage(const std::string &url, int w, int h, bool use_placeholder)
|
||||
: m_url(url)
|
||||
LazyImage::LazyImage(std::string url, int w, int h, bool use_placeholder)
|
||||
: m_url(std::move(url))
|
||||
, m_width(w)
|
||||
, m_height(h) {
|
||||
if (use_placeholder)
|
||||
@@ -27,7 +29,7 @@ void LazyImage::SetURL(const std::string &url) {
|
||||
}
|
||||
|
||||
bool LazyImage::OnDraw(const Cairo::RefPtr<Cairo::Context> &context) {
|
||||
if (!m_needs_request || m_url == "") return false;
|
||||
if (!m_needs_request || m_url.empty()) return false;
|
||||
m_needs_request = false;
|
||||
|
||||
if (m_animated) {
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
class LazyImage : public Gtk::Image {
|
||||
public:
|
||||
LazyImage(int w, int h, bool use_placeholder = true);
|
||||
LazyImage(const std::string &url, int w, int h, bool use_placeholder = true);
|
||||
LazyImage(std::string url, int w, int h, bool use_placeholder = true);
|
||||
|
||||
void SetAnimated(bool is_animated);
|
||||
void SetURL(const std::string &url);
|
||||
|
||||
@@ -151,7 +151,7 @@ void MemberList::UpdateMemberList() {
|
||||
if (!pos_role.has_value()) {
|
||||
roleless_users.push_back(id);
|
||||
continue;
|
||||
};
|
||||
}
|
||||
|
||||
pos_to_role[pos_role->Position] = *pos_role;
|
||||
pos_to_users[pos_role->Position].push_back(std::move(*user));
|
||||
@@ -160,17 +160,18 @@ void MemberList::UpdateMemberList() {
|
||||
}
|
||||
|
||||
int num_rows = 0;
|
||||
const auto guild = *discord.GetGuild(m_guild_id);
|
||||
auto add_user = [this, &user_to_color, &num_rows, guild](const UserData &data) -> bool {
|
||||
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);
|
||||
return true;
|
||||
};
|
||||
|
||||
auto add_role = [this](std::string name) {
|
||||
auto add_role = [this](const std::string &name) {
|
||||
auto *role_row = Gtk::manage(new Gtk::ListBoxRow);
|
||||
auto *role_lbl = Gtk::manage(new Gtk::Label);
|
||||
|
||||
@@ -215,7 +216,7 @@ void MemberList::UpdateMemberList() {
|
||||
}
|
||||
|
||||
void MemberList::AttachUserMenuHandler(Gtk::ListBoxRow *row, Snowflake id) {
|
||||
row->signal_button_press_event().connect([this, row, id](GdkEventButton *e) -> bool {
|
||||
row->signal_button_press_event().connect([this, id](GdkEventButton *e) -> bool {
|
||||
if (e->type == GDK_BUTTON_PRESS && e->button == GDK_BUTTON_SECONDARY) {
|
||||
Abaddon::Get().ShowUserMenu(reinterpret_cast<const GdkEvent *>(e), id, m_guild_id);
|
||||
return true;
|
||||
|
||||
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;
|
||||
};
|
||||
@@ -66,7 +66,7 @@ int RateLimitIndicator::GetTimeLeft() const {
|
||||
if (sec_diff <= 0)
|
||||
return 0;
|
||||
else
|
||||
return sec_diff;
|
||||
return static_cast<int>(sec_diff);
|
||||
}
|
||||
|
||||
int RateLimitIndicator::GetRateLimit() const {
|
||||
|
||||
@@ -2,10 +2,6 @@
|
||||
#include "abaddon.hpp"
|
||||
|
||||
static const constexpr int Diameter = 8;
|
||||
static const auto OnlineColor = Gdk::RGBA("#43B581");
|
||||
static const auto IdleColor = Gdk::RGBA("#FAA61A");
|
||||
static const auto DNDColor = Gdk::RGBA("#982929");
|
||||
static const auto OfflineColor = Gdk::RGBA("#808080");
|
||||
|
||||
StatusIndicator::StatusIndicator(Snowflake user_id)
|
||||
: Glib::ObjectBase("statusindicator")
|
||||
@@ -26,9 +22,6 @@ StatusIndicator::StatusIndicator(Snowflake user_id)
|
||||
CheckStatus();
|
||||
}
|
||||
|
||||
StatusIndicator::~StatusIndicator() {
|
||||
}
|
||||
|
||||
void StatusIndicator::CheckStatus() {
|
||||
const auto status = Abaddon::Get().GetDiscordClient().GetUserStatus(m_id);
|
||||
const auto last_status = m_status;
|
||||
@@ -121,7 +114,7 @@ bool StatusIndicator::on_draw(const Cairo::RefPtr<Cairo::Context> &cr) {
|
||||
const auto color = get_style_context()->get_color(Gtk::STATE_FLAG_NORMAL);
|
||||
|
||||
cr->set_source_rgb(color.get_red(), color.get_green(), color.get_blue());
|
||||
cr->arc(width / 2, height / 2, width / 3, 0.0, 2 * (4 * std::atan(1)));
|
||||
cr->arc(width / 2.0, height / 2.0, width / 3.0, 0.0, 2 * (4 * std::atan(1)));
|
||||
cr->close_path();
|
||||
cr->fill_preserve();
|
||||
cr->stroke();
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
class StatusIndicator : public Gtk::Widget {
|
||||
public:
|
||||
StatusIndicator(Snowflake user_id);
|
||||
virtual ~StatusIndicator();
|
||||
~StatusIndicator() override = default;
|
||||
|
||||
protected:
|
||||
Gtk::SizeRequestMode get_request_mode_vfunc() const override;
|
||||
|
||||
@@ -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();
|
||||
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;
|
||||
};
|
||||
@@ -1,6 +1,6 @@
|
||||
#include "token.hpp"
|
||||
|
||||
std::string trim(const std::string& str) {
|
||||
std::string trim(const std::string &str) {
|
||||
const auto first = str.find_first_not_of(' ');
|
||||
if (first == std::string::npos) return str;
|
||||
const auto last = str.find_last_not_of(' ');
|
||||
@@ -30,6 +30,8 @@ TokenDialog::TokenDialog(Gtk::Window &parent)
|
||||
m_bbox.pack_start(m_cancel, Gtk::PACK_SHRINK);
|
||||
m_bbox.set_layout(Gtk::BUTTONBOX_END);
|
||||
|
||||
m_entry.set_input_purpose(Gtk::INPUT_PURPOSE_PASSWORD);
|
||||
m_entry.set_visibility(false);
|
||||
m_entry.set_hexpand(true);
|
||||
m_layout.add(m_entry);
|
||||
m_layout.add(m_bbox);
|
||||
|
||||
@@ -26,7 +26,7 @@ constexpr inline const char *GetPresenceString(PresenceStatus s) {
|
||||
return "";
|
||||
}
|
||||
|
||||
constexpr inline const char* GetPresenceDisplayString(PresenceStatus s) {
|
||||
constexpr inline const char *GetPresenceDisplayString(PresenceStatus s) {
|
||||
switch (s) {
|
||||
case PresenceStatus::Online:
|
||||
return "Online";
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
struct BanData {
|
||||
std::string Reason; // null
|
||||
UserData User; // access id
|
||||
UserData User; // access id
|
||||
|
||||
friend void from_json(const nlohmann::json &j, BanData &m);
|
||||
};
|
||||
|
||||
@@ -13,6 +13,8 @@ void from_json(const nlohmann::json &j, ThreadMemberObject &m) {
|
||||
JS_O("user_id", m.UserID);
|
||||
JS_D("join_timestamp", m.JoinTimestamp);
|
||||
JS_D("flags", m.Flags);
|
||||
JS_O("muted", m.IsMuted);
|
||||
JS_ON("mute_config", m.MuteConfig);
|
||||
}
|
||||
|
||||
void from_json(const nlohmann::json &j, ChannelData &m) {
|
||||
@@ -63,6 +65,11 @@ bool ChannelData::NSFW() const {
|
||||
return IsNSFW.has_value() && *IsNSFW;
|
||||
}
|
||||
|
||||
bool ChannelData::IsDM() const noexcept {
|
||||
return Type == ChannelType::DM ||
|
||||
Type == ChannelType::GROUP_DM;
|
||||
}
|
||||
|
||||
bool ChannelData::IsThread() const noexcept {
|
||||
return Type == ChannelType::GUILD_PUBLIC_THREAD ||
|
||||
Type == ChannelType::GUILD_PRIVATE_THREAD ||
|
||||
@@ -73,6 +80,43 @@ bool ChannelData::IsJoinedThread() const {
|
||||
return Abaddon::Get().GetDiscordClient().IsThreadJoined(ID);
|
||||
}
|
||||
|
||||
bool ChannelData::IsCategory() const noexcept {
|
||||
return Type == ChannelType::GUILD_CATEGORY;
|
||||
}
|
||||
|
||||
bool ChannelData::HasIcon() const noexcept {
|
||||
return Icon.has_value();
|
||||
}
|
||||
|
||||
std::string ChannelData::GetIconURL() const {
|
||||
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 {
|
||||
return Abaddon::Get().GetDiscordClient().GetChildChannelIDs(ID);
|
||||
}
|
||||
|
||||
std::optional<PermissionOverwrite> ChannelData::GetOverwrite(Snowflake id) const {
|
||||
return Abaddon::Get().GetDiscordClient().GetPermissionOverwrite(ID, id);
|
||||
}
|
||||
@@ -93,5 +137,8 @@ std::vector<UserData> ChannelData::GetDMRecipients() const {
|
||||
return ret;
|
||||
}
|
||||
|
||||
return std::vector<UserData>();
|
||||
return {};
|
||||
}
|
||||
bool ChannelData::IsText() const noexcept {
|
||||
return Type == ChannelType::GUILD_TEXT || Type == ChannelType::GUILD_NEWS;
|
||||
}
|
||||
|
||||
@@ -49,9 +49,19 @@ struct ThreadMetadataData {
|
||||
friend void from_json(const nlohmann::json &j, ThreadMetadataData &m);
|
||||
};
|
||||
|
||||
struct MuteConfigData {
|
||||
std::optional<std::string> EndTime; // nullopt is encoded as null
|
||||
int SelectedTimeWindow;
|
||||
|
||||
friend void from_json(const nlohmann::json &j, MuteConfigData &m);
|
||||
friend void to_json(nlohmann::json &j, const MuteConfigData &m);
|
||||
};
|
||||
|
||||
struct ThreadMemberObject {
|
||||
std::optional<Snowflake> ThreadID;
|
||||
std::optional<Snowflake> UserID;
|
||||
std::optional<bool> IsMuted;
|
||||
std::optional<MuteConfigData> MuteConfig;
|
||||
std::string JoinTimestamp;
|
||||
int Flags;
|
||||
|
||||
@@ -84,9 +94,16 @@ struct ChannelData {
|
||||
friend void from_json(const nlohmann::json &j, ChannelData &m);
|
||||
void update_from_json(const nlohmann::json &j);
|
||||
|
||||
bool NSFW() const;
|
||||
bool IsThread() const noexcept;
|
||||
bool IsJoinedThread() const;
|
||||
std::optional<PermissionOverwrite> GetOverwrite(Snowflake id) const;
|
||||
std::vector<UserData> GetDMRecipients() const;
|
||||
[[nodiscard]] bool NSFW() const;
|
||||
[[nodiscard]] bool IsDM() const noexcept;
|
||||
[[nodiscard]] bool IsThread() const noexcept;
|
||||
[[nodiscard]] bool IsJoinedThread() const;
|
||||
[[nodiscard]] bool IsCategory() const noexcept;
|
||||
[[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;
|
||||
};
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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,13 +50,12 @@ class DiscordClient {
|
||||
public:
|
||||
DiscordClient(bool mem_store = false);
|
||||
void Start();
|
||||
void Stop();
|
||||
bool Stop();
|
||||
bool IsStarted() const;
|
||||
bool IsStoreValid() const;
|
||||
|
||||
std::unordered_set<Snowflake> GetGuilds() const;
|
||||
const UserData &GetUserData() const;
|
||||
const UserSettings &GetUserSettings() const;
|
||||
std::vector<Snowflake> GetUserSortedGuilds() const;
|
||||
std::vector<Message> GetMessagesForChannel(Snowflake id, size_t limit = 50) const;
|
||||
std::vector<Message> GetMessagesBefore(Snowflake channel_id, Snowflake message_id, size_t limit = 50) const;
|
||||
@@ -63,8 +63,8 @@ public:
|
||||
|
||||
EPremiumType GetSelfPremiumType() const;
|
||||
|
||||
void FetchMessagesInChannel(Snowflake id, sigc::slot<void(const std::vector<Message> &)> cb);
|
||||
void FetchMessagesInChannelBefore(Snowflake channel_id, Snowflake before_id, sigc::slot<void(const std::vector<Message> &)> cb);
|
||||
void FetchMessagesInChannel(Snowflake id, const sigc::slot<void(const std::vector<Message> &)> &cb);
|
||||
void FetchMessagesInChannelBefore(Snowflake channel_id, Snowflake before_id, const sigc::slot<void(const std::vector<Message> &)> &cb);
|
||||
std::optional<Message> GetMessage(Snowflake id) const;
|
||||
std::optional<ChannelData> GetChannel(Snowflake id) const;
|
||||
std::optional<EmojiData> GetEmoji(Snowflake id) const;
|
||||
@@ -73,78 +73,103 @@ public:
|
||||
std::optional<RoleData> GetRole(Snowflake id) const;
|
||||
std::optional<GuildData> GetGuild(Snowflake id) const;
|
||||
std::optional<GuildMember> GetMember(Snowflake user_id, Snowflake guild_id) const;
|
||||
std::optional<BanData> GetBan(Snowflake guild_id, Snowflake user_id) const;
|
||||
Snowflake GetMemberHoistedRole(Snowflake guild_id, Snowflake user_id, bool with_color = false) const;
|
||||
std::optional<RoleData> GetMemberHighestRole(Snowflake guild_id, Snowflake user_id) const;
|
||||
std::set<Snowflake> GetUsersInGuild(Snowflake id) const;
|
||||
std::set<Snowflake> GetChannelsInGuild(Snowflake id) const;
|
||||
std::vector<Snowflake> GetUsersInThread(Snowflake id) const;
|
||||
std::vector<ChannelData> GetActiveThreads(Snowflake channel_id) const;
|
||||
void GetArchivedPublicThreads(Snowflake channel_id, sigc::slot<void(DiscordError, const ArchivedThreadsResponseData &)> callback);
|
||||
void GetArchivedPrivateThreads(Snowflake channel_id, sigc::slot<void(DiscordError, const ArchivedThreadsResponseData &)> callback);
|
||||
void GetArchivedPublicThreads(Snowflake channel_id, const sigc::slot<void(DiscordError, const ArchivedThreadsResponseData &)> &callback);
|
||||
void GetArchivedPrivateThreads(Snowflake channel_id, const sigc::slot<void(DiscordError, const ArchivedThreadsResponseData &)> &callback);
|
||||
std::vector<Snowflake> GetChildChannelIDs(Snowflake parent_id) const;
|
||||
|
||||
// get ids of given list of members for who we do not have the member data
|
||||
template<typename Iter>
|
||||
std::unordered_set<Snowflake> FilterUnknownMembersFrom(Snowflake guild_id, Iter begin, Iter end) {
|
||||
std::unordered_set<Snowflake> ret;
|
||||
const auto known = m_store.GetMembersInGuild(guild_id);
|
||||
for (auto iter = begin; iter != end; iter++)
|
||||
if (known.find(*iter) == known.end())
|
||||
ret.insert(*iter);
|
||||
return ret;
|
||||
}
|
||||
|
||||
bool IsThreadJoined(Snowflake thread_id) const;
|
||||
bool HasGuildPermission(Snowflake user_id, Snowflake guild_id, Permission perm) const;
|
||||
|
||||
bool 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(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(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);
|
||||
void CreateDM(Snowflake user_id, sigc::slot<void(DiscordError code, Snowflake channel_id)> callback);
|
||||
void CloseDM(Snowflake channel_id);
|
||||
std::optional<Snowflake> FindDM(Snowflake user_id); // wont find group dms
|
||||
void AddReaction(Snowflake id, Glib::ustring param);
|
||||
void RemoveReaction(Snowflake id, Glib::ustring param);
|
||||
void SetGuildName(Snowflake id, const Glib::ustring &name);
|
||||
void SetGuildName(Snowflake id, const Glib::ustring &name, sigc::slot<void(DiscordError code)> callback);
|
||||
void SetGuildIcon(Snowflake id, const std::string &data);
|
||||
void SetGuildIcon(Snowflake id, const std::string &data, sigc::slot<void(DiscordError code)> callback);
|
||||
void UnbanUser(Snowflake guild_id, Snowflake user_id);
|
||||
void UnbanUser(Snowflake guild_id, Snowflake user_id, sigc::slot<void(DiscordError code)> callback);
|
||||
void DeleteInvite(const std::string &code);
|
||||
void DeleteInvite(const std::string &code, sigc::slot<void(DiscordError code)> callback);
|
||||
void SetGuildName(Snowflake id, const Glib::ustring &name, const sigc::slot<void(DiscordError code)> &callback);
|
||||
void SetGuildIcon(Snowflake id, const std::string &data, const sigc::slot<void(DiscordError code)> &callback);
|
||||
void UnbanUser(Snowflake guild_id, Snowflake user_id, const sigc::slot<void(DiscordError code)> &callback);
|
||||
void DeleteInvite(const std::string &code, const sigc::slot<void(DiscordError code)> &callback);
|
||||
void AddGroupDMRecipient(Snowflake channel_id, Snowflake user_id);
|
||||
void RemoveGroupDMRecipient(Snowflake channel_id, Snowflake user_id);
|
||||
void ModifyRolePermissions(Snowflake guild_id, Snowflake role_id, Permission permissions, sigc::slot<void(DiscordError code)> callback);
|
||||
void ModifyRoleName(Snowflake guild_id, Snowflake role_id, const Glib::ustring &name, sigc::slot<void(DiscordError code)> callback);
|
||||
void ModifyRoleColor(Snowflake guild_id, Snowflake role_id, uint32_t color, sigc::slot<void(DiscordError code)> callback);
|
||||
void ModifyRoleColor(Snowflake guild_id, Snowflake role_id, Gdk::RGBA color, sigc::slot<void(DiscordError code)> callback);
|
||||
void ModifyRolePosition(Snowflake guild_id, Snowflake role_id, int position, sigc::slot<void(DiscordError code)> callback);
|
||||
void ModifyEmojiName(Snowflake guild_id, Snowflake emoji_id, const Glib::ustring &name, sigc::slot<void(DiscordError code)> callback);
|
||||
void DeleteEmoji(Snowflake guild_id, Snowflake emoji_id, sigc::slot<void(DiscordError code)> callback);
|
||||
std::optional<GuildApplicationData> GetGuildApplication(Snowflake guild_id) const;
|
||||
void RemoveRelationship(Snowflake id, sigc::slot<void(DiscordError code)> callback);
|
||||
void SendFriendRequest(const Glib::ustring &username, int discriminator, sigc::slot<void(DiscordError code)> callback);
|
||||
void PutRelationship(Snowflake id, sigc::slot<void(DiscordError code)> callback); // send fr by id, accept incoming
|
||||
void Pin(Snowflake channel_id, Snowflake message_id, sigc::slot<void(DiscordError code)> callback);
|
||||
void Unpin(Snowflake channel_id, Snowflake message_id, sigc::slot<void(DiscordError code)> callback);
|
||||
void LeaveThread(Snowflake channel_id, const std::string &location, sigc::slot<void(DiscordError code)> callback);
|
||||
void ArchiveThread(Snowflake channel_id, sigc::slot<void(DiscordError code)> callback);
|
||||
void UnArchiveThread(Snowflake channel_id, sigc::slot<void(DiscordError code)> callback);
|
||||
void ModifyRolePermissions(Snowflake guild_id, Snowflake role_id, Permission permissions, const sigc::slot<void(DiscordError code)> &callback);
|
||||
void ModifyRoleName(Snowflake guild_id, Snowflake role_id, const Glib::ustring &name, const sigc::slot<void(DiscordError code)> &callback);
|
||||
void ModifyRoleColor(Snowflake guild_id, Snowflake role_id, uint32_t color, const sigc::slot<void(DiscordError code)> &callback);
|
||||
void ModifyRoleColor(Snowflake guild_id, Snowflake role_id, const Gdk::RGBA &color, const sigc::slot<void(DiscordError code)> &callback);
|
||||
void ModifyRolePosition(Snowflake guild_id, Snowflake role_id, int position, const sigc::slot<void(DiscordError code)> &callback);
|
||||
void ModifyEmojiName(Snowflake guild_id, Snowflake emoji_id, const Glib::ustring &name, const sigc::slot<void(DiscordError code)> &callback);
|
||||
void DeleteEmoji(Snowflake guild_id, Snowflake emoji_id, const sigc::slot<void(DiscordError code)> &callback);
|
||||
void RemoveRelationship(Snowflake id, const sigc::slot<void(DiscordError code)> &callback);
|
||||
void SendFriendRequest(const Glib::ustring &username, int discriminator, const sigc::slot<void(DiscordError code)> &callback);
|
||||
void PutRelationship(Snowflake id, const sigc::slot<void(DiscordError code)> &callback); // send fr by id, accept incoming
|
||||
void Pin(Snowflake channel_id, Snowflake message_id, const sigc::slot<void(DiscordError code)> &callback);
|
||||
void Unpin(Snowflake channel_id, Snowflake message_id, const sigc::slot<void(DiscordError code)> &callback);
|
||||
void LeaveThread(Snowflake channel_id, const std::string &location, const sigc::slot<void(DiscordError code)> &callback);
|
||||
void ArchiveThread(Snowflake channel_id, const sigc::slot<void(DiscordError code)> &callback);
|
||||
void UnArchiveThread(Snowflake channel_id, const sigc::slot<void(DiscordError code)> &callback);
|
||||
void MarkChannelAsRead(Snowflake channel_id, const sigc::slot<void(DiscordError code)> &callback);
|
||||
void MarkGuildAsRead(Snowflake guild_id, const sigc::slot<void(DiscordError code)> &callback);
|
||||
void MuteChannel(Snowflake channel_id, const sigc::slot<void(DiscordError code)> &callback);
|
||||
void UnmuteChannel(Snowflake channel_id, const sigc::slot<void(DiscordError code)> &callback);
|
||||
void MuteGuild(Snowflake id, const sigc::slot<void(DiscordError code)> &callback);
|
||||
void UnmuteGuild(Snowflake id, const sigc::slot<void(DiscordError code)> &callback);
|
||||
void MuteThread(Snowflake id, const sigc::slot<void(DiscordError code)> &callback);
|
||||
void UnmuteThread(Snowflake id, const sigc::slot<void(DiscordError code)> &callback);
|
||||
|
||||
bool CanModifyRole(Snowflake guild_id, Snowflake role_id) const;
|
||||
bool CanModifyRole(Snowflake guild_id, Snowflake role_id, Snowflake user_id) const;
|
||||
|
||||
// send op 8 to get member data for unknown members
|
||||
template<typename Iter>
|
||||
void RequestMembers(Snowflake guild_id, Iter begin, Iter end) {
|
||||
if (std::distance(begin, end) == 0) return;
|
||||
|
||||
RequestGuildMembersMessage obj;
|
||||
obj.GuildID = guild_id;
|
||||
obj.Presences = false;
|
||||
obj.UserIDs = { begin, end };
|
||||
m_websocket.Send(obj);
|
||||
}
|
||||
|
||||
// real client doesn't seem to use the single role endpoints so neither do we
|
||||
template<typename Iter>
|
||||
auto SetMemberRoles(Snowflake guild_id, Snowflake user_id, Iter begin, Iter end, sigc::slot<void(DiscordError code)> callback) {
|
||||
auto SetMemberRoles(Snowflake guild_id, Snowflake user_id, Iter begin, Iter end, const sigc::slot<void(DiscordError code)> &callback) {
|
||||
ModifyGuildMemberObject obj;
|
||||
obj.Roles = { begin, end };
|
||||
m_http.MakePATCH("/guilds/" + std::to_string(guild_id) + "/members/" + std::to_string(user_id), nlohmann::json(obj).dump(), [this, callback](const http::response_type &response) {
|
||||
@@ -157,30 +182,40 @@ public:
|
||||
|
||||
// FetchGuildBans fetches all bans+reasons via api, this func fetches stored bans (so usually just GUILD_BAN_ADD data)
|
||||
std::vector<BanData> GetBansInGuild(Snowflake guild_id);
|
||||
void FetchGuildBan(Snowflake guild_id, Snowflake user_id, sigc::slot<void(BanData)> callback);
|
||||
void FetchGuildBans(Snowflake guild_id, sigc::slot<void(std::vector<BanData>)> callback);
|
||||
void FetchGuildBan(Snowflake guild_id, Snowflake user_id, const sigc::slot<void(BanData)> &callback);
|
||||
void FetchGuildBans(Snowflake guild_id, const sigc::slot<void(std::vector<BanData>)> &callback);
|
||||
|
||||
void FetchInvite(std::string code, sigc::slot<void(std::optional<InviteData>)> callback);
|
||||
void FetchGuildInvites(Snowflake guild_id, sigc::slot<void(std::vector<InviteData>)> callback);
|
||||
void FetchInvite(const std::string &code, const sigc::slot<void(std::optional<InviteData>)> &callback);
|
||||
void FetchGuildInvites(Snowflake guild_id, const sigc::slot<void(std::vector<InviteData>)> &callback);
|
||||
|
||||
void FetchAuditLog(Snowflake guild_id, sigc::slot<void(AuditLogData)> callback);
|
||||
void FetchAuditLog(Snowflake guild_id, const sigc::slot<void(AuditLogData)> &callback);
|
||||
|
||||
void FetchGuildEmojis(Snowflake guild_id, sigc::slot<void(std::vector<EmojiData>)> callback);
|
||||
void FetchGuildEmojis(Snowflake guild_id, const sigc::slot<void(std::vector<EmojiData>)> &callback);
|
||||
|
||||
void FetchUserProfile(Snowflake user_id, sigc::slot<void(UserProfileData)> callback);
|
||||
void FetchUserNote(Snowflake user_id, sigc::slot<void(std::string note)> callback);
|
||||
void SetUserNote(Snowflake user_id, std::string note);
|
||||
void SetUserNote(Snowflake user_id, std::string note, sigc::slot<void(DiscordError code)> callback);
|
||||
void FetchUserRelationships(Snowflake user_id, sigc::slot<void(std::vector<UserData>)> callback);
|
||||
void FetchUserProfile(Snowflake user_id, const sigc::slot<void(UserProfileData)> &callback);
|
||||
void FetchUserNote(Snowflake user_id, const sigc::slot<void(std::string note)> &callback);
|
||||
void SetUserNote(Snowflake user_id, std::string note, const sigc::slot<void(DiscordError code)> &callback);
|
||||
void FetchUserRelationships(Snowflake user_id, const sigc::slot<void(std::vector<UserData>)> &callback);
|
||||
|
||||
void FetchPinned(Snowflake id, sigc::slot<void(std::vector<Message>, DiscordError code)> callback);
|
||||
void FetchPinned(Snowflake id, const sigc::slot<void(std::vector<Message>, DiscordError code)> &callback);
|
||||
|
||||
bool IsVerificationRequired(Snowflake guild_id);
|
||||
void GetVerificationGateInfo(Snowflake guild_id, sigc::slot<void(std::optional<VerificationGateInfoObject>)> callback);
|
||||
void AcceptVerificationGate(Snowflake guild_id, VerificationGateInfoObject info, sigc::slot<void(DiscordError code)> callback);
|
||||
bool IsVerificationRequired(Snowflake guild_id) const;
|
||||
void GetVerificationGateInfo(Snowflake guild_id, const sigc::slot<void(std::optional<VerificationGateInfoObject>)> &callback);
|
||||
void AcceptVerificationGate(Snowflake guild_id, VerificationGateInfoObject info, const sigc::slot<void(DiscordError code)> &callback);
|
||||
|
||||
void UpdateToken(std::string token);
|
||||
void SetUserAgent(std::string agent);
|
||||
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);
|
||||
|
||||
bool IsChannelMuted(Snowflake id) const noexcept;
|
||||
bool IsGuildMuted(Snowflake id) const noexcept;
|
||||
int GetUnreadStateForChannel(Snowflake id) const noexcept;
|
||||
bool GetUnreadStateForGuild(Snowflake id, int &total_mentions) const noexcept;
|
||||
int GetUnreadDMsCount() const;
|
||||
|
||||
PresenceStatus GetUserStatus(Snowflake id) const;
|
||||
|
||||
@@ -194,8 +229,8 @@ private:
|
||||
std::vector<uint8_t> m_decompress_buf;
|
||||
z_stream m_zstream;
|
||||
|
||||
std::string GetAPIURL();
|
||||
std::string GetGatewayURL();
|
||||
static std::string GetAPIURL();
|
||||
static std::string GetGatewayURL();
|
||||
|
||||
static DiscordError GetCodeFromResponse(const http::response_type &response);
|
||||
|
||||
@@ -244,6 +279,9 @@ private:
|
||||
void HandleGatewayThreadMemberUpdate(const GatewayMessage &msg);
|
||||
void HandleGatewayThreadUpdate(const GatewayMessage &msg);
|
||||
void HandleGatewayThreadMemberListUpdate(const GatewayMessage &msg);
|
||||
void HandleGatewayMessageAck(const GatewayMessage &msg);
|
||||
void HandleGatewayUserGuildSettingsUpdate(const GatewayMessage &msg);
|
||||
void HandleGatewayGuildMembersChunk(const GatewayMessage &msg);
|
||||
void HandleGatewayReadySupplemental(const GatewayMessage &msg);
|
||||
void HandleGatewayReconnect(const GatewayMessage &msg);
|
||||
void HandleGatewayInvalidSession(const GatewayMessage &msg);
|
||||
@@ -251,16 +289,24 @@ private:
|
||||
void SendIdentify();
|
||||
void SendResume();
|
||||
|
||||
void SetHeaders();
|
||||
void SetSuperPropertiesFromIdentity(const IdentifyMessage &identity);
|
||||
|
||||
void HandleSocketOpen();
|
||||
void HandleSocketClose(uint16_t code);
|
||||
|
||||
bool CheckCode(const http::response_type &r);
|
||||
bool CheckCode(const http::response_type &r, int expected);
|
||||
static bool CheckCode(const http::response_type &r);
|
||||
static bool CheckCode(const http::response_type &r, int expected);
|
||||
|
||||
void StoreMessageData(Message &msg);
|
||||
|
||||
void HandleReadyReadState(const ReadyEventData &data);
|
||||
void HandleReadyGuildSettings(const ReadyEventData &data);
|
||||
|
||||
std::string m_token;
|
||||
|
||||
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;
|
||||
@@ -269,6 +315,11 @@ private:
|
||||
std::map<Snowflake, RelationshipType> m_user_relationships;
|
||||
std::set<Snowflake> m_joined_threads;
|
||||
std::map<Snowflake, std::vector<Snowflake>> m_thread_members;
|
||||
std::map<Snowflake, Snowflake> m_last_message_id;
|
||||
std::unordered_set<Snowflake> m_muted_guilds;
|
||||
std::unordered_set<Snowflake> m_muted_channels;
|
||||
std::unordered_map<Snowflake, int> m_unread;
|
||||
std::unordered_set<Snowflake> m_channel_muted_parent;
|
||||
|
||||
UserData m_user_data;
|
||||
UserSettings m_user_settings;
|
||||
@@ -302,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;
|
||||
|
||||
@@ -343,6 +396,8 @@ public:
|
||||
typedef sigc::signal<void, ThreadMembersUpdateData> type_signal_thread_members_update;
|
||||
typedef sigc::signal<void, ThreadUpdateData> type_signal_thread_update;
|
||||
typedef sigc::signal<void, ThreadMemberListUpdateData> type_signal_thread_member_list_update;
|
||||
typedef sigc::signal<void, MessageAckData> type_signal_message_ack;
|
||||
typedef sigc::signal<void, GuildMembersChunkData> type_signal_guild_members_chunk;
|
||||
|
||||
// not discord dispatch events
|
||||
typedef sigc::signal<void, Snowflake> type_signal_added_to_thread;
|
||||
@@ -350,10 +405,16 @@ public:
|
||||
typedef sigc::signal<void, Message> type_signal_message_unpinned;
|
||||
typedef sigc::signal<void, Message> type_signal_message_pinned;
|
||||
typedef sigc::signal<void, Message> type_signal_message_sent;
|
||||
typedef sigc::signal<void, Snowflake> type_signal_channel_muted;
|
||||
typedef sigc::signal<void, Snowflake> type_signal_channel_unmuted;
|
||||
typedef sigc::signal<void, Snowflake> type_signal_guild_muted;
|
||||
typedef sigc::signal<void, Snowflake> type_signal_guild_unmuted;
|
||||
typedef sigc::signal<void, 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();
|
||||
@@ -393,13 +454,21 @@ public:
|
||||
type_signal_thread_members_update signal_thread_members_update();
|
||||
type_signal_thread_update signal_thread_update();
|
||||
type_signal_thread_member_list_update signal_thread_member_list_update();
|
||||
type_signal_message_ack signal_message_ack();
|
||||
type_signal_guild_members_chunk signal_guild_members_chunk();
|
||||
|
||||
type_signal_added_to_thread signal_added_to_thread();
|
||||
type_signal_removed_from_thread signal_removed_from_thread();
|
||||
type_signal_message_sent signal_message_sent();
|
||||
type_signal_channel_muted signal_channel_muted();
|
||||
type_signal_channel_unmuted signal_channel_unmuted();
|
||||
type_signal_guild_muted signal_guild_muted();
|
||||
type_signal_guild_unmuted signal_guild_unmuted();
|
||||
type_signal_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;
|
||||
@@ -440,11 +509,19 @@ protected:
|
||||
type_signal_thread_members_update m_signal_thread_members_update;
|
||||
type_signal_thread_update m_signal_thread_update;
|
||||
type_signal_thread_member_list_update m_signal_thread_member_list_update;
|
||||
type_signal_message_ack m_signal_message_ack;
|
||||
type_signal_guild_members_chunk m_signal_guild_members_chunk;
|
||||
|
||||
type_signal_removed_from_thread m_signal_removed_from_thread;
|
||||
type_signal_added_to_thread m_signal_added_to_thread;
|
||||
type_signal_message_sent m_signal_message_sent;
|
||||
type_signal_channel_muted m_signal_channel_muted;
|
||||
type_signal_channel_unmuted m_signal_channel_unmuted;
|
||||
type_signal_guild_muted m_signal_guild_muted;
|
||||
type_signal_guild_unmuted m_signal_guild_unmuted;
|
||||
type_signal_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,7 +16,7 @@ void to_json(nlohmann::json &j, const EmojiData &m) {
|
||||
j["id"] = m.ID;
|
||||
else
|
||||
j["id"] = nullptr;
|
||||
if (m.Name != "")
|
||||
if (!m.Name.empty())
|
||||
j["name"] = m.Name;
|
||||
else
|
||||
j["name"] = nullptr;
|
||||
|
||||
@@ -77,7 +77,7 @@ void GuildData::update_from_json(const nlohmann::json &j) {
|
||||
JS_RD("owner_id", OwnerID);
|
||||
std::string tmp;
|
||||
JS_RD("permissions", tmp);
|
||||
if (tmp != "")
|
||||
if (!tmp.empty())
|
||||
Permissions = std::stoull(tmp);
|
||||
JS_RD("region", VoiceRegion);
|
||||
JS_RD("afk_channel_id", AFKChannelID);
|
||||
@@ -119,7 +119,7 @@ void GuildData::update_from_json(const nlohmann::json &j) {
|
||||
JS_RD("approximate_presence_count", ApproximatePresenceCount);
|
||||
}
|
||||
|
||||
bool GuildData::HasFeature(const std::string &search_feature) {
|
||||
bool GuildData::HasFeature(const std::string &search_feature) const {
|
||||
if (!Features.has_value()) return false;
|
||||
for (const auto &feature : *Features)
|
||||
if (search_feature == feature)
|
||||
@@ -128,81 +128,17 @@ bool GuildData::HasFeature(const std::string &search_feature) {
|
||||
}
|
||||
|
||||
bool GuildData::HasIcon() const {
|
||||
return Icon != "";
|
||||
return !Icon.empty();
|
||||
}
|
||||
|
||||
bool GuildData::HasAnimatedIcon() const {
|
||||
return HasIcon() && Icon[0] == 'a' && Icon[1] == '_';
|
||||
}
|
||||
|
||||
std::string GuildData::GetIconURL(std::string ext, std::string size) const {
|
||||
std::string GuildData::GetIconURL(const std::string &ext, const std::string &size) const {
|
||||
return "https://cdn.discordapp.com/icons/" + std::to_string(ID) + "/" + Icon + "." + ext + "?size=" + size;
|
||||
}
|
||||
|
||||
std::vector<Snowflake> GuildData::GetSortedChannels(Snowflake ignore) const {
|
||||
std::vector<Snowflake> ret;
|
||||
|
||||
const auto &discord = Abaddon::Get().GetDiscordClient();
|
||||
auto channels = discord.GetChannelsInGuild(ID);
|
||||
|
||||
std::unordered_map<Snowflake, std::vector<ChannelData>> category_to_channels;
|
||||
std::map<int, std::vector<ChannelData>> position_to_categories;
|
||||
std::map<int, std::vector<ChannelData>> orphan_channels;
|
||||
for (const auto &channel_id : channels) {
|
||||
const auto data = discord.GetChannel(channel_id);
|
||||
if (!data->ParentID.has_value() && (data->Type == ChannelType::GUILD_TEXT || data->Type == ChannelType::GUILD_NEWS))
|
||||
orphan_channels[*data->Position].push_back(*data);
|
||||
else if (data->ParentID.has_value() && (data->Type == ChannelType::GUILD_TEXT || data->Type == ChannelType::GUILD_NEWS))
|
||||
category_to_channels[*data->ParentID].push_back(*data);
|
||||
else if (data->Type == ChannelType::GUILD_CATEGORY)
|
||||
position_to_categories[*data->Position].push_back(*data);
|
||||
}
|
||||
|
||||
for (auto &[pos, channels] : orphan_channels) {
|
||||
std::sort(channels.begin(), channels.end(), [&](const ChannelData &a, const ChannelData &b) -> bool {
|
||||
return a.ID < b.ID;
|
||||
});
|
||||
for (const auto &chan : channels)
|
||||
ret.push_back(chan.ID);
|
||||
}
|
||||
|
||||
for (auto &[pos, categories] : position_to_categories) {
|
||||
std::sort(categories.begin(), categories.end(), [&](const ChannelData &a, const ChannelData &b) -> bool {
|
||||
return a.ID < b.ID;
|
||||
});
|
||||
for (const auto &category : categories) {
|
||||
ret.push_back(category.ID);
|
||||
if (ignore == category.ID) continue; // stupid hack to save me some time
|
||||
auto it = category_to_channels.find(category.ID);
|
||||
if (it == category_to_channels.end()) continue;
|
||||
auto &channels = it->second;
|
||||
std::sort(channels.begin(), channels.end(), [&](const ChannelData &a, const ChannelData &b) -> bool {
|
||||
return a.Position < b.Position;
|
||||
});
|
||||
for (auto &channel : channels) {
|
||||
ret.push_back(channel.ID);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
std::vector<RoleData> GuildData::FetchRoles() const {
|
||||
if (!Roles.has_value()) return {};
|
||||
std::vector<RoleData> ret;
|
||||
ret.reserve(Roles->size());
|
||||
for (const auto thing : *Roles) {
|
||||
auto r = Abaddon::Get().GetDiscordClient().GetRole(thing.ID);
|
||||
if (r.has_value())
|
||||
ret.push_back(*r);
|
||||
}
|
||||
std::sort(ret.begin(), ret.end(), [](const RoleData &a, const RoleData &b) -> bool {
|
||||
return a.Position > b.Position;
|
||||
});
|
||||
return ret;
|
||||
}
|
||||
|
||||
void from_json(const nlohmann::json &j, GuildApplicationData &m) {
|
||||
JS_D("user_id", m.UserID);
|
||||
JS_D("guild_id", m.GuildID);
|
||||
|
||||
@@ -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;
|
||||
@@ -50,7 +57,7 @@ struct GuildData {
|
||||
std::optional<int> VerificationLevel;
|
||||
std::optional<int> DefaultMessageNotifications;
|
||||
std::optional<int> ExplicitContentFilter;
|
||||
std::optional<std::vector<RoleData>> Roles; // only access id
|
||||
std::optional<std::vector<RoleData>> Roles;
|
||||
std::optional<std::vector<EmojiData>> Emojis; // only access id
|
||||
std::optional<std::unordered_set<std::string>> Features;
|
||||
std::optional<int> MFALevel;
|
||||
@@ -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
|
||||
@@ -91,10 +98,8 @@ struct GuildData {
|
||||
friend void from_json(const nlohmann::json &j, GuildData &m);
|
||||
void update_from_json(const nlohmann::json &j);
|
||||
|
||||
bool HasFeature(const std::string &feature);
|
||||
bool HasFeature(const std::string &feature) const;
|
||||
bool HasIcon() const;
|
||||
bool HasAnimatedIcon() const;
|
||||
std::string GetIconURL(std::string ext = "png", std::string size = "32") const;
|
||||
std::vector<Snowflake> GetSortedChannels(Snowflake ignore = Snowflake::Invalid) const;
|
||||
std::vector<RoleData> FetchRoles() const; // sorted
|
||||
std::string GetIconURL(const std::string &ext = "png", const std::string &size = "32") const;
|
||||
};
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#include "httpclient.hpp"
|
||||
|
||||
//#define USE_LOCAL_PROXY
|
||||
#include <utility>
|
||||
|
||||
HTTPClient::HTTPClient() {
|
||||
m_dispatcher.connect(sigc::mem_fun(*this, &HTTPClient::RunCallbacks));
|
||||
}
|
||||
@@ -10,23 +11,29 @@ void HTTPClient::SetBase(const std::string &url) {
|
||||
}
|
||||
|
||||
void HTTPClient::SetUserAgent(std::string agent) {
|
||||
m_agent = agent;
|
||||
m_agent = std::move(agent);
|
||||
}
|
||||
|
||||
void HTTPClient::SetAuth(std::string auth) {
|
||||
m_authorization = auth;
|
||||
m_authorization = std::move(auth);
|
||||
}
|
||||
|
||||
void HTTPClient::MakeDELETE(const std::string &path, std::function<void(http::response_type r)> cb) {
|
||||
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_user_agent(m_agent != "" ? m_agent : "Abaddon");
|
||||
#ifdef USE_LOCAL_PROXY
|
||||
req.set_proxy("http://127.0.0.1:8888");
|
||||
req.set_verify_ssl(false);
|
||||
#endif
|
||||
req.set_header("Origin", "https://discord.com");
|
||||
req.set_user_agent(!m_agent.empty() ? m_agent : "Abaddon");
|
||||
|
||||
auto res = req.execute();
|
||||
|
||||
@@ -34,18 +41,16 @@ void HTTPClient::MakeDELETE(const std::string &path, std::function<void(http::re
|
||||
}));
|
||||
}
|
||||
|
||||
void HTTPClient::MakePATCH(const std::string &path, const std::string &payload, std::function<void(http::response_type r)> cb) {
|
||||
void HTTPClient::MakePATCH(const std::string &path, const std::string &payload, const std::function<void(http::response_type r)> &cb) {
|
||||
printf("PATCH %s\n", path.c_str());
|
||||
m_futures.push_back(std::async(std::launch::async, [this, path, cb, payload] {
|
||||
http::request req(http::REQUEST_PATCH, m_api_base + path);
|
||||
AddHeaders(req);
|
||||
req.set_header("Authorization", m_authorization);
|
||||
req.set_header("Content-Type", "application/json");
|
||||
req.set_user_agent(m_agent != "" ? m_agent : "Abaddon");
|
||||
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();
|
||||
|
||||
@@ -53,18 +58,16 @@ void HTTPClient::MakePATCH(const std::string &path, const std::string &payload,
|
||||
}));
|
||||
}
|
||||
|
||||
void HTTPClient::MakePOST(const std::string &path, const std::string &payload, std::function<void(http::response_type r)> cb) {
|
||||
void HTTPClient::MakePOST(const std::string &path, const std::string &payload, const std::function<void(http::response_type r)> &cb) {
|
||||
printf("POST %s\n", path.c_str());
|
||||
m_futures.push_back(std::async(std::launch::async, [this, path, cb, payload] {
|
||||
http::request req(http::REQUEST_POST, m_api_base + path);
|
||||
AddHeaders(req);
|
||||
req.set_header("Authorization", m_authorization);
|
||||
req.set_header("Content-Type", "application/json");
|
||||
req.set_user_agent(m_agent != "" ? m_agent : "Abaddon");
|
||||
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();
|
||||
|
||||
@@ -72,19 +75,17 @@ void HTTPClient::MakePOST(const std::string &path, const std::string &payload, s
|
||||
}));
|
||||
}
|
||||
|
||||
void HTTPClient::MakePUT(const std::string &path, const std::string &payload, std::function<void(http::response_type r)> cb) {
|
||||
void HTTPClient::MakePUT(const std::string &path, const std::string &payload, const std::function<void(http::response_type r)> &cb) {
|
||||
printf("PUT %s\n", path.c_str());
|
||||
m_futures.push_back(std::async(std::launch::async, [this, path, cb, payload] {
|
||||
http::request req(http::REQUEST_PUT, m_api_base + path);
|
||||
AddHeaders(req);
|
||||
req.set_header("Authorization", m_authorization);
|
||||
if (payload != "")
|
||||
req.set_header("Origin", "https://discord.com");
|
||||
if (!payload.empty())
|
||||
req.set_header("Content-Type", "application/json");
|
||||
req.set_user_agent(m_agent != "" ? m_agent : "Abaddon");
|
||||
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();
|
||||
|
||||
@@ -92,17 +93,13 @@ void HTTPClient::MakePUT(const std::string &path, const std::string &payload, st
|
||||
}));
|
||||
}
|
||||
|
||||
void HTTPClient::MakeGET(const std::string &path, std::function<void(http::response_type r)> cb) {
|
||||
void HTTPClient::MakeGET(const std::string &path, const std::function<void(http::response_type r)> &cb) {
|
||||
printf("GET %s\n", path.c_str());
|
||||
m_futures.push_back(std::async(std::launch::async, [this, path, cb] {
|
||||
http::request req(http::REQUEST_GET, m_api_base + path);
|
||||
AddHeaders(req);
|
||||
req.set_header("Authorization", m_authorization);
|
||||
req.set_header("Content-Type", "application/json");
|
||||
req.set_user_agent(m_agent != "" ? m_agent : "Abaddon");
|
||||
#ifdef USE_LOCAL_PROXY
|
||||
req.set_proxy("http://127.0.0.1:8888");
|
||||
req.set_verify_ssl(false);
|
||||
#endif
|
||||
req.set_user_agent(!m_agent.empty() ? m_agent : "Abaddon");
|
||||
|
||||
auto res = req.execute();
|
||||
|
||||
@@ -110,6 +107,21 @@ void HTTPClient::MakeGET(const std::string &path, std::function<void(http::respo
|
||||
}));
|
||||
}
|
||||
|
||||
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)
|
||||
@@ -126,11 +138,19 @@ void HTTPClient::RunCallbacks() {
|
||||
m_mutex.unlock();
|
||||
}
|
||||
|
||||
void HTTPClient::OnResponse(const http::response_type &r, std::function<void(http::response_type r)> cb) {
|
||||
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 {
|
||||
m_mutex.lock();
|
||||
m_queue.push([this, r, cb] { cb(r); });
|
||||
m_queue.push([r, cb] { cb(r); });
|
||||
m_dispatcher.emit();
|
||||
m_mutex.unlock();
|
||||
} catch (const std::exception &e) {
|
||||
|
||||
@@ -17,14 +17,22 @@ public:
|
||||
|
||||
void SetUserAgent(std::string agent);
|
||||
void SetAuth(std::string auth);
|
||||
void MakeDELETE(const std::string &path, std::function<void(http::response_type r)> cb);
|
||||
void MakeGET(const std::string &path, std::function<void(http::response_type r)> cb);
|
||||
void MakePATCH(const std::string &path, const std::string &payload, std::function<void(http::response_type r)> cb);
|
||||
void MakePOST(const std::string &path, const std::string &payload, std::function<void(http::response_type r)> cb);
|
||||
void MakePUT(const std::string &path, const std::string &payload, std::function<void(http::response_type r)> cb);
|
||||
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 OnResponse(const http::response_type &r, std::function<void(http::response_type r)> cb);
|
||||
void AddHeaders(http::request &r);
|
||||
|
||||
void OnResponse(const http::response_type &r, const std::function<void(http::response_type r)> &cb);
|
||||
void CleanupFutures();
|
||||
|
||||
mutable std::mutex m_mutex;
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -14,10 +14,10 @@ enum class InteractionType {
|
||||
};
|
||||
|
||||
struct MessageInteractionData {
|
||||
Snowflake ID; // id of the interaction
|
||||
InteractionType Type; // the type of interaction
|
||||
std::string Name; // the name of the ApplicationCommand
|
||||
UserData User; // the user who invoked the interaction
|
||||
Snowflake ID; // id of the interaction
|
||||
InteractionType Type; // the type of interaction
|
||||
std::string Name; // the name of the ApplicationCommand
|
||||
UserData User; // the user who invoked the interaction
|
||||
// undocumented???
|
||||
std::optional<GuildMember> Member; // the member who invoked the interaction (in a guild)
|
||||
|
||||
|
||||
@@ -141,8 +141,8 @@ inline void json_update_optional_nullable_default(const ::nlohmann::json &j, con
|
||||
} while (0)
|
||||
|
||||
// set a json value from a std::optional only if it has a value
|
||||
#define JS_IF(k, v) \
|
||||
do { \
|
||||
if (v.has_value()) \
|
||||
j[k] = *v; \
|
||||
#define JS_IF(k, v) \
|
||||
do { \
|
||||
if ((v).has_value()) \
|
||||
j[k] = *(v); \
|
||||
} while (0)
|
||||
|
||||
@@ -19,7 +19,7 @@ std::vector<RoleData> GuildMember::GetSortedRoles() const {
|
||||
for (const auto role_id : Roles) {
|
||||
const auto role = Abaddon::Get().GetDiscordClient().GetRole(role_id);
|
||||
if (!role.has_value()) continue;
|
||||
roles.push_back(std::move(*role));
|
||||
roles.push_back(*role);
|
||||
}
|
||||
|
||||
std::sort(roles.begin(), roles.end(), [](const RoleData &a, const RoleData &b) {
|
||||
|
||||
@@ -20,7 +20,7 @@ struct GuildMember {
|
||||
// undocuemtned moment !!!1
|
||||
std::optional<std::string> Avatar;
|
||||
|
||||
std::vector<RoleData> GetSortedRoles() const;
|
||||
[[nodiscard]] std::vector<RoleData> GetSortedRoles() const;
|
||||
|
||||
void update_from_json(const nlohmann::json &j);
|
||||
friend void from_json(const nlohmann::json &j, GuildMember &m);
|
||||
|
||||
@@ -176,7 +176,7 @@ void to_json(nlohmann::json &j, const MessageApplicationData &m) {
|
||||
j["id"] = m.ID;
|
||||
JS_IF("cover_image", m.CoverImage);
|
||||
j["description"] = m.Description;
|
||||
if (m.Icon == "")
|
||||
if (m.Icon.empty())
|
||||
j["icon"] = nullptr;
|
||||
else
|
||||
j["icon"] = m.Icon;
|
||||
@@ -230,7 +230,7 @@ void Message::from_json_edited(const nlohmann::json &j) {
|
||||
JS_O("content", Content);
|
||||
JS_O("timestamp", Timestamp);
|
||||
JS_ON("edited_timestamp", EditedTimestamp);
|
||||
if (EditedTimestamp.size() > 0)
|
||||
if (!EditedTimestamp.empty())
|
||||
SetEdited();
|
||||
JS_O("tts", IsTTS);
|
||||
JS_O("mention_everyone", DoesMentionEveryone);
|
||||
@@ -263,3 +263,9 @@ bool Message::IsDeleted() const {
|
||||
bool Message::IsEdited() const {
|
||||
return m_edited;
|
||||
}
|
||||
|
||||
bool Message::DoesMention(Snowflake id) const noexcept {
|
||||
return std::any_of(Mentions.begin(), Mentions.end(), [id](const UserData &user) {
|
||||
return user.ID == id;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -209,8 +209,10 @@ struct Message {
|
||||
|
||||
void SetDeleted();
|
||||
void SetEdited();
|
||||
bool IsDeleted() const;
|
||||
bool IsEdited() const;
|
||||
[[nodiscard]] bool IsDeleted() const;
|
||||
[[nodiscard]] bool IsEdited() const;
|
||||
|
||||
[[nodiscard]] bool DoesMention(Snowflake id) const noexcept;
|
||||
|
||||
private:
|
||||
bool m_deleted = false;
|
||||
|
||||
@@ -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);
|
||||
@@ -77,7 +77,7 @@ void from_json(const nlohmann::json &j, GuildMemberListUpdateMessage &m) {
|
||||
}
|
||||
|
||||
void to_json(nlohmann::json &j, const LazyLoadRequestMessage &m) {
|
||||
j["op"] = GatewayOp::LazyLoadRequest;
|
||||
j["op"] = GatewayOp::GuildSubscriptions;
|
||||
j["d"] = nlohmann::json::object();
|
||||
j["d"]["guild_id"] = m.GuildID;
|
||||
if (m.Channels.has_value()) {
|
||||
@@ -98,7 +98,7 @@ void to_json(nlohmann::json &j, const LazyLoadRequestMessage &m) {
|
||||
}
|
||||
|
||||
void to_json(nlohmann::json &j, const UpdateStatusMessage &m) {
|
||||
j["op"] = GatewayOp::UpdateStatus;
|
||||
j["op"] = GatewayOp::PresenceUpdate;
|
||||
j["d"] = nlohmann::json::object();
|
||||
j["d"]["since"] = m.Since;
|
||||
j["d"]["activities"] = m.Activities;
|
||||
@@ -119,6 +119,92 @@ void to_json(nlohmann::json &j, const UpdateStatusMessage &m) {
|
||||
}
|
||||
}
|
||||
|
||||
void to_json(nlohmann::json &j, const RequestGuildMembersMessage &m) {
|
||||
j["op"] = GatewayOp::RequestGuildMembers;
|
||||
j["d"] = nlohmann::json::object();
|
||||
j["d"]["guild_id"] = m.GuildID;
|
||||
j["d"]["presences"] = m.Presences;
|
||||
j["d"]["user_ids"] = m.UserIDs;
|
||||
}
|
||||
|
||||
void from_json(const nlohmann::json &j, ReadStateEntry &m) {
|
||||
JS_ON("mention_count", m.MentionCount);
|
||||
JS_ON("last_message_id", m.LastMessageID);
|
||||
JS_D("id", m.ID);
|
||||
}
|
||||
|
||||
void to_json(nlohmann::json &j, const ReadStateEntry &m) {
|
||||
j["channel_id"] = m.ID;
|
||||
j["message_id"] = m.LastMessageID;
|
||||
}
|
||||
|
||||
void from_json(const nlohmann::json &j, ReadStateData &m) {
|
||||
JS_ON("version", m.Version);
|
||||
JS_ON("partial", m.IsPartial);
|
||||
JS_ON("entries", m.Entries);
|
||||
}
|
||||
|
||||
void from_json(const nlohmann::json &j, UserGuildSettingsChannelOverride &m) {
|
||||
JS_D("muted", m.Muted);
|
||||
JS_D("message_notifications", m.MessageNotifications);
|
||||
JS_D("collapsed", m.Collapsed);
|
||||
JS_D("channel_id", m.ChannelID);
|
||||
JS_N("mute_config", m.MuteConfig);
|
||||
}
|
||||
|
||||
void to_json(nlohmann::json &j, const UserGuildSettingsChannelOverride &m) {
|
||||
j["channel_id"] = m.ChannelID;
|
||||
j["collapsed"] = m.Collapsed;
|
||||
j["message_notifications"] = m.MessageNotifications;
|
||||
j["mute_config"] = m.MuteConfig;
|
||||
j["muted"] = m.Muted;
|
||||
}
|
||||
|
||||
void from_json(const nlohmann::json &j, MuteConfigData &m) {
|
||||
JS_ON("end_time", m.EndTime);
|
||||
JS_ON("selected_time_window", m.SelectedTimeWindow);
|
||||
}
|
||||
|
||||
void to_json(nlohmann::json &j, const MuteConfigData &m) {
|
||||
if (m.EndTime.has_value())
|
||||
j["end_time"] = *m.EndTime;
|
||||
else
|
||||
j["end_time"] = nullptr;
|
||||
j["selected_time_window"] = m.SelectedTimeWindow;
|
||||
}
|
||||
|
||||
void from_json(const nlohmann::json &j, UserGuildSettingsEntry &m) {
|
||||
JS_D("version", m.Version);
|
||||
JS_D("suppress_roles", m.SuppressRoles);
|
||||
JS_D("suppress_everyone", m.SuppressEveryone);
|
||||
JS_D("muted", m.Muted);
|
||||
JS_D("mobile_push", m.MobilePush);
|
||||
JS_D("message_notifications", m.MessageNotifications);
|
||||
JS_D("hide_muted_channels", m.HideMutedChannels);
|
||||
JS_N("guild_id", m.GuildID);
|
||||
JS_D("channel_overrides", m.ChannelOverrides);
|
||||
JS_N("mute_config", m.MuteConfig);
|
||||
}
|
||||
|
||||
void to_json(nlohmann::json &j, const UserGuildSettingsEntry &m) {
|
||||
j["channel_overrides"] = m.ChannelOverrides;
|
||||
j["guild_id"] = m.GuildID;
|
||||
j["hide_muted_channels"] = m.HideMutedChannels;
|
||||
j["message_notifications"] = m.MessageNotifications;
|
||||
j["mobile_push"] = m.MobilePush;
|
||||
j["mute_config"] = m.MuteConfig;
|
||||
j["muted"] = m.Muted;
|
||||
j["suppress_everyone"] = m.SuppressEveryone;
|
||||
j["suppress_roles"] = m.SuppressRoles;
|
||||
j["version"] = m.Version;
|
||||
}
|
||||
|
||||
void from_json(const nlohmann::json &j, UserGuildSettingsData &m) {
|
||||
JS_D("version", m.Version);
|
||||
JS_D("partial", m.IsPartial);
|
||||
JS_D("entries", m.Entries);
|
||||
}
|
||||
|
||||
void from_json(const nlohmann::json &j, ReadyEventData &m) {
|
||||
JS_D("v", m.GatewayVersion);
|
||||
JS_D("user", m.SelfUser);
|
||||
@@ -132,6 +218,8 @@ void from_json(const nlohmann::json &j, ReadyEventData &m) {
|
||||
JS_ON("merged_members", m.MergedMembers);
|
||||
JS_O("relationships", m.Relationships);
|
||||
JS_O("guild_join_requests", m.GuildJoinRequests);
|
||||
JS_O("read_state", m.ReadState);
|
||||
JS_D("user_guild_settings", m.GuildSettings);
|
||||
}
|
||||
|
||||
void from_json(const nlohmann::json &j, MergedPresence &m) {
|
||||
@@ -163,7 +251,7 @@ void to_json(nlohmann::json &j, const IdentifyProperties &m) {
|
||||
j["referring_domain_current"] = m.ReferringDomainCurrent;
|
||||
j["release_channel"] = m.ReleaseChannel;
|
||||
j["client_build_number"] = m.ClientBuildNumber;
|
||||
if (m.ClientEventSource == "")
|
||||
if (m.ClientEventSource.empty())
|
||||
j["client_event_source"] = nullptr;
|
||||
else
|
||||
j["client_event_source"] = m.ClientEventSource;
|
||||
@@ -174,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) {
|
||||
@@ -202,7 +291,7 @@ void to_json(nlohmann::json &j, const CreateMessageObject &m) {
|
||||
}
|
||||
|
||||
void to_json(nlohmann::json &j, const MessageEditObject &m) {
|
||||
if (m.Content.size() > 0)
|
||||
if (!m.Content.empty())
|
||||
j["content"] = m.Content;
|
||||
|
||||
// todo EmbedData to_json
|
||||
@@ -532,3 +621,22 @@ void to_json(nlohmann::json &j, const ModifyChannelObject &m) {
|
||||
JS_IF("archived", m.Archived);
|
||||
JS_IF("locked", m.Locked);
|
||||
}
|
||||
|
||||
void from_json(const nlohmann::json &j, MessageAckData &m) {
|
||||
// JS_D("version", m.Version);
|
||||
JS_D("message_id", m.MessageID);
|
||||
JS_D("channel_id", m.ChannelID);
|
||||
}
|
||||
|
||||
void to_json(nlohmann::json &j, const AckBulkData &m) {
|
||||
j["read_states"] = m.ReadStates;
|
||||
}
|
||||
|
||||
void from_json(const nlohmann::json &j, UserGuildSettingsUpdateData &m) {
|
||||
m.Settings = j;
|
||||
}
|
||||
|
||||
void from_json(const nlohmann::json &j, GuildMembersChunkData &m) {
|
||||
JS_D("members", m.Members);
|
||||
JS_D("guild_id", m.GuildID);
|
||||
}
|
||||
|
||||
@@ -24,16 +24,35 @@
|
||||
// most stuff below should just be objects that get processed and thrown away immediately
|
||||
|
||||
enum class GatewayOp : int {
|
||||
Event = 0,
|
||||
Dispatch = 0,
|
||||
Heartbeat = 1,
|
||||
Identify = 2,
|
||||
UpdateStatus = 3,
|
||||
PresenceUpdate = 3,
|
||||
VoiceStateUpdate = 4,
|
||||
VoiceServerPing = 5,
|
||||
Resume = 6,
|
||||
Reconnect = 7,
|
||||
RequestGuildMembers = 8,
|
||||
InvalidSession = 9,
|
||||
Hello = 10,
|
||||
HeartbeatAck = 11,
|
||||
LazyLoadRequest = 14,
|
||||
// 12 unused
|
||||
CallConnect = 13,
|
||||
GuildSubscriptions = 14,
|
||||
LobbyConnect = 15,
|
||||
LobbyDisconnect = 16,
|
||||
LobbyVoiceStatesUpdate = 17,
|
||||
StreamCreate = 18,
|
||||
StreamDelete = 19,
|
||||
StreamWatch = 20,
|
||||
StreamPing = 21,
|
||||
StreamSetPaused = 22,
|
||||
// 23 unused
|
||||
RequestGuildApplicationCommands = 24,
|
||||
EmbeddedActivityLaunch = 25,
|
||||
EmbeddedActivityClose = 26,
|
||||
EmbeddedActivityUpdate = 27,
|
||||
RequestForumUnreads = 28,
|
||||
};
|
||||
|
||||
enum class GatewayEvent : int {
|
||||
@@ -78,6 +97,9 @@ enum class GatewayEvent : int {
|
||||
THREAD_MEMBER_UPDATE,
|
||||
THREAD_MEMBERS_UPDATE,
|
||||
THREAD_MEMBER_LIST_UPDATE,
|
||||
MESSAGE_ACK,
|
||||
USER_GUILD_SETTINGS_UPDATE,
|
||||
GUILD_MEMBERS_CHUNK,
|
||||
};
|
||||
|
||||
enum class GatewayCloseCode : uint16_t {
|
||||
@@ -175,7 +197,7 @@ struct GuildMemberListUpdateMessage {
|
||||
std::string HoistedRole; // null
|
||||
bool IsDefeaned;
|
||||
|
||||
GuildMember GetAsMemberData() const;
|
||||
[[nodiscard]] GuildMember GetAsMemberData() const;
|
||||
|
||||
friend void from_json(const nlohmann::json &j, MemberItem &m);
|
||||
|
||||
@@ -224,6 +246,67 @@ struct UpdateStatusMessage {
|
||||
friend void to_json(nlohmann::json &j, const UpdateStatusMessage &m);
|
||||
};
|
||||
|
||||
struct RequestGuildMembersMessage {
|
||||
Snowflake GuildID;
|
||||
bool Presences;
|
||||
std::vector<Snowflake> UserIDs;
|
||||
|
||||
friend void to_json(nlohmann::json &j, const RequestGuildMembersMessage &m);
|
||||
};
|
||||
|
||||
struct ReadStateEntry {
|
||||
int MentionCount;
|
||||
Snowflake LastMessageID;
|
||||
Snowflake ID;
|
||||
// std::string LastPinTimestamp; iso
|
||||
|
||||
friend void from_json(const nlohmann::json &j, ReadStateEntry &m);
|
||||
friend void to_json(nlohmann::json &j, const ReadStateEntry &m);
|
||||
};
|
||||
|
||||
struct ReadStateData {
|
||||
int Version;
|
||||
bool IsPartial;
|
||||
std::vector<ReadStateEntry> Entries;
|
||||
|
||||
friend void from_json(const nlohmann::json &j, ReadStateData &m);
|
||||
};
|
||||
|
||||
struct UserGuildSettingsChannelOverride {
|
||||
bool Muted;
|
||||
MuteConfigData MuteConfig;
|
||||
int MessageNotifications;
|
||||
bool Collapsed;
|
||||
Snowflake ChannelID;
|
||||
|
||||
friend void from_json(const nlohmann::json &j, UserGuildSettingsChannelOverride &m);
|
||||
friend void to_json(nlohmann::json &j, const UserGuildSettingsChannelOverride &m);
|
||||
};
|
||||
|
||||
struct UserGuildSettingsEntry {
|
||||
int Version;
|
||||
bool SuppressRoles;
|
||||
bool SuppressEveryone;
|
||||
bool Muted;
|
||||
MuteConfigData MuteConfig;
|
||||
bool MobilePush;
|
||||
int MessageNotifications;
|
||||
bool HideMutedChannels;
|
||||
Snowflake GuildID;
|
||||
std::vector<UserGuildSettingsChannelOverride> ChannelOverrides;
|
||||
|
||||
friend void from_json(const nlohmann::json &j, UserGuildSettingsEntry &m);
|
||||
friend void to_json(nlohmann::json &j, const UserGuildSettingsEntry &m);
|
||||
};
|
||||
|
||||
struct UserGuildSettingsData {
|
||||
int Version;
|
||||
bool IsPartial;
|
||||
std::vector<UserGuildSettingsEntry> Entries;
|
||||
|
||||
friend void from_json(const nlohmann::json &j, UserGuildSettingsData &m);
|
||||
};
|
||||
|
||||
struct ReadyEventData {
|
||||
int GatewayVersion;
|
||||
UserData SelfUser;
|
||||
@@ -239,6 +322,8 @@ struct ReadyEventData {
|
||||
std::optional<std::vector<std::vector<GuildMember>>> MergedMembers;
|
||||
std::optional<std::vector<RelationshipData>> Relationships;
|
||||
std::optional<std::vector<GuildApplicationData>> GuildJoinRequests;
|
||||
ReadStateData ReadState;
|
||||
UserGuildSettingsData GuildSettings;
|
||||
// std::vector<Unknown> ConnectedAccounts; // opt
|
||||
// std::map<std::string, Unknown> Consents; // opt
|
||||
// std::vector<Unknown> Experiments; // opt
|
||||
@@ -297,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);
|
||||
};
|
||||
@@ -745,3 +831,36 @@ struct ModifyChannelObject {
|
||||
|
||||
friend void to_json(nlohmann::json &j, const ModifyChannelObject &m);
|
||||
};
|
||||
|
||||
struct MessageAckData {
|
||||
// int Version; // what is this ?!?!?!!?
|
||||
Snowflake MessageID;
|
||||
Snowflake ChannelID;
|
||||
|
||||
friend void from_json(const nlohmann::json &j, MessageAckData &m);
|
||||
};
|
||||
|
||||
struct AckBulkData {
|
||||
std::vector<ReadStateEntry> ReadStates;
|
||||
|
||||
friend void to_json(nlohmann::json &j, const AckBulkData &m);
|
||||
};
|
||||
|
||||
struct UserGuildSettingsUpdateData {
|
||||
UserGuildSettingsEntry Settings;
|
||||
|
||||
friend void from_json(const nlohmann::json &j, UserGuildSettingsUpdateData &m);
|
||||
};
|
||||
|
||||
struct GuildMembersChunkData {
|
||||
/*
|
||||
not needed so not deserialized
|
||||
int ChunkCount;
|
||||
int ChunkIndex;
|
||||
std::vector<?> NotFound;
|
||||
*/
|
||||
Snowflake GuildID;
|
||||
std::vector<GuildMember> Members;
|
||||
|
||||
friend void from_json(const nlohmann::json &j, GuildMembersChunkData &m);
|
||||
};
|
||||
|
||||
@@ -3,19 +3,19 @@
|
||||
#include "user.hpp"
|
||||
|
||||
enum class RelationshipType {
|
||||
None = 0,
|
||||
Friend = 1,
|
||||
Blocked = 2,
|
||||
PendingIncoming = 3,
|
||||
PendingOutgoing = 4,
|
||||
Implicit = 5,
|
||||
None = 0,
|
||||
Friend = 1,
|
||||
Blocked = 2,
|
||||
PendingIncoming = 3,
|
||||
PendingOutgoing = 4,
|
||||
Implicit = 5,
|
||||
};
|
||||
|
||||
struct RelationshipData {
|
||||
// Snowflake UserID; this is the same as ID apparently but it looks new so i wont touch it
|
||||
RelationshipType Type;
|
||||
Snowflake ID;
|
||||
// Unknown Nickname; // null
|
||||
// Unknown Nickname; // null
|
||||
|
||||
friend void from_json(const nlohmann::json &j, RelationshipData &m);
|
||||
friend void from_json(const nlohmann::json &j, RelationshipData &m);
|
||||
};
|
||||
|
||||
@@ -12,3 +12,11 @@ void from_json(const nlohmann::json &j, RoleData &m) {
|
||||
JS_D("managed", m.IsManaged);
|
||||
JS_D("mentionable", m.IsMentionable);
|
||||
}
|
||||
|
||||
bool RoleData::HasColor() const noexcept {
|
||||
return Color != 0;
|
||||
}
|
||||
|
||||
Glib::ustring RoleData::GetEscapedName() const {
|
||||
return Glib::Markup::escape_text(Name);
|
||||
}
|
||||
|
||||
@@ -16,5 +16,8 @@ struct RoleData {
|
||||
bool IsManaged;
|
||||
bool IsMentionable;
|
||||
|
||||
[[nodiscard]] bool HasColor() const noexcept;
|
||||
[[nodiscard]] Glib::ustring GetEscapedName() const;
|
||||
|
||||
friend void from_json(const nlohmann::json &j, RoleData &m);
|
||||
};
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
#include "snowflake.hpp"
|
||||
#include "util.hpp"
|
||||
#include <chrono>
|
||||
#include <ctime>
|
||||
#include <iomanip>
|
||||
#include <chrono>
|
||||
|
||||
constexpr static uint64_t DiscordEpochSeconds = 1420070400;
|
||||
|
||||
@@ -14,17 +15,17 @@ Snowflake::Snowflake(uint64_t n)
|
||||
: m_num(n) {}
|
||||
|
||||
Snowflake::Snowflake(const std::string &str) {
|
||||
if (str.size())
|
||||
if (!str.empty())
|
||||
m_num = std::stoull(str);
|
||||
else
|
||||
m_num = Invalid;
|
||||
}
|
||||
Snowflake::Snowflake(const Glib::ustring &str) {
|
||||
if (str.size())
|
||||
if (!str.empty())
|
||||
m_num = std::strtoull(str.c_str(), nullptr, 10);
|
||||
else
|
||||
m_num = Invalid;
|
||||
};
|
||||
}
|
||||
|
||||
Snowflake Snowflake::FromNow() {
|
||||
using namespace std::chrono;
|
||||
@@ -38,18 +39,26 @@ Snowflake Snowflake::FromNow() {
|
||||
return snowflake;
|
||||
}
|
||||
|
||||
Snowflake Snowflake::FromISO8601(std::string_view ts) {
|
||||
int yr, mon, day, hr, min, sec, tzhr, tzmin;
|
||||
float milli;
|
||||
if (std::sscanf(ts.data(), "%d-%d-%dT%d:%d:%d%f+%d:%d",
|
||||
&yr, &mon, &day, &hr, &min, &sec, &milli, &tzhr, &tzmin) != 9) return Snowflake::Invalid;
|
||||
const auto epoch = util::TimeToEpoch(yr, mon, day, hr, min, sec);
|
||||
if (epoch < DiscordEpochSeconds) return Snowflake::Invalid;
|
||||
return SecondsInterval * (epoch - DiscordEpochSeconds) + static_cast<uint64_t>(milli * static_cast<float>(SecondsInterval));
|
||||
}
|
||||
|
||||
bool Snowflake::IsValid() const {
|
||||
return m_num != Invalid;
|
||||
}
|
||||
|
||||
std::string Snowflake::GetLocalTimestamp() const {
|
||||
Glib::ustring Snowflake::GetLocalTimestamp() const {
|
||||
const time_t secs_since_epoch = (m_num / SecondsInterval) + DiscordEpochSeconds;
|
||||
const std::tm tm = *localtime(&secs_since_epoch);
|
||||
std::stringstream ss;
|
||||
const static std::locale locale("");
|
||||
ss.imbue(locale);
|
||||
ss << std::put_time(&tm, "%X %x");
|
||||
return ss.str();
|
||||
std::array<char, 256> tmp {};
|
||||
std::strftime(tmp.data(), sizeof(tmp), "%X %x", &tm);
|
||||
return tmp.data();
|
||||
}
|
||||
|
||||
void from_json(const nlohmann::json &j, Snowflake &s) {
|
||||
|
||||
@@ -10,9 +10,10 @@ struct Snowflake {
|
||||
Snowflake(const Glib::ustring &str);
|
||||
|
||||
static Snowflake FromNow(); // not thread safe
|
||||
static Snowflake FromISO8601(std::string_view ts);
|
||||
|
||||
bool IsValid() const;
|
||||
std::string GetLocalTimestamp() const;
|
||||
[[nodiscard]] bool IsValid() const;
|
||||
[[nodiscard]] Glib::ustring GetLocalTimestamp() const;
|
||||
|
||||
bool operator==(const Snowflake &s) const noexcept {
|
||||
return m_num == s.m_num;
|
||||
@@ -26,7 +27,7 @@ struct Snowflake {
|
||||
return m_num;
|
||||
}
|
||||
|
||||
const static Snowflake Invalid; // makes sense to me
|
||||
const static Snowflake Invalid; // makes sense to me
|
||||
const static uint64_t SecondsInterval = 4194304000ULL; // the "difference" between two snowflakes one second apart
|
||||
|
||||
friend void from_json(const nlohmann::json &j, Snowflake &s);
|
||||
|
||||
@@ -22,15 +22,6 @@ void from_json(const nlohmann::json &j, StickerData &m) {
|
||||
JS_D("format_type", m.FormatType);
|
||||
}
|
||||
|
||||
std::string StickerData::GetURL() const {
|
||||
if (!AssetHash.has_value()) return "";
|
||||
if (FormatType == StickerFormatType::PNG || FormatType == StickerFormatType::APNG)
|
||||
return "https://media.discordapp.net/stickers/" + std::to_string(ID) + "/" + *AssetHash + ".png?size=256";
|
||||
else if (FormatType == StickerFormatType::LOTTIE)
|
||||
return "https://media.discordapp.net/stickers/" + std::to_string(ID) + "/" + *AssetHash + ".json";
|
||||
return "";
|
||||
}
|
||||
|
||||
void to_json(nlohmann::json &j, const StickerItem &m) {
|
||||
j["id"] = m.ID;
|
||||
j["name"] = m.Name;
|
||||
|
||||
@@ -24,8 +24,6 @@ struct StickerData {
|
||||
|
||||
friend void to_json(nlohmann::json &j, const StickerData &m);
|
||||
friend void from_json(const nlohmann::json &j, StickerData &m);
|
||||
|
||||
std::string GetURL() const;
|
||||
};
|
||||
|
||||
struct StickerItem {
|
||||
@@ -36,5 +34,5 @@ struct StickerItem {
|
||||
friend void to_json(nlohmann::json &j, const StickerItem &m);
|
||||
friend void from_json(const nlohmann::json &j, StickerItem &m);
|
||||
|
||||
std::string GetURL() const;
|
||||
[[nodiscard]] std::string GetURL() const;
|
||||
};
|
||||
|
||||
@@ -13,18 +13,6 @@ Store::Store(bool mem_store)
|
||||
return;
|
||||
}
|
||||
|
||||
m_db.Execute(R"(
|
||||
PRAGMA writable_schema = 1;
|
||||
DELETE FROM sqlite_master;
|
||||
PRAGMA writable_schema = 0;
|
||||
VACUUM;
|
||||
PRAGMA integrity_check;
|
||||
)");
|
||||
if (!m_db.OK()) {
|
||||
fprintf(stderr, "failed to clear database: %s\n", m_db.ErrStr());
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_db.Execute("PRAGMA journal_mode = WAL") != SQLITE_OK) {
|
||||
fprintf(stderr, "enabling write-ahead-log failed: %s\n", m_db.ErrStr());
|
||||
return;
|
||||
@@ -265,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;
|
||||
|
||||
@@ -571,6 +567,40 @@ std::vector<ChannelData> Store::GetActiveThreads(Snowflake channel_id) const {
|
||||
return ret;
|
||||
}
|
||||
|
||||
std::vector<Snowflake> Store::GetChannelIDsWithParentID(Snowflake channel_id) const {
|
||||
auto &s = m_stmt_get_chan_ids_parent;
|
||||
|
||||
s->Bind(1, channel_id);
|
||||
|
||||
std::vector<Snowflake> ret;
|
||||
while (s->FetchOne()) {
|
||||
Snowflake x;
|
||||
s->Get(0, x);
|
||||
ret.push_back(x);
|
||||
}
|
||||
|
||||
s->Reset();
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
std::unordered_set<Snowflake> Store::GetMembersInGuild(Snowflake guild_id) const {
|
||||
auto &s = m_stmt_get_guild_member_ids;
|
||||
|
||||
s->Bind(1, guild_id);
|
||||
|
||||
std::unordered_set<Snowflake> ret;
|
||||
while (s->FetchOne()) {
|
||||
Snowflake x;
|
||||
s->Get(0, x);
|
||||
ret.insert(x);
|
||||
}
|
||||
|
||||
s->Reset();
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
void Store::AddReaction(const MessageReactionAddObject &data, bool byself) {
|
||||
auto &s = m_stmt_add_reaction;
|
||||
|
||||
@@ -629,6 +659,7 @@ std::optional<ChannelData> Store::GetChannel(Snowflake id) const {
|
||||
s->Get(6, r.IsNSFW);
|
||||
s->Get(7, r.LastMessageID);
|
||||
s->Get(10, r.RateLimitPerUser);
|
||||
s->Get(11, r.Icon);
|
||||
s->Get(12, r.OwnerID);
|
||||
s->Get(14, r.ParentID);
|
||||
if (!s->IsNull(16)) {
|
||||
@@ -715,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();
|
||||
|
||||
@@ -765,6 +797,16 @@ std::optional<GuildData> Store::GetGuild(Snowflake id) const {
|
||||
s->Reset();
|
||||
}
|
||||
|
||||
{
|
||||
auto &s = m_stmt_get_guild_roles;
|
||||
s->Bind(1, id);
|
||||
r.Roles.emplace();
|
||||
while (s->FetchOne()) {
|
||||
r.Roles->push_back(GetRoleBound(s));
|
||||
}
|
||||
s->Reset();
|
||||
}
|
||||
|
||||
return r;
|
||||
}
|
||||
|
||||
@@ -785,8 +827,8 @@ std::optional<GuildMember> Store::GetGuildMember(Snowflake guild_id, Snowflake u
|
||||
s->Get(2, r.Nickname);
|
||||
s->Get(3, r.JoinedAt);
|
||||
s->Get(4, r.PremiumSince);
|
||||
//s->Get(5, r.IsDeafened);
|
||||
//s->Get(6, r.IsMuted);
|
||||
// s->Get(5, r.IsDeafened);
|
||||
// s->Get(6, r.IsMuted);
|
||||
s->Get(7, r.Avatar);
|
||||
s->Get(8, r.IsPending);
|
||||
|
||||
@@ -846,8 +888,8 @@ Message Store::GetMessageBound(std::unique_ptr<Statement> &s) const {
|
||||
s->Get(4, r.Content);
|
||||
s->Get(5, r.Timestamp);
|
||||
s->Get(6, r.EditedTimestamp);
|
||||
//s->Get(7, r.IsTTS);
|
||||
//s->Get(8, r.DoesMentionEveryone);
|
||||
// s->Get(7, r.IsTTS);
|
||||
// s->Get(8, r.DoesMentionEveryone);
|
||||
s->GetJSON(9, r.Embeds);
|
||||
s->Get(10, r.IsPinned);
|
||||
s->Get(11, r.WebhookID);
|
||||
@@ -873,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();
|
||||
}
|
||||
|
||||
{
|
||||
@@ -961,10 +1010,18 @@ std::optional<RoleData> Store::GetRole(Snowflake id) const {
|
||||
return {};
|
||||
}
|
||||
|
||||
auto role = GetRoleBound(s);
|
||||
|
||||
s->Reset();
|
||||
|
||||
return role;
|
||||
}
|
||||
|
||||
RoleData Store::GetRoleBound(std::unique_ptr<Statement> &s) {
|
||||
RoleData r;
|
||||
|
||||
r.ID = id;
|
||||
//s->Get(1, guild id);
|
||||
s->Get(0, r.ID);
|
||||
// s->Get(1, guild id);
|
||||
s->Get(2, r.Name);
|
||||
s->Get(3, r.Color);
|
||||
s->Get(4, r.IsHoisted);
|
||||
@@ -973,8 +1030,6 @@ std::optional<RoleData> Store::GetRole(Snowflake id) const {
|
||||
s->Get(7, r.IsManaged);
|
||||
s->Get(8, r.IsMentionable);
|
||||
|
||||
s->Reset();
|
||||
|
||||
return r;
|
||||
}
|
||||
|
||||
@@ -1039,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;
|
||||
@@ -1483,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;
|
||||
}
|
||||
|
||||
@@ -1572,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());
|
||||
@@ -1644,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()) {
|
||||
@@ -1726,6 +1781,14 @@ bool Store::CreateStatements() {
|
||||
return false;
|
||||
}
|
||||
|
||||
m_stmt_get_guild_roles = std::make_unique<Statement>(m_db, R"(
|
||||
SELECT * FROM roles WHERE guild = ?
|
||||
)");
|
||||
if (!m_stmt_get_guild_roles->OK()) {
|
||||
fprintf(stderr, "failed to prepare get guild roles statement: %s\n", m_db.ErrStr());
|
||||
return false;
|
||||
}
|
||||
|
||||
m_stmt_set_emoji = std::make_unique<Statement>(m_db, R"(
|
||||
REPLACE INTO emojis VALUES (
|
||||
?, ?, ?, ?, ?, ?, ?
|
||||
@@ -1827,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 (
|
||||
?, ?
|
||||
@@ -2096,10 +2173,42 @@ bool Store::CreateStatements() {
|
||||
return false;
|
||||
}
|
||||
|
||||
m_stmt_get_chan_ids_parent = std::make_unique<Statement>(m_db, R"(
|
||||
SELECT id FROM channels WHERE parent_id = ?
|
||||
)");
|
||||
if (!m_stmt_get_chan_ids_parent->OK()) {
|
||||
fprintf(stderr, "failed to prepare get channel ids for parent statement: %s\n", m_db.ErrStr());
|
||||
return false;
|
||||
}
|
||||
|
||||
m_stmt_get_guild_member_ids = std::make_unique<Statement>(m_db, R"(
|
||||
SELECT user_id FROM members WHERE guild_id = ?
|
||||
)");
|
||||
if (!m_stmt_get_guild_member_ids->OK()) {
|
||||
fprintf(stderr, "failed to prepare get guild member ids statement: %s\n", m_db.ErrStr());
|
||||
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;
|
||||
}
|
||||
|
||||
Store::Database::Database(const char *path) {
|
||||
if (path != ":memory:"s) {
|
||||
std::error_code ec;
|
||||
if (std::filesystem::exists(path, ec) && !std::filesystem::remove(path, ec)) {
|
||||
fprintf(stderr, "the database could not be removed. the database may be corrupted as a result\n");
|
||||
}
|
||||
}
|
||||
|
||||
m_err = sqlite3_open(path, &m_db);
|
||||
}
|
||||
|
||||
@@ -2179,11 +2288,11 @@ int Store::Statement::Bind(int index, Snowflake id) {
|
||||
|
||||
int Store::Statement::Bind(int index, const char *str, size_t len) {
|
||||
if (len == -1) len = strlen(str);
|
||||
return m_db->SetError(sqlite3_bind_blob(m_stmt, index, str, len, SQLITE_TRANSIENT));
|
||||
return m_db->SetError(sqlite3_bind_blob(m_stmt, index, str, static_cast<int>(len), SQLITE_TRANSIENT));
|
||||
}
|
||||
|
||||
int Store::Statement::Bind(int index, const std::string &str) {
|
||||
return m_db->SetError(sqlite3_bind_blob(m_stmt, index, str.c_str(), str.size(), SQLITE_TRANSIENT));
|
||||
return m_db->SetError(sqlite3_bind_blob(m_stmt, index, str.c_str(), static_cast<int>(str.size()), SQLITE_TRANSIENT));
|
||||
}
|
||||
|
||||
int Store::Statement::Bind(int index) {
|
||||
|
||||
@@ -43,6 +43,9 @@ public:
|
||||
std::vector<Message> GetMessagesBefore(Snowflake channel_id, Snowflake message_id, size_t limit) const;
|
||||
std::vector<Message> GetPinnedMessages(Snowflake channel_id) const;
|
||||
std::vector<ChannelData> GetActiveThreads(Snowflake channel_id) const; // public
|
||||
std::vector<Snowflake> GetChannelIDsWithParentID(Snowflake channel_id) const;
|
||||
std::unordered_set<Snowflake> GetMembersInGuild(Snowflake guild_id) const;
|
||||
// ^ not the same as GetUsersInGuild since users in a guild may include users who do not have retrieved member data
|
||||
|
||||
void AddReaction(const MessageReactionAddObject &data, bool byself);
|
||||
void RemoveReaction(const MessageReactionRemoveObject &data, bool byself);
|
||||
@@ -51,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;
|
||||
@@ -98,7 +102,7 @@ private:
|
||||
~Statement();
|
||||
Statement &operator=(Statement &other) = delete;
|
||||
|
||||
bool OK() const;
|
||||
[[nodiscard]] bool OK() const;
|
||||
|
||||
int Bind(int index, Snowflake id);
|
||||
int Bind(int index, const char *str, size_t len = -1);
|
||||
@@ -221,7 +225,7 @@ private:
|
||||
*first++ = id.get<T>();
|
||||
}
|
||||
|
||||
bool IsNull(int index) const;
|
||||
[[nodiscard]] bool IsNull(int index) const;
|
||||
int Step();
|
||||
bool Insert();
|
||||
bool FetchOne();
|
||||
@@ -235,6 +239,7 @@ private:
|
||||
};
|
||||
|
||||
Message GetMessageBound(std::unique_ptr<Statement> &stmt) const;
|
||||
static RoleData GetRoleBound(std::unique_ptr<Statement> &stmt);
|
||||
|
||||
void SetMessageInteractionPair(Snowflake message_id, const MessageInteractionData &interaction);
|
||||
|
||||
@@ -264,6 +269,7 @@ private:
|
||||
STMT(get_member);
|
||||
STMT(set_role);
|
||||
STMT(get_role);
|
||||
STMT(get_guild_roles);
|
||||
STMT(set_emoji);
|
||||
STMT(get_emoji);
|
||||
STMT(set_perm);
|
||||
@@ -275,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);
|
||||
@@ -298,5 +305,8 @@ private:
|
||||
STMT(add_reaction);
|
||||
STMT(sub_reaction);
|
||||
STMT(get_reactions);
|
||||
STMT(get_chan_ids_parent);
|
||||
STMT(get_guild_member_ids);
|
||||
STMT(clr_role);
|
||||
#undef STMT
|
||||
};
|
||||
|
||||
@@ -1,37 +1,60 @@
|
||||
#include "user.hpp"
|
||||
#include "abaddon.hpp"
|
||||
|
||||
bool UserData::IsABot() const noexcept {
|
||||
return IsBot.has_value() && *IsBot;
|
||||
}
|
||||
|
||||
bool UserData::IsDeleted() const {
|
||||
return Discriminator == "0000";
|
||||
}
|
||||
|
||||
bool UserData::HasAvatar() const {
|
||||
return Avatar.size() > 0;
|
||||
return !Avatar.empty();
|
||||
}
|
||||
|
||||
bool UserData::HasAnimatedAvatar() const {
|
||||
return Avatar.size() > 0 && Avatar[0] == 'a' && Avatar[1] == '_';
|
||||
bool UserData::HasAnimatedAvatar() const noexcept {
|
||||
return !Avatar.empty() && Avatar[0] == 'a' && Avatar[1] == '_';
|
||||
}
|
||||
|
||||
std::string UserData::GetAvatarURL(Snowflake guild_id, std::string ext, std::string size) const {
|
||||
bool UserData::HasAnimatedAvatar(Snowflake guild_id) const {
|
||||
const auto member = Abaddon::Get().GetDiscordClient().GetMember(ID, guild_id);
|
||||
if (member.has_value() && member->Avatar.has_value())
|
||||
if (member.has_value() && member->Avatar.has_value() && member->Avatar.value()[0] == 'a' && member->Avatar.value()[1] == '_')
|
||||
return true;
|
||||
else if (member.has_value() && !member->Avatar.has_value())
|
||||
return HasAnimatedAvatar();
|
||||
return false;
|
||||
}
|
||||
|
||||
bool UserData::HasAnimatedAvatar(const std::optional<Snowflake> &guild_id) const {
|
||||
if (guild_id.has_value())
|
||||
return HasAnimatedAvatar(*guild_id);
|
||||
else
|
||||
return HasAnimatedAvatar();
|
||||
}
|
||||
|
||||
std::string UserData::GetAvatarURL(Snowflake guild_id, const std::string &ext, std::string size) const {
|
||||
const auto member = Abaddon::Get().GetDiscordClient().GetMember(ID, guild_id);
|
||||
if (member.has_value() && member->Avatar.has_value()) {
|
||||
if (ext == "gif" && !(member->Avatar.value()[0] == 'a' && member->Avatar.value()[1] == '_'))
|
||||
return GetAvatarURL(ext, size);
|
||||
return "https://cdn.discordapp.com/guilds/" +
|
||||
std::to_string(guild_id) + "/users/" + std::to_string(ID) +
|
||||
"/avatars/" + *member->Avatar + "." +
|
||||
ext + "?" + "size=" + size;
|
||||
else
|
||||
} else {
|
||||
return GetAvatarURL(ext, size);
|
||||
}
|
||||
}
|
||||
|
||||
std::string UserData::GetAvatarURL(const std::optional<Snowflake> &guild_id, std::string ext, std::string size) const {
|
||||
std::string UserData::GetAvatarURL(const std::optional<Snowflake> &guild_id, const std::string &ext, std::string size) const {
|
||||
if (guild_id.has_value())
|
||||
return GetAvatarURL(*guild_id, ext, size);
|
||||
else
|
||||
return GetAvatarURL(ext, size);
|
||||
}
|
||||
|
||||
std::string UserData::GetAvatarURL(std::string ext, std::string size) const {
|
||||
std::string UserData::GetAvatarURL(const std::string &ext, std::string size) const {
|
||||
if (HasAvatar())
|
||||
return "https://cdn.discordapp.com/avatars/" + std::to_string(ID) + "/" + Avatar + "." + ext + "?size=" + size;
|
||||
else
|
||||
@@ -88,7 +111,7 @@ void to_json(nlohmann::json &j, const UserData &m) {
|
||||
j["id"] = m.ID;
|
||||
j["username"] = m.Username;
|
||||
j["discriminator"] = m.Discriminator;
|
||||
if (m.Avatar == "")
|
||||
if (m.Avatar.empty())
|
||||
j["avatar"] = nullptr;
|
||||
else
|
||||
j["avatar"] = m.Avatar;
|
||||
|
||||
@@ -60,20 +60,23 @@ struct UserData {
|
||||
friend void to_json(nlohmann::json &j, const UserData &m);
|
||||
void update_from_json(const nlohmann::json &j);
|
||||
|
||||
bool IsDeleted() const;
|
||||
bool HasAvatar() const;
|
||||
bool HasAnimatedAvatar() const;
|
||||
std::string GetAvatarURL(Snowflake guild_id, std::string ext = "png", std::string size = "32") const;
|
||||
std::string GetAvatarURL(const std::optional<Snowflake> &guild_id, std::string ext = "png", std::string size = "32") const;
|
||||
std::string GetAvatarURL(std::string ext = "png", std::string size = "32") const;
|
||||
std::string GetDefaultAvatarURL() const;
|
||||
Snowflake GetHoistedRole(Snowflake guild_id, bool with_color = false) const;
|
||||
std::string GetMention() const;
|
||||
std::string GetEscapedName() const;
|
||||
std::string GetEscapedBoldName() const;
|
||||
std::string GetEscapedString() const;
|
||||
[[nodiscard]] bool IsABot() const noexcept;
|
||||
[[nodiscard]] bool IsDeleted() const;
|
||||
[[nodiscard]] bool HasAvatar() const;
|
||||
[[nodiscard]] bool HasAnimatedAvatar() const noexcept;
|
||||
[[nodiscard]] bool HasAnimatedAvatar(Snowflake guild_id) const;
|
||||
[[nodiscard]] bool HasAnimatedAvatar(const std::optional<Snowflake> &guild_id) const;
|
||||
[[nodiscard]] std::string GetAvatarURL(Snowflake guild_id, const std::string &ext = "png", std::string size = "32") const;
|
||||
[[nodiscard]] std::string GetAvatarURL(const std::optional<Snowflake> &guild_id, const std::string &ext = "png", std::string size = "32") const;
|
||||
[[nodiscard]] std::string GetAvatarURL(const std::string &ext = "png", std::string size = "32") const;
|
||||
[[nodiscard]] std::string GetDefaultAvatarURL() const;
|
||||
[[nodiscard]] Snowflake GetHoistedRole(Snowflake guild_id, bool with_color = false) const;
|
||||
[[nodiscard]] std::string GetMention() const;
|
||||
[[nodiscard]] std::string GetEscapedName() const;
|
||||
[[nodiscard]] std::string GetEscapedBoldName() const;
|
||||
[[nodiscard]] std::string GetEscapedString() const;
|
||||
template<bool with_at>
|
||||
inline std::string GetEscapedBoldString() const {
|
||||
[[nodiscard]] inline std::string GetEscapedBoldString() const {
|
||||
if constexpr (with_at)
|
||||
return "<b>@" + Glib::Markup::escape_text(Username) + "</b>#" + Discriminator;
|
||||
else
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -6,42 +6,43 @@
|
||||
struct UserSettingsGuildFoldersEntry {
|
||||
int Color = -1; // null
|
||||
std::vector<Snowflake> GuildIDs;
|
||||
Snowflake ID; // null (this can be a snowflake as a string or an int that isnt a snowflake lol)
|
||||
Snowflake ID; // null (this can be a snowflake as a string or an int that isnt a snowflake lol)
|
||||
std::string Name; // null
|
||||
|
||||
friend void from_json(const nlohmann::json &j, UserSettingsGuildFoldersEntry &m);
|
||||
};
|
||||
|
||||
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<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?
|
||||
std::vector<UserSettingsGuildFoldersEntry> GuildFolders; //
|
||||
bool ShouldGIFAutoplay; //
|
||||
// Unknown FriendSourceFlags; //
|
||||
int ExplicitContentFilter; //
|
||||
bool IsTTSCommandEnabled; //
|
||||
bool ShouldDisableGamesTab; //
|
||||
bool DeveloperMode; //
|
||||
bool ShouldDetectPlatformAccounts; //
|
||||
bool AreDefaultGuildsRestricted; //
|
||||
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);
|
||||
};
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
#include "websocket.hpp"
|
||||
#include <functional>
|
||||
#include <utility>
|
||||
|
||||
Websocket::Websocket() {}
|
||||
Websocket::Websocket() = default;
|
||||
|
||||
void Websocket::StartConnection(std::string url) {
|
||||
void Websocket::StartConnection(const std::string &url) {
|
||||
m_websocket.disableAutomaticReconnection();
|
||||
m_websocket.setUrl(url);
|
||||
m_websocket.setOnMessageCallback(std::bind(&Websocket::OnMessage, this, std::placeholders::_1));
|
||||
m_websocket.setOnMessageCallback([this](auto &&msg) { OnMessage(std::forward<decltype(msg)>(msg)); });
|
||||
m_websocket.setExtraHeaders(ix::WebSocketHttpHeaders { { "User-Agent", m_agent } }); // idk if this actually works
|
||||
m_websocket.start();
|
||||
}
|
||||
|
||||
void Websocket::SetUserAgent(std::string agent) {
|
||||
m_agent = agent;
|
||||
m_agent = std::move(agent);
|
||||
}
|
||||
|
||||
bool Websocket::GetPrintMessages() const noexcept {
|
||||
@@ -31,11 +31,6 @@ void Websocket::Stop(uint16_t code) {
|
||||
m_websocket.stop(code);
|
||||
}
|
||||
|
||||
bool Websocket::IsOpen() const {
|
||||
auto state = m_websocket.getReadyState();
|
||||
return state == ix::ReadyState::Open;
|
||||
}
|
||||
|
||||
void Websocket::Send(const std::string &str) {
|
||||
if (m_print_messages)
|
||||
printf("sending %s\n", str.c_str());
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
class Websocket {
|
||||
public:
|
||||
Websocket();
|
||||
void StartConnection(std::string url);
|
||||
void StartConnection(const std::string &url);
|
||||
|
||||
void SetUserAgent(std::string agent);
|
||||
|
||||
@@ -20,7 +20,6 @@ public:
|
||||
void Send(const nlohmann::json &j);
|
||||
void Stop();
|
||||
void Stop(uint16_t code);
|
||||
bool IsOpen() const;
|
||||
|
||||
private:
|
||||
void OnMessage(const ix::WebSocketMessagePtr &msg);
|
||||
|
||||
@@ -1,8 +1,25 @@
|
||||
#include "emojis.hpp"
|
||||
#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(filepath) {}
|
||||
: m_filepath(std::move(filepath)) {}
|
||||
|
||||
bool EmojiResource::Load() {
|
||||
m_fp = std::fopen(m_filepath.c_str(), "rb");
|
||||
@@ -10,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));
|
||||
@@ -29,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.push_back(surrogates);
|
||||
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)
|
||||
@@ -52,7 +76,7 @@ bool EmojiResource::Load() {
|
||||
|
||||
Glib::RefPtr<Gdk::Pixbuf> EmojiResource::GetPixBuf(const Glib::ustring &pattern) {
|
||||
const auto it = m_index.find(pattern);
|
||||
if (it == m_index.end()) return Glib::RefPtr<Gdk::Pixbuf>();
|
||||
if (it == m_index.end()) return {};
|
||||
const int pos = it->second.first;
|
||||
const int len = it->second.second;
|
||||
std::fseek(m_fp, pos, SEEK_SET);
|
||||
@@ -86,17 +110,17 @@ void EmojiResource::ReplaceEmojis(Glib::RefPtr<Gtk::TextBuffer> buf, int size) {
|
||||
else
|
||||
break;
|
||||
}
|
||||
searchpos = r + pattern.size();
|
||||
searchpos = static_cast<int>(r + pattern.size());
|
||||
|
||||
const auto start_it = buf->get_iter_at_offset(r);
|
||||
const auto end_it = buf->get_iter_at_offset(r + pattern.size());
|
||||
const auto start_it = buf->get_iter_at_offset(static_cast<int>(r));
|
||||
const auto end_it = buf->get_iter_at_offset(static_cast<int>(r + pattern.size()));
|
||||
|
||||
auto it = buf->erase(start_it, end_it);
|
||||
buf->insert_pixbuf(it, pixbuf);
|
||||
|
||||
int alen = text.size();
|
||||
int alen = static_cast<int>(text.size());
|
||||
text = get_text();
|
||||
int blen = text.size();
|
||||
int blen = static_cast<int>(text.size());
|
||||
searchpos -= (alen - blen);
|
||||
}
|
||||
}
|
||||
@@ -109,10 +133,6 @@ std::string EmojiResource::GetShortCodeForPattern(const Glib::ustring &pattern)
|
||||
return "";
|
||||
}
|
||||
|
||||
const std::vector<Glib::ustring> &EmojiResource::GetPatterns() const {
|
||||
return m_patterns;
|
||||
}
|
||||
|
||||
const std::map<std::string, std::string> &EmojiResource::GetShortCodes() const {
|
||||
return m_shortcode_index;
|
||||
}
|
||||
|
||||
@@ -12,7 +12,6 @@ public:
|
||||
EmojiResource(std::string filepath);
|
||||
bool Load();
|
||||
Glib::RefPtr<Gdk::Pixbuf> GetPixBuf(const Glib::ustring &pattern);
|
||||
const std::vector<Glib::ustring> &GetPatterns() const;
|
||||
const std::map<std::string, std::string> &GetShortCodes() const;
|
||||
void ReplaceEmojis(Glib::RefPtr<Gtk::TextBuffer> buf, int size = 24);
|
||||
std::string GetShortCodeForPattern(const Glib::ustring &pattern);
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
#include "abaddon.hpp"
|
||||
#include "filecache.hpp"
|
||||
|
||||
#include <utility>
|
||||
#include "MurmurHash3.h"
|
||||
|
||||
std::string GetCachedName(std::string str) {
|
||||
std::string GetCachedName(const std::string &str) {
|
||||
uint32_t out;
|
||||
MurmurHash3_x86_32(str.c_str(), str.size(), 0, &out);
|
||||
MurmurHash3_x86_32(str.c_str(), static_cast<int>(str.size()), 0, &out);
|
||||
return std::to_string(out);
|
||||
}
|
||||
|
||||
@@ -35,15 +37,15 @@ void Cache::ClearCache() {
|
||||
std::filesystem::remove_all(path);
|
||||
}
|
||||
|
||||
void Cache::RespondFromPath(std::filesystem::path path, callback_type cb) {
|
||||
void Cache::RespondFromPath(const std::filesystem::path &path, const callback_type &cb) {
|
||||
cb(path.string());
|
||||
}
|
||||
|
||||
void Cache::GetFileFromURL(std::string url, callback_type cb) {
|
||||
void Cache::GetFileFromURL(const std::string &url, const callback_type &cb) {
|
||||
auto cache_path = m_tmp_path / GetCachedName(url);
|
||||
if (std::filesystem::exists(cache_path)) {
|
||||
m_mutex.lock();
|
||||
m_futures.push_back(std::async(std::launch::async, [this, cache_path, cb]() { RespondFromPath(cache_path, cb); }));
|
||||
m_futures.push_back(std::async(std::launch::async, [cache_path, cb]() { RespondFromPath(cache_path, cb); }));
|
||||
m_mutex.unlock();
|
||||
return;
|
||||
}
|
||||
@@ -58,7 +60,7 @@ void Cache::GetFileFromURL(std::string url, callback_type cb) {
|
||||
}
|
||||
}
|
||||
|
||||
std::string Cache::GetPathIfCached(std::string url) {
|
||||
std::string Cache::GetPathIfCached(const std::string &url) {
|
||||
auto cache_path = m_tmp_path / GetCachedName(url);
|
||||
if (std::filesystem::exists(cache_path)) {
|
||||
return cache_path.string();
|
||||
@@ -94,13 +96,13 @@ void Cache::OnResponse(const std::string &url) {
|
||||
|
||||
void Cache::OnFetchComplete(const std::string &url) {
|
||||
m_mutex.lock();
|
||||
m_futures.push_back(std::async(std::launch::async, std::bind(&Cache::OnResponse, this, url)));
|
||||
m_futures.push_back(std::async(std::launch::async, [this, url] { OnResponse(url); }));
|
||||
m_mutex.unlock();
|
||||
}
|
||||
|
||||
FileCacheWorkerThread::FileCacheWorkerThread() {
|
||||
m_multi_handle = curl_multi_init();
|
||||
m_thread = std::thread(std::bind(&FileCacheWorkerThread::loop, this));
|
||||
m_thread = std::thread([this] { loop(); });
|
||||
}
|
||||
|
||||
FileCacheWorkerThread::~FileCacheWorkerThread() {
|
||||
@@ -116,7 +118,7 @@ void FileCacheWorkerThread::set_file_path(const std::filesystem::path &path) {
|
||||
|
||||
void FileCacheWorkerThread::add_image(const std::string &string, callback_type callback) {
|
||||
m_queue_mutex.lock();
|
||||
m_queue.push({ string, callback });
|
||||
m_queue.push({ string, std::move(callback) });
|
||||
m_cv.notify_one();
|
||||
m_queue_mutex.unlock();
|
||||
}
|
||||
@@ -130,15 +132,14 @@ void FileCacheWorkerThread::stop() {
|
||||
}
|
||||
|
||||
void FileCacheWorkerThread::loop() {
|
||||
timeval timeout;
|
||||
timeval timeout {};
|
||||
timeout.tv_sec = 1;
|
||||
timeout.tv_usec = 0;
|
||||
|
||||
while (!m_stop) {
|
||||
if (m_handles.size() == 0) {
|
||||
if (m_handles.empty()) {
|
||||
std::unique_lock<std::mutex> lock(m_queue_mutex);
|
||||
int s = m_queue.size();
|
||||
if (s == 0)
|
||||
if (m_queue.empty())
|
||||
m_cv.wait(lock);
|
||||
}
|
||||
|
||||
@@ -146,7 +147,7 @@ void FileCacheWorkerThread::loop() {
|
||||
if (m_handles.size() < concurrency) {
|
||||
std::optional<QueueEntry> entry;
|
||||
m_queue_mutex.lock();
|
||||
if (m_queue.size() > 0) {
|
||||
if (!m_queue.empty()) {
|
||||
entry = std::move(m_queue.front());
|
||||
m_queue.pop();
|
||||
}
|
||||
|
||||
@@ -59,13 +59,13 @@ public:
|
||||
~Cache();
|
||||
|
||||
using callback_type = std::function<void(std::string)>;
|
||||
void GetFileFromURL(std::string url, callback_type cb);
|
||||
std::string GetPathIfCached(std::string url);
|
||||
void GetFileFromURL(const std::string &url, const callback_type &cb);
|
||||
std::string GetPathIfCached(const std::string &url);
|
||||
void ClearCache();
|
||||
|
||||
private:
|
||||
void CleanupFutures();
|
||||
void RespondFromPath(std::filesystem::path path, callback_type cb);
|
||||
static void RespondFromPath(const std::filesystem::path &path, const callback_type &cb);
|
||||
void OnResponse(const std::string &url);
|
||||
void OnFetchComplete(const std::string &url);
|
||||
|
||||
|
||||
99
src/http.cpp
99
src/http.cpp
@@ -1,8 +1,12 @@
|
||||
#include "http.hpp"
|
||||
|
||||
#include <utility>
|
||||
|
||||
// #define USE_LOCAL_PROXY
|
||||
|
||||
namespace http {
|
||||
request::request(EMethod method, const std::string &url)
|
||||
: m_url(url) {
|
||||
request::request(EMethod method, std::string url)
|
||||
: m_url(std::move(url)) {
|
||||
switch (method) {
|
||||
case REQUEST_GET:
|
||||
m_method = "GET";
|
||||
@@ -27,12 +31,52 @@ request::request(EMethod method, const 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) {
|
||||
@@ -55,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);
|
||||
@@ -64,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;
|
||||
}
|
||||
|
||||
@@ -99,7 +186,7 @@ void request::prepare() {
|
||||
namespace detail {
|
||||
size_t curl_write_data_callback(void *ptr, size_t size, size_t nmemb, void *userdata) {
|
||||
const size_t n = size * nmemb;
|
||||
static_cast<std::string*>(userdata)->append(static_cast<char*>(ptr), n);
|
||||
static_cast<std::string *>(userdata)->append(static_cast<char *>(ptr), n);
|
||||
return n;
|
||||
}
|
||||
|
||||
|
||||
29
src/http.hpp
29
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 {
|
||||
@@ -97,17 +99,31 @@ struct response {
|
||||
};
|
||||
|
||||
struct request {
|
||||
request(EMethod method, const std::string &url);
|
||||
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;
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
#include "imgmanager.hpp"
|
||||
|
||||
#include <utility>
|
||||
#include "util.hpp"
|
||||
#include "abaddon.hpp"
|
||||
|
||||
@@ -6,17 +8,13 @@ ImageManager::ImageManager() {
|
||||
m_cb_dispatcher.connect(sigc::mem_fun(*this, &ImageManager::RunCallbacks));
|
||||
}
|
||||
|
||||
Cache &ImageManager::GetCache() {
|
||||
return m_cache;
|
||||
}
|
||||
|
||||
void ImageManager::ClearCache() {
|
||||
m_cache.ClearCache();
|
||||
}
|
||||
|
||||
Glib::RefPtr<Gdk::Pixbuf> ImageManager::ReadFileToPixbuf(std::string path) {
|
||||
const auto &data = ReadWholeFile(path);
|
||||
if (data.size() == 0) return Glib::RefPtr<Gdk::Pixbuf>(nullptr);
|
||||
const auto &data = ReadWholeFile(std::move(path));
|
||||
if (data.empty()) return Glib::RefPtr<Gdk::Pixbuf>(nullptr);
|
||||
auto loader = Gdk::PixbufLoader::create();
|
||||
loader->signal_size_prepared().connect([&loader](int w, int h) {
|
||||
int cw, ch;
|
||||
@@ -29,8 +27,8 @@ Glib::RefPtr<Gdk::Pixbuf> ImageManager::ReadFileToPixbuf(std::string path) {
|
||||
}
|
||||
|
||||
Glib::RefPtr<Gdk::PixbufAnimation> ImageManager::ReadFileToPixbufAnimation(std::string path, int w, int h) {
|
||||
const auto &data = ReadWholeFile(path);
|
||||
if (data.size() == 0) return Glib::RefPtr<Gdk::PixbufAnimation>(nullptr);
|
||||
const auto &data = ReadWholeFile(std::move(path));
|
||||
if (data.empty()) return Glib::RefPtr<Gdk::PixbufAnimation>(nullptr);
|
||||
auto loader = Gdk::PixbufLoader::create();
|
||||
loader->signal_size_prepared().connect([&loader, w, h](int, int) {
|
||||
loader->set_size(w, h);
|
||||
@@ -40,10 +38,10 @@ Glib::RefPtr<Gdk::PixbufAnimation> ImageManager::ReadFileToPixbufAnimation(std::
|
||||
return loader->get_animation();
|
||||
}
|
||||
|
||||
void ImageManager::LoadFromURL(std::string url, callback_type cb) {
|
||||
void ImageManager::LoadFromURL(const std::string &url, const callback_type &cb) {
|
||||
sigc::signal<void(Glib::RefPtr<Gdk::Pixbuf>)> signal;
|
||||
signal.connect(cb);
|
||||
m_cache.GetFileFromURL(url, [this, url, signal](std::string path) {
|
||||
m_cache.GetFileFromURL(url, [this, url, signal](const std::string &path) {
|
||||
try {
|
||||
auto buf = ReadFileToPixbuf(path);
|
||||
if (!buf)
|
||||
@@ -60,10 +58,10 @@ void ImageManager::LoadFromURL(std::string url, callback_type cb) {
|
||||
});
|
||||
}
|
||||
|
||||
void ImageManager::LoadAnimationFromURL(std::string url, int w, int h, callback_anim_type cb) {
|
||||
void ImageManager::LoadAnimationFromURL(const std::string &url, int w, int h, const callback_anim_type &cb) {
|
||||
sigc::signal<void(Glib::RefPtr<Gdk::PixbufAnimation>)> signal;
|
||||
signal.connect(cb);
|
||||
m_cache.GetFileFromURL(url, [this, url, signal, w, h](std::string path) {
|
||||
m_cache.GetFileFromURL(url, [this, url, signal, w, h](const std::string &path) {
|
||||
try {
|
||||
auto buf = ReadFileToPixbufAnimation(path, w, h);
|
||||
if (!buf)
|
||||
@@ -80,7 +78,7 @@ void ImageManager::LoadAnimationFromURL(std::string url, int w, int h, callback_
|
||||
});
|
||||
}
|
||||
|
||||
void ImageManager::Prefetch(std::string url) {
|
||||
void ImageManager::Prefetch(const std::string &url) {
|
||||
m_cache.GetFileFromURL(url, [](const auto &) {});
|
||||
}
|
||||
|
||||
@@ -91,22 +89,6 @@ void ImageManager::RunCallbacks() {
|
||||
m_cb_mutex.unlock();
|
||||
}
|
||||
|
||||
Glib::RefPtr<Gdk::Pixbuf> ImageManager::GetFromURLIfCached(std::string url) {
|
||||
std::string path = m_cache.GetPathIfCached(url);
|
||||
if (path != "")
|
||||
return ReadFileToPixbuf(path);
|
||||
|
||||
return Glib::RefPtr<Gdk::Pixbuf>(nullptr);
|
||||
}
|
||||
|
||||
Glib::RefPtr<Gdk::PixbufAnimation> ImageManager::GetAnimationFromURLIfCached(std::string url, int w, int h) {
|
||||
std::string path = m_cache.GetPathIfCached(url);
|
||||
if (path != "")
|
||||
return ReadFileToPixbufAnimation(path, w, h);
|
||||
|
||||
return Glib::RefPtr<Gdk::PixbufAnimation>(nullptr);
|
||||
}
|
||||
|
||||
Glib::RefPtr<Gdk::Pixbuf> ImageManager::GetPlaceholder(int size) {
|
||||
std::string name = "/placeholder" + std::to_string(size);
|
||||
if (m_pixs.find(name) != m_pixs.end())
|
||||
|
||||
@@ -13,19 +13,16 @@ public:
|
||||
using callback_anim_type = sigc::slot<void(Glib::RefPtr<Gdk::PixbufAnimation>)>;
|
||||
using callback_type = sigc::slot<void(Glib::RefPtr<Gdk::Pixbuf>)>;
|
||||
|
||||
Cache &GetCache();
|
||||
void ClearCache();
|
||||
void LoadFromURL(std::string url, callback_type cb);
|
||||
void LoadFromURL(const std::string &url, const callback_type &cb);
|
||||
// animations need dimensions before loading since there is no (easy) way to scale a PixbufAnimation
|
||||
void LoadAnimationFromURL(std::string url, int w, int h, callback_anim_type cb);
|
||||
void Prefetch(std::string url);
|
||||
Glib::RefPtr<Gdk::Pixbuf> GetFromURLIfCached(std::string url);
|
||||
Glib::RefPtr<Gdk::PixbufAnimation> GetAnimationFromURLIfCached(std::string url, int w, int h);
|
||||
void LoadAnimationFromURL(const std::string &url, int w, int h, const callback_anim_type &cb);
|
||||
void Prefetch(const std::string &url);
|
||||
Glib::RefPtr<Gdk::Pixbuf> GetPlaceholder(int size);
|
||||
|
||||
private:
|
||||
Glib::RefPtr<Gdk::Pixbuf> ReadFileToPixbuf(std::string path);
|
||||
Glib::RefPtr<Gdk::PixbufAnimation> ReadFileToPixbufAnimation(std::string path, int w, int h);
|
||||
static Glib::RefPtr<Gdk::Pixbuf> ReadFileToPixbuf(std::string path);
|
||||
static Glib::RefPtr<Gdk::PixbufAnimation> ReadFileToPixbufAnimation(std::string path, int w, int h);
|
||||
|
||||
mutable std::mutex m_load_mutex;
|
||||
void RunCallbacks();
|
||||
|
||||
@@ -1,18 +1,22 @@
|
||||
#include "platform.hpp"
|
||||
#include "util.hpp"
|
||||
#include <string>
|
||||
#include <fstream>
|
||||
#include <filesystem>
|
||||
#include <config.h>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <string>
|
||||
|
||||
using namespace std::literals::string_literals;
|
||||
|
||||
#if defined(_WIN32) && defined(_MSC_VER)
|
||||
#include <Windows.h>
|
||||
#include <Shlwapi.h>
|
||||
#include <ShlObj_core.h>
|
||||
#if defined(_WIN32)
|
||||
#include <pango/pangocairo.h>
|
||||
#include <pango/pangofc-fontmap.h>
|
||||
#if defined(_MSC_VER)
|
||||
#include <ShlObj_core.h>
|
||||
#else
|
||||
#include <shlobj.h>
|
||||
#endif
|
||||
#include <Shlwapi.h>
|
||||
#include <Windows.h>
|
||||
#pragma comment(lib, "Shlwapi.lib")
|
||||
bool Platform::SetupFonts() {
|
||||
using namespace std::string_literals;
|
||||
@@ -22,8 +26,8 @@ bool Platform::SetupFonts() {
|
||||
{
|
||||
// thanks @WorkingRobot for da help :^))
|
||||
|
||||
std::ifstream template_stream(buf + "\\fonts\\fonts.template.conf"s);
|
||||
std::ofstream conf_stream(buf + "\\fonts\\fonts.conf"s);
|
||||
std::ifstream template_stream(buf + R"(\fonts\fonts.template.conf)"s);
|
||||
std::ofstream conf_stream(buf + R"(\fonts\fonts.conf)"s);
|
||||
if (!template_stream.good()) {
|
||||
printf("can't open fonts/fonts.template.conf\n");
|
||||
return false;
|
||||
@@ -36,7 +40,7 @@ bool Platform::SetupFonts() {
|
||||
std::string line;
|
||||
while (std::getline(template_stream, line)) {
|
||||
if (line == "<!--(CONFD)-->")
|
||||
conf_stream << "<include ignore_missing=\"no\">" << (buf + "\\fonts\\conf.d"s) << "</include>";
|
||||
conf_stream << "<include ignore_missing=\"no\">" << (buf + R"(\fonts\conf.d)"s) << "</include>";
|
||||
else
|
||||
conf_stream << line;
|
||||
conf_stream << '\n';
|
||||
@@ -45,11 +49,11 @@ bool Platform::SetupFonts() {
|
||||
|
||||
auto fc = FcConfigCreate();
|
||||
FcConfigSetCurrent(fc);
|
||||
FcConfigParseAndLoad(fc, const_cast<FcChar8 *>(reinterpret_cast<const FcChar8 *>((buf + "\\fonts\\fonts.conf"s).c_str())), true);
|
||||
FcConfigParseAndLoad(fc, const_cast<FcChar8 *>(reinterpret_cast<const FcChar8 *>((buf + R"(\fonts\fonts.conf)"s).c_str())), true);
|
||||
FcConfigAppFontAddDir(fc, const_cast<FcChar8 *>(reinterpret_cast<const FcChar8 *>((buf + "\\fonts"s).c_str())));
|
||||
|
||||
char fonts_path[MAX_PATH];
|
||||
if (SHGetFolderPathA(NULL, CSIDL_FONTS, NULL, SHGFP_TYPE_CURRENT, fonts_path) == S_OK) {
|
||||
if (SHGetFolderPathA(nullptr, CSIDL_FONTS, nullptr, SHGFP_TYPE_CURRENT, fonts_path) == S_OK) {
|
||||
FcConfigAppFontAddDir(fc, reinterpret_cast<FcChar8 *>(fonts_path));
|
||||
}
|
||||
|
||||
@@ -107,17 +111,29 @@ std::string Platform::FindResourceFolder() {
|
||||
}
|
||||
|
||||
std::string Platform::FindConfigFile() {
|
||||
const auto x = std::getenv("ABADDON_CONFIG");
|
||||
if (x != nullptr)
|
||||
return x;
|
||||
const auto cfg = std::getenv("ABADDON_CONFIG");
|
||||
if (cfg != nullptr) return cfg;
|
||||
|
||||
const auto home_env = std::getenv("HOME");
|
||||
if (home_env != nullptr) {
|
||||
const auto home_path = home_env + "/.config/abaddon/abaddon.ini"s;
|
||||
for (auto path : { "./abaddon.ini"s, home_path }) {
|
||||
if (util::IsFile(path)) return path;
|
||||
// use config present in cwd first
|
||||
if (util::IsFile("./abaddon.ini"))
|
||||
return "./abaddon.ini";
|
||||
|
||||
if (const auto home_env = std::getenv("HOME")) {
|
||||
// use ~/.config if present
|
||||
if (auto home_path = home_env + "/.config/abaddon/abaddon.ini"s; util::IsFile(home_path)) {
|
||||
return home_path;
|
||||
}
|
||||
|
||||
// fallback to ~/.config if the directory exists/can be created
|
||||
std::error_code ec;
|
||||
const auto home_path = home_env + "/.config/abaddon"s;
|
||||
if (!util::IsFolder(home_path))
|
||||
std::filesystem::create_directories(home_path, ec);
|
||||
if (util::IsFolder(home_path))
|
||||
return home_path + "/abaddon.ini";
|
||||
}
|
||||
|
||||
// fallback to cwd if cant find + cant make in ~/.config
|
||||
puts("can't find configuration file!");
|
||||
return "./abaddon.ini";
|
||||
}
|
||||
|
||||
@@ -6,4 +6,4 @@ bool SetupFonts();
|
||||
std::string FindResourceFolder();
|
||||
std::string FindConfigFile();
|
||||
std::string FindStateCacheFolder();
|
||||
}
|
||||
} // namespace Platform
|
||||
|
||||
@@ -1,6 +1,15 @@
|
||||
#include "settings.hpp"
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <glibmm/miscutils.h>
|
||||
|
||||
#ifdef WITH_KEYCHAIN
|
||||
#include <keychain/keychain.h>
|
||||
#endif
|
||||
|
||||
const std::string KeychainPackage = "com.github.uowuo.abaddon";
|
||||
const std::string KeychainService = "abaddon-client-token";
|
||||
const std::string KeychainUser = "";
|
||||
|
||||
SettingsManager::SettingsManager(const std::string &filename)
|
||||
: m_filename(filename) {
|
||||
@@ -36,9 +45,9 @@ void SettingsManager::ReadSettings() {
|
||||
|
||||
SMSTR("discord", "api_base", APIBaseURL);
|
||||
SMSTR("discord", "gateway", GatewayURL);
|
||||
SMSTR("discord", "token", DiscordToken);
|
||||
SMBOOL("discord", "memory_db", UseMemoryDB);
|
||||
SMBOOL("discord", "prefetch", Prefetch);
|
||||
SMBOOL("discord", "autoconnect", Autoconnect);
|
||||
SMSTR("gui", "css", MainCSS);
|
||||
SMBOOL("gui", "animated_guild_hover_only", AnimatedGuildHoverOnly);
|
||||
SMBOOL("gui", "animations", ShowAnimations);
|
||||
@@ -47,11 +56,44 @@ void SettingsManager::ReadSettings() {
|
||||
SMBOOL("gui", "owner_crown", ShowOwnerCrown);
|
||||
SMBOOL("gui", "save_state", SaveState);
|
||||
SMBOOL("gui", "stock_emojis", ShowStockEmojis);
|
||||
SMBOOL("gui", "unreads", Unreads);
|
||||
SMBOOL("gui", "alt_menu", AltMenu);
|
||||
SMBOOL("gui", "hide_to_tray", HideToTray);
|
||||
SMINT("http", "concurrent", CacheHTTPConcurrency);
|
||||
SMSTR("http", "user_agent", UserAgent);
|
||||
SMSTR("style", "expandercolor", ChannelsExpanderColor);
|
||||
SMSTR("style", "linkcolor", LinkColor);
|
||||
SMSTR("style", "nsfwchannelcolor", NSFWChannelColor);
|
||||
SMSTR("style", "channelcolor", ChannelColor);
|
||||
SMSTR("style", "mentionbadgecolor", MentionBadgeColor);
|
||||
SMSTR("style", "mentionbadgetextcolor", MentionBadgeTextColor);
|
||||
SMSTR("style", "unreadcolor", UnreadIndicatorColor);
|
||||
|
||||
#ifdef WITH_KEYCHAIN
|
||||
keychain::Error error {};
|
||||
|
||||
// convert to keychain if present in normal settings
|
||||
SMSTR("discord", "token", DiscordToken);
|
||||
|
||||
if (!m_settings.DiscordToken.empty()) {
|
||||
keychain::Error set_error {};
|
||||
|
||||
keychain::setPassword(KeychainPackage, KeychainService, KeychainUser, m_settings.DiscordToken, set_error);
|
||||
if (set_error) {
|
||||
printf("keychain error setting token: %s\n", set_error.message.c_str());
|
||||
} else {
|
||||
m_file.remove_key("discord", "token");
|
||||
}
|
||||
}
|
||||
|
||||
m_settings.DiscordToken = keychain::getPassword(KeychainPackage, KeychainService, KeychainUser, error);
|
||||
if (error && error.type != keychain::ErrorType::NotFound) {
|
||||
printf("keychain error reading token: %s (%d)\n", error.message.c_str(), error.code);
|
||||
}
|
||||
|
||||
#else
|
||||
SMSTR("discord", "token", DiscordToken);
|
||||
#endif
|
||||
|
||||
#undef SMBOOL
|
||||
#undef SMSTR
|
||||
@@ -84,9 +126,9 @@ void SettingsManager::Close() {
|
||||
|
||||
SMSTR("discord", "api_base", APIBaseURL);
|
||||
SMSTR("discord", "gateway", GatewayURL);
|
||||
SMSTR("discord", "token", DiscordToken);
|
||||
SMBOOL("discord", "memory_db", UseMemoryDB);
|
||||
SMBOOL("discord", "prefetch", Prefetch);
|
||||
SMBOOL("discord", "autoconnect", Autoconnect);
|
||||
SMSTR("gui", "css", MainCSS);
|
||||
SMBOOL("gui", "animated_guild_hover_only", AnimatedGuildHoverOnly);
|
||||
SMBOOL("gui", "animations", ShowAnimations);
|
||||
@@ -95,11 +137,29 @@ void SettingsManager::Close() {
|
||||
SMBOOL("gui", "owner_crown", ShowOwnerCrown);
|
||||
SMBOOL("gui", "save_state", SaveState);
|
||||
SMBOOL("gui", "stock_emojis", ShowStockEmojis);
|
||||
SMBOOL("gui", "unreads", Unreads);
|
||||
SMBOOL("gui", "alt_menu", AltMenu);
|
||||
SMBOOL("gui", "hide_to_tray", HideToTray);
|
||||
SMINT("http", "concurrent", CacheHTTPConcurrency);
|
||||
SMSTR("http", "user_agent", UserAgent);
|
||||
SMSTR("style", "expandercolor", ChannelsExpanderColor);
|
||||
SMSTR("style", "linkcolor", LinkColor);
|
||||
SMSTR("style", "nsfwchannelcolor", NSFWChannelColor);
|
||||
SMSTR("style", "channelcolor", ChannelColor);
|
||||
SMSTR("style", "mentionbadgecolor", MentionBadgeColor);
|
||||
SMSTR("style", "mentionbadgetextcolor", MentionBadgeTextColor);
|
||||
SMSTR("style", "unreadcolor", UnreadIndicatorColor);
|
||||
|
||||
#ifdef WITH_KEYCHAIN
|
||||
keychain::Error error {};
|
||||
|
||||
keychain::setPassword(KeychainPackage, KeychainService, KeychainUser, m_settings.DiscordToken, error);
|
||||
if (error) {
|
||||
printf("keychain error setting token: %s\n", error.message.c_str());
|
||||
}
|
||||
#else
|
||||
SMSTR("discord", "token", DiscordToken);
|
||||
#endif
|
||||
|
||||
#undef SMSTR
|
||||
#undef SMBOOL
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user