35 Commits

Author SHA1 Message Date
ouwou
de3b53c676 add include 2022-05-22 00:03:51 -04:00
ouwou
3ac993bae4 always quietly try and include fontconfig if present 2022-05-21 22:59:53 -04:00
ouwou
756de57919 save state per-user 2022-05-20 02:29:32 -04:00
ouwou
d9cf989813 remove thing i shouldnt have committed 2022-05-19 03:15:27 -04:00
ouwou
ffc69576f2 fix role updates (fixes #69, fixes #70) 2022-05-19 03:13:02 -04:00
ouwou
2bed7f161b only save state when actually disconnecting (fixes #65) 2022-05-17 20:47:01 -04:00
ouwou
607607ef0a update css 2022-05-16 01:17:34 -04:00
ouwou
b2ba7709df make it look sort of better i guess 2022-05-15 23:38:10 -04:00
ouwou
8b488a5ca9 add icons to dms 2022-05-11 15:05:32 -04:00
ouwou
1d8ef79da6 add ctrl+number key shortcuts for tabs 2022-05-10 01:09:42 -04:00
ouwou
bbf32730cd add ctrl(+shift)+tab keybinds 2022-05-09 01:33:09 -04:00
ouwou
f58ca39e8c request channels again if accessibility was lost 2022-05-08 02:40:27 -04:00
ouwou
3b5f4ded31 stop sending messages to inaccessible channels 2022-05-08 00:27:48 -04:00
ouwou
f6fdfeb95f compile oopsy 2022-05-06 04:13:29 -04:00
ouwou
2c25319fb8 clear tabs when access lost, show blanks for missing channels 2022-05-06 01:14:15 -04:00
ouwou
7daa0a250c Merge branch 'master' into tabs 2022-05-03 18:26:17 -04:00
ouwou
121c2585c8 improve resizing behavior (fixes #67) 2022-05-03 18:19:26 -04:00
ouwou
b18b94818a respect muted state for tab indicator 2022-05-02 02:38:41 -04:00
ouwou
63db16a711 open channel if its the first opened tab 2022-05-02 02:31:15 -04:00
ouwou
c30ab91738 add menu+accelerator for go back/forward 2022-04-27 17:40:37 -04:00
ouwou
e8f16292d1 add back/forward history to tabs
also lots of reformatting in .cmake because clion is weird and did that for some reason
2022-04-27 16:24:11 -04:00
ouwou
db28abaa44 dont show attention indicator on checked tab 2022-04-23 15:41:31 -04:00
ouwou
bfb2490938 dont expand channel list when changing tabs 2022-04-23 15:33:54 -04:00
ouwou
b4ab88f708 add opened tabs to state 2022-04-21 14:41:45 -04:00
ouwou
2dab595476 add open dm in new tab 2022-04-21 14:19:21 -04:00
ouwou
a98967fccc add ci run for minimal dependencies 2022-04-20 21:50:57 -04:00
ouwou
32e4540464 install libhandy dep on msys 2022-04-17 02:26:21 -04:00
ouwou
02dc28e89c Merge branch 'master' into tabs 2022-04-17 02:20:34 -04:00
ouwou
47545d9d32 fix menu bar updates again (fixes #61) 2022-04-15 02:14:25 -04:00
ouwou
5670dfc1d5 update nightly.link download url 2022-04-15 03:26:27 +00:00
ouwou
34f8af599d fix tab titles 2022-04-14 00:11:39 -04:00
ouwou
d36fe4d0e9 add server icons to channels 2022-04-09 03:33:56 -04:00
ouwou
44317e2d34 more tab work
- only one tab for any channel can be open
- rudimentary unread indicators
- add some css
2022-04-09 02:45:09 -04:00
ouwou
5b806a2589 basic tabs system 2022-04-08 23:47:12 -04:00
ouwou
5a13c7fef7 pull in libhandy optionally 2022-04-08 14:50:11 -04:00
31 changed files with 1095 additions and 184 deletions

View File

@@ -9,6 +9,10 @@ jobs:
strategy:
matrix:
buildtype: [Debug, RelWithDebInfo, MinSizeRel]
mindeps: [false]
include:
- buildtype: RelWithDebInfo
mindeps: true
defaults:
run:
shell: msys2 {0}
@@ -17,12 +21,12 @@ jobs:
with:
submodules: true
- name: Setup MSYS2
uses: msys2/setup-msys2@v2
- name: Setup MSYS2 (1)
uses: haya14busa/action-cond@v1
id: setupmsys
with:
msystem: mingw64
update: true
install: >-
cond: ${{ matrix.mindeps == true }}
if_true: >-
git
make
mingw-w64-x86_64-toolchain
@@ -33,6 +37,25 @@ jobs:
mingw-w64-x86_64-curl
mingw-w64-x86_64-zlib
mingw-w64-x86_64-gtkmm3
if_false: >-
git
make
mingw-w64-x86_64-toolchain
mingw-w64-x86_64-cmake
mingw-w64-x86_64-ninja
mingw-w64-x86_64-sqlite3
mingw-w64-x86_64-nlohmann-json
mingw-w64-x86_64-curl
mingw-w64-x86_64-zlib
mingw-w64-x86_64-gtkmm3
mingw-w64-x86_64-libhandy
- name: Setup MSYS2 (2)
uses: msys2/setup-msys2@v2
with:
msystem: mingw64
update: true
install: ${{ steps.setupmsys.outputs.value }}
- name: Build
run: |
@@ -49,12 +72,20 @@ jobs:
cp -r /mingw64/lib/gdk-pixbuf-2.0 build/artifactdir/lib
cp -r res/css res/res res/fonts build/artifactdir/bin
cp /mingw64/share/glib-2.0/schemas/gschemas.compiled build/artifactdir/share/glib-2.0/schemas
cat "ci/msys-deps.txt" | sed 's/\r$//' | xargs -I % cp /mingw64% build/artifactdir/bin
cat "ci/msys-deps.txt" | sed 's/\r$//' | xargs -I % cp /mingw64% build/artifactdir/bin || :
- name: Upload build
- name: Upload build (1)
uses: haya14busa/action-cond@v1
id: buildname
with:
cond: ${{ matrix.mindeps == true }}
if_true: "${{ matrix.buildtype }}-mindeps"
if_false: "${{ matrix.buildtype }}"
- name: Upload build (2)
uses: actions/upload-artifact@v2
with:
name: build-windows-msys2-${{ matrix.buildtype }}
name: build-windows-msys2-${{ steps.buildname.outputs.value }}
path: build/artifactdir
mac:

View File

@@ -7,6 +7,8 @@ set(ABADDON_RESOURCE_DIR "/usr/share/abaddon" CACHE PATH "Fallback directory for
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${CMAKE_SOURCE_DIR}/cmake/")
option(USE_LIBHANDY "Enable features that require libhandy (default)" ON)
find_package(nlohmann_json REQUIRED)
find_package(CURL)
find_package(ZLIB REQUIRED)
@@ -17,30 +19,27 @@ 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)
find_package(Fontconfig REQUIRED)
link_libraries(${Fontconfig_LIBRARIES})
endif()
if (WIN32)
add_compile_definitions(_CRT_SECURE_NO_WARNINGS)
add_compile_definitions(NOMINMAX)
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 +50,53 @@ target_include_directories(abaddon PUBLIC ${SQLite3_INCLUDE_DIRS})
target_include_directories(abaddon PUBLIC ${NLOHMANN_JSON_INCLUDE_DIRS})
if ((CMAKE_CXX_COMPILER_ID STREQUAL "GNU") OR
(CMAKE_CXX_COMPILER_ID STREQUAL "Clang" AND
((CMAKE_SYSTEM_NAME STREQUAL "Linux") OR (CMAKE_CXX_COMPILER_VERSION LESS 9))))
target_link_libraries(abaddon stdc++fs)
endif()
(CMAKE_CXX_COMPILER_ID STREQUAL "Clang" AND
((CMAKE_SYSTEM_NAME STREQUAL "Linux") OR (CMAKE_CXX_COMPILER_VERSION LESS 9))))
target_link_libraries(abaddon stdc++fs)
endif ()
if (IXWebSocket_LIBRARIES)
target_link_libraries(abaddon ${IXWebSocket_LIBRARIES})
find_library(MBEDTLS_X509_LIBRARY mbedx509)
find_library(MBEDTLS_TLS_LIBRARY mbedtls)
find_library(MBEDTLS_CRYPTO_LIBRARY mbedcrypto)
if (MBEDTLS_TLS_LIBRARY)
target_link_libraries(abaddon ${MBEDTLS_TLS_LIBRARY})
endif()
if (MBEDTLS_X509_LIBRARY)
target_link_libraries(abaddon ${MBEDTLS_X509_LIBRARY})
endif()
if (MBEDTLS_CRYPTO_LIBRARY)
target_link_libraries(abaddon ${MBEDTLS_CRYPTO_LIBRARY})
endif()
else()
target_link_libraries(abaddon $<BUILD_INTERFACE:ixwebsocket>)
endif()
target_link_libraries(abaddon ${IXWebSocket_LIBRARIES})
find_library(MBEDTLS_X509_LIBRARY mbedx509)
find_library(MBEDTLS_TLS_LIBRARY mbedtls)
find_library(MBEDTLS_CRYPTO_LIBRARY mbedcrypto)
if (MBEDTLS_TLS_LIBRARY)
target_link_libraries(abaddon ${MBEDTLS_TLS_LIBRARY})
endif ()
if (MBEDTLS_X509_LIBRARY)
target_link_libraries(abaddon ${MBEDTLS_X509_LIBRARY})
endif ()
if (MBEDTLS_CRYPTO_LIBRARY)
target_link_libraries(abaddon ${MBEDTLS_CRYPTO_LIBRARY})
endif ()
else ()
target_link_libraries(abaddon $<BUILD_INTERFACE:ixwebsocket>)
endif ()
find_package(Threads)
if (Threads_FOUND)
target_link_libraries(abaddon Threads::Threads)
endif()
target_link_libraries(abaddon Threads::Threads)
endif ()
find_package(Fontconfig QUIET)
if (Fontconfig_FOUND)
target_link_libraries(abaddon Fontconfig::Fontconfig)
endif ()
target_link_libraries(abaddon ${SQLite3_LIBRARIES})
target_link_libraries(abaddon ${GTKMM_LIBRARIES})
target_link_libraries(abaddon ${CURL_LIBRARIES})
target_link_libraries(abaddon ${ZLIB_LIBRARY})
target_link_libraries(abaddon ${NLOHMANN_JSON_LIBRARIES})
if (USE_LIBHANDY)
find_package(libhandy)
if (NOT libhandy_FOUND)
message("libhandy could not be found. features requiring it have been disabled")
set(USE_LIBHANDY OFF)
else ()
target_include_directories(abaddon PUBLIC ${libhandy_INCLUDE_DIRS})
target_link_libraries(abaddon ${libhandy_LIBRARIES})
target_compile_definitions(abaddon PRIVATE WITH_LIBHANDY)
endif ()
endif ()

View File

@@ -81,7 +81,7 @@ Latest release version: https://github.com/uowuo/abaddon/releases/latest
**CI:**
- Windows: [here](https://nightly.link/uowuo/abaddon/workflows/ci/master/build-windows-RelWithDebInfo.zip)
- Windows: [here](https://nightly.link/uowuo/abaddon/workflows/ci/master/build-windows-msys2-MinSizeRel.zip)
- MacOS: [here](https://nightly.link/uowuo/abaddon/workflows/ci/master/build-macos-RelWithDebInfo.zip) unsigned,
unpackaged, requires gtkmm3 (e.g. from homebrew)
- Linux: [here](https://nightly.link/uowuo/abaddon/workflows/ci/master/build-linux-MinSizeRel.zip) unpackaged (for now),

View File

@@ -31,6 +31,7 @@
/bin/libgraphite2.dll
/bin/libgtk-3-0.dll
/bin/libgtkmm-3.0-1.dll
/bin/libhandy-1-0.dll
/bin/libharfbuzz-0.dll
/bin/libiconv-2.dll
/bin/libidn2-0.dll

37
cmake/Findgdk.cmake Normal file
View 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)

View File

@@ -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)

View File

@@ -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)

View File

@@ -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
View 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)

View File

@@ -282,3 +282,44 @@
.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);
}

BIN
res/res.7z Normal file

Binary file not shown.

View File

@@ -17,6 +17,10 @@
#include "windows/pinnedwindow.hpp"
#include "windows/threadswindow.hpp"
#ifdef WITH_LIBHANDY
#include <handy.h>
#endif
#ifdef _WIN32
#pragma comment(lib, "crypt32.lib")
#endif
@@ -43,6 +47,10 @@ Abaddon::Abaddon()
m_discord.signal_thread_update().connect(sigc::mem_fun(*this, &Abaddon::DiscordOnThreadUpdate));
m_discord.signal_message_sent().connect(sigc::mem_fun(*this, &Abaddon::DiscordOnMessageSent));
m_discord.signal_disconnected().connect(sigc::mem_fun(*this, &Abaddon::DiscordOnDisconnect));
m_discord.signal_channel_accessibility_changed().connect([this](Snowflake id, bool accessible) {
if (!accessible)
m_channels_requested.erase(id);
});
if (GetSettings().Prefetch)
m_discord.signal_message_create().connect([this](const Message &message) {
if (message.Author.HasAvatar())
@@ -59,9 +67,92 @@ Abaddon &Abaddon::Get() {
return instance;
}
#ifdef WITH_LIBHANDY
#ifdef _WIN32
constexpr static guint BUTTON_BACK = 4;
constexpr static guint BUTTON_FORWARD = 5;
#else
constexpr static guint BUTTON_BACK = 8;
constexpr static guint BUTTON_FORWARD = 9;
#endif
static bool HandleButtonEvents(GdkEvent *event, MainWindow *main_window) {
if (event->type != GDK_BUTTON_PRESS) return false;
auto *widget = gtk_get_event_widget(event);
if (widget == nullptr) return false;
auto *window = gtk_widget_get_toplevel(widget);
if (static_cast<void *>(window) != static_cast<void *>(main_window->gobj())) return false; // is this the right way???
switch (event->button.button) {
case BUTTON_BACK:
main_window->GoBack();
break;
case BUTTON_FORWARD:
main_window->GoForward();
break;
}
return false;
}
static bool HandleKeyEvents(GdkEvent *event, MainWindow *main_window) {
if (event->type != GDK_KEY_PRESS) return false;
auto *widget = gtk_get_event_widget(event);
if (widget == nullptr) return false;
auto *window = gtk_widget_get_toplevel(widget);
if (static_cast<void *>(window) != static_cast<void *>(main_window->gobj())) return false;
const bool ctrl = (event->key.state & GDK_CONTROL_MASK) == GDK_CONTROL_MASK;
const bool shft = (event->key.state & GDK_SHIFT_MASK) == GDK_SHIFT_MASK;
if (ctrl) {
switch (event->key.keyval) {
case GDK_KEY_Tab:
case GDK_KEY_KP_Tab:
case GDK_KEY_ISO_Left_Tab:
if (shft)
main_window->GoToPreviousTab();
else
main_window->GoToNextTab();
return true;
case GDK_KEY_1:
case GDK_KEY_2:
case GDK_KEY_3:
case GDK_KEY_4:
case GDK_KEY_5:
case GDK_KEY_6:
case GDK_KEY_7:
case GDK_KEY_8:
case GDK_KEY_9:
main_window->GoToTab(event->key.keyval - GDK_KEY_1);
return true;
case GDK_KEY_0:
main_window->GoToTab(9);
return true;
}
}
return false;
}
static void MainEventHandler(GdkEvent *event, void *main_window) {
if (HandleButtonEvents(event, static_cast<MainWindow *>(main_window))) return;
if (HandleKeyEvents(event, static_cast<MainWindow *>(main_window))) return;
gtk_main_do_event(event);
}
#endif
int Abaddon::StartGTK() {
m_gtk_app = Gtk::Application::create("com.github.uowuo.abaddon");
#ifdef WITH_LIBHANDY
m_gtk_app->signal_activate().connect([] {
hdy_init();
});
#endif
m_css_provider = Gtk::CssProvider::create();
m_css_provider->signal_parsing_error().connect([](const Glib::RefPtr<const Gtk::CssSection> &section, const Glib::Error &error) {
Gtk::MessageDialog dlg("css failed parsing (" + error.what() + ")", false, Gtk::MESSAGE_ERROR, Gtk::BUTTONS_OK, true);
@@ -103,6 +194,10 @@ int Abaddon::StartGTK() {
m_main_window->set_title(APP_TITLE);
m_main_window->set_position(Gtk::WIN_POS_CENTER);
#ifdef WITH_LIBHANDY
gdk_event_handler_set(&MainEventHandler, m_main_window.get(), nullptr);
#endif
if (!m_settings.IsValid()) {
Gtk::MessageDialog dlg(*m_main_window, "The settings file could not be opened!", false, Gtk::MESSAGE_ERROR, Gtk::BUTTONS_OK, true);
dlg.set_position(Gtk::WIN_POS_CENTER);
@@ -138,7 +233,7 @@ int Abaddon::StartGTK() {
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));
@@ -154,6 +249,8 @@ int Abaddon::StartGTK() {
m_gtk_app->signal_shutdown().connect(sigc::mem_fun(*this, &Abaddon::OnShutdown), false);
m_main_window->UpdateMenus();
m_main_window->show();
return m_gtk_app->run(*m_main_window);
}
@@ -173,11 +270,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 {
@@ -410,6 +509,9 @@ void Abaddon::SaveState() {
AbaddonApplicationState state;
state.ActiveChannel = m_main_window->GetChatActiveChannel();
state.Expansion = m_main_window->GetChannelList()->GetExpansionState();
#ifdef WITH_LIBHANDY
state.Tabs = m_main_window->GetChatWindow()->GetTabsState();
#endif
const auto path = GetStateCachePath();
if (!util::IsFolder(path)) {
@@ -417,7 +519,8 @@ void Abaddon::SaveState() {
std::filesystem::create_directories(path, ec);
}
auto *fp = std::fopen(GetStateCachePath("/state.json").c_str(), "wb");
auto file_name = "/" + std::to_string(m_discord.GetUserData().ID) + ".json";
auto *fp = std::fopen(GetStateCachePath(file_name).c_str(), "wb");
if (fp == nullptr) return;
const auto s = nlohmann::json(state).dump(4);
std::fwrite(s.c_str(), 1, s.size(), fp);
@@ -431,11 +534,15 @@ void Abaddon::LoadState() {
return;
}
const auto data = ReadWholeFile(GetStateCachePath("/state.json"));
auto file_name = "/" + std::to_string(m_discord.GetUserData().ID) + ".json";
const auto data = ReadWholeFile(GetStateCachePath(file_name));
if (data.empty()) return;
try {
AbaddonApplicationState state = nlohmann::json::parse(data.begin(), data.end());
m_main_window->GetChannelList()->UseExpansionState(state.Expansion);
#ifdef WITH_LIBHANDY
m_main_window->GetChatWindow()->UseTabsState(state.Tabs);
#endif
ActionChannelOpened(state.ActiveChannel);
} catch (const std::exception &e) {
printf("failed to load application state: %s\n", e.what());
@@ -535,6 +642,7 @@ void Abaddon::ActionSetToken() {
m_main_window->UpdateComponents();
GetSettings().DiscordToken = m_discord_token;
}
m_main_window->UpdateMenus();
}
void Abaddon::ActionJoinGuildDialog() {
@@ -546,13 +654,17 @@ void Abaddon::ActionJoinGuildDialog() {
}
}
void Abaddon::ActionChannelOpened(Snowflake id) {
void Abaddon::ActionChannelOpened(Snowflake id, bool expand_to) {
if (!id.IsValid() || id == m_main_window->GetChatActiveChannel()) return;
m_main_window->GetChatWindow()->SetTopic("");
const auto channel = m_discord.GetChannel(id);
if (!channel.has_value()) return;
if (!channel.has_value()) {
m_main_window->UpdateChatActiveChannel(Snowflake::Invalid, false);
m_main_window->UpdateChatWindowContents();
return;
}
const bool can_access = channel->IsDM() || m_discord.HasChannelPermission(m_discord.GetUserData().ID, id, Permission::VIEW_CHANNEL);
@@ -569,7 +681,7 @@ void Abaddon::ActionChannelOpened(Snowflake id) {
display = "Empty group";
m_main_window->set_title(std::string(APP_TITLE) + " - " + display);
}
m_main_window->UpdateChatActiveChannel(id);
m_main_window->UpdateChatActiveChannel(id, expand_to);
if (m_channels_requested.find(id) == m_channels_requested.end()) {
// dont fire requests we know will fail
if (can_access) {
@@ -595,6 +707,8 @@ void Abaddon::ActionChannelOpened(Snowflake id) {
ShowGuildVerificationGateDialog(*channel->GuildID);
}
}
m_main_window->UpdateMenus();
}
void Abaddon::ActionChatLoadHistory(Snowflake id) {
@@ -632,6 +746,10 @@ 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 (!channel.IsValid()) return;
if (!m_discord.HasChannelPermission(m_discord.GetUserData().ID, channel, Permission::VIEW_CHANNEL)) return;
if (referenced_message.IsValid())
m_discord.SendChatMessage(msg, channel, referenced_message);
else

View File

@@ -35,7 +35,7 @@ public:
void ActionDisconnect();
void ActionSetToken();
void ActionJoinGuildDialog();
void ActionChannelOpened(Snowflake id);
void ActionChannelOpened(Snowflake id, bool expand_to = true);
void ActionChatInputSubmit(std::string msg, Snowflake channel, Snowflake referenced_message);
void ActionChatLoadHistory(Snowflake id);
void ActionChatEditMessage(Snowflake channel_id, Snowflake id);

View File

@@ -17,6 +17,10 @@ ChannelList::ChannelList()
, m_menu_category_copy_id("_Copy ID", true)
, m_menu_channel_copy_id("_Copy ID", true)
, m_menu_channel_mark_as_read("Mark as _Read", true)
#ifdef WITH_LIBHANDY
, m_menu_channel_open_tab("Open in New _Tab", true)
, m_menu_dm_open_tab("Open in New _Tab", true)
#endif
, m_menu_dm_copy_id("_Copy ID", true)
, m_menu_dm_close("") // changes depending on if group or not
, m_menu_thread_copy_id("_Copy ID", true)
@@ -143,6 +147,15 @@ ChannelList::ChannelList()
else
discord.MuteChannel(id, NOOP_CALLBACK);
});
#ifdef WITH_LIBHANDY
m_menu_channel_open_tab.signal_activate().connect([this] {
const auto id = static_cast<Snowflake>((*m_model->get_iter(m_path_for_menu))[m_columns.m_id]);
m_signal_action_open_new_tab.emit(id);
});
m_menu_channel.append(m_menu_channel_open_tab);
#endif
m_menu_channel.append(m_menu_channel_mark_as_read);
m_menu_channel.append(m_menu_channel_toggle_mute);
m_menu_channel.append(m_menu_channel_copy_id);
@@ -170,6 +183,13 @@ ChannelList::ChannelList()
else
discord.MuteChannel(id, NOOP_CALLBACK);
});
#ifdef WITH_LIBHANDY
m_menu_dm_open_tab.signal_activate().connect([this] {
const auto id = static_cast<Snowflake>((*m_model->get_iter(m_path_for_menu))[m_columns.m_id]);
m_signal_action_open_new_tab.emit(id);
});
m_menu_dm.append(m_menu_dm_open_tab);
#endif
m_menu_dm.append(m_menu_dm_toggle_mute);
m_menu_dm.append(m_menu_dm_close);
m_menu_dm.append(m_menu_dm_copy_id);
@@ -442,7 +462,7 @@ void ChannelList::OnGuildUnmute(Snowflake id) {
// create a temporary channel row for non-joined threads
// and delete them when the active channel switches off of them if still not joined
void ChannelList::SetActiveChannel(Snowflake id) {
void ChannelList::SetActiveChannel(Snowflake id, bool expand_to) {
// mark channel as read when switching off
if (m_active_channel.IsValid())
Abaddon::Get().GetDiscordClient().MarkChannelAsRead(m_active_channel, [](...) {});
@@ -459,11 +479,12 @@ void ChannelList::SetActiveChannel(Snowflake id) {
const auto channel_iter = GetIteratorForChannelFromID(id);
if (channel_iter) {
m_view.expand_to_path(m_model->get_path(channel_iter));
if (expand_to) {
m_view.expand_to_path(m_model->get_path(channel_iter));
}
m_view.get_selection()->select(channel_iter);
} else {
m_view.get_selection()->unselect_all();
// SetActiveChannel should probably just take the channel object
const auto channel = Abaddon::Get().GetDiscordClient().GetChannel(id);
if (!channel.has_value() || !channel->IsThread()) return;
auto parent_iter = GetIteratorForChannelFromID(*channel->ParentID);
@@ -910,7 +931,12 @@ void ChannelList::OnChannelSubmenuPopup() {
const auto iter = m_model->get_iter(m_path_for_menu);
if (!iter) return;
const auto id = static_cast<Snowflake>((*iter)[m_columns.m_id]);
if (Abaddon::Get().GetDiscordClient().IsChannelMuted(id))
auto &discord = Abaddon::Get().GetDiscordClient();
#ifdef WITH_LIBHANDY
const auto perms = discord.HasChannelPermission(discord.GetUserData().ID, id, Permission::VIEW_CHANNEL);
m_menu_channel_open_tab.set_sensitive(perms);
#endif
if (discord.IsChannelMuted(id))
m_menu_channel_toggle_mute.set_label("Unmute");
else
m_menu_channel_toggle_mute.set_label("Mute");
@@ -960,6 +986,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);

View File

@@ -19,7 +19,7 @@ public:
ChannelList();
void UpdateListing();
void SetActiveChannel(Snowflake id);
void SetActiveChannel(Snowflake id, bool expand_to);
// channel list should be populated when this is called
void UseExpansionState(const ExpansionStateRoot &state);
@@ -121,11 +121,19 @@ protected:
Gtk::MenuItem m_menu_channel_mark_as_read;
Gtk::MenuItem m_menu_channel_toggle_mute;
#ifdef WITH_LIBHANDY
Gtk::MenuItem m_menu_channel_open_tab;
#endif
Gtk::Menu m_menu_dm;
Gtk::MenuItem m_menu_dm_copy_id;
Gtk::MenuItem m_menu_dm_close;
Gtk::MenuItem m_menu_dm_toggle_mute;
#ifdef WITH_LIBHANDY
Gtk::MenuItem m_menu_dm_open_tab;
#endif
Gtk::Menu m_menu_thread;
Gtk::MenuItem m_menu_thread_copy_id;
Gtk::MenuItem m_menu_thread_leave;
@@ -149,16 +157,25 @@ protected:
std::unordered_map<Snowflake, Gtk::TreeModel::iterator> m_tmp_channel_map;
public:
typedef sigc::signal<void, Snowflake> type_signal_action_channel_item_select;
typedef sigc::signal<void, Snowflake> type_signal_action_guild_leave;
typedef sigc::signal<void, Snowflake> type_signal_action_guild_settings;
using type_signal_action_channel_item_select = sigc::signal<void, Snowflake>;
using type_signal_action_guild_leave = sigc::signal<void, Snowflake>;
using type_signal_action_guild_settings = sigc::signal<void, Snowflake>;
#ifdef WITH_LIBHANDY
using type_signal_action_open_new_tab = sigc::signal<void, Snowflake>;
type_signal_action_open_new_tab signal_action_open_new_tab();
#endif
type_signal_action_channel_item_select signal_action_channel_item_select();
type_signal_action_guild_leave signal_action_guild_leave();
type_signal_action_guild_settings signal_action_guild_settings();
protected:
private:
type_signal_action_channel_item_select m_signal_action_channel_item_select;
type_signal_action_guild_leave m_signal_action_guild_leave;
type_signal_action_guild_settings m_signal_action_guild_settings;
#ifdef WITH_LIBHANDY
type_signal_action_open_new_tab m_signal_action_open_new_tab;
#endif
};

View 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

View 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

View File

@@ -4,6 +4,9 @@
#include "ratelimitindicator.hpp"
#include "chatinput.hpp"
#include "chatlist.hpp"
#ifdef WITH_LIBHANDY
#include "channeltabswitcherhandy.hpp"
#endif
ChatWindow::ChatWindow() {
Abaddon::Get().GetDiscordClient().signal_message_send_fail().connect(sigc::mem_fun(*this, &ChatWindow::OnMessageSendFail));
@@ -15,6 +18,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);
@@ -55,7 +65,7 @@ ChatWindow::ChatWindow() {
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);
@@ -88,6 +98,10 @@ ChatWindow::ChatWindow() {
m_meta->add(*m_input_indicator);
m_meta->add(*m_rate_limit_indicator);
// m_scroll->add(*m_list);
#ifdef WITH_LIBHANDY
m_main->add(*m_tab_switcher);
m_tab_switcher->show();
#endif
m_main->add(m_topic);
m_main->add(*m_chat);
m_main->add(m_completer);
@@ -115,6 +129,10 @@ void ChatWindow::SetActiveChannel(Snowflake id) {
m_rate_limit_indicator->SetActiveChannel(id);
if (m_is_replying)
StopReplying();
#ifdef WITH_LIBHANDY
m_tab_switcher->ReplaceActiveTab(id);
#endif
}
void ChatWindow::AddNewMessage(const Message &data) {
@@ -150,6 +168,44 @@ void ChatWindow::SetTopic(const std::string &text) {
m_topic.set_visible(text.length() > 0);
}
#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;
}

View File

@@ -4,6 +4,11 @@
#include <set>
#include "discord/discord.hpp"
#include "completer.hpp"
#include "state.hpp"
#ifdef WITH_LIBHANDY
class ChannelTabSwitcherHandy;
#endif
class ChatMessageHeader;
class ChatMessageItemContainer;
@@ -25,11 +30,22 @@ public:
void DeleteMessage(Snowflake id); // add [deleted] indicator
void UpdateMessage(Snowflake id); // add [edited] indicator
void AddNewHistory(const std::vector<Message> &msgs); // prepend messages
void InsertChatInput(const std::string& text);
void InsertChatInput(const std::string &text);
Snowflake GetOldestListedMessage(); // oldest message that is currently in the ListBox
void UpdateReactions(Snowflake id);
void SetTopic(const std::string &text);
#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;
Snowflake m_replying_to;
@@ -62,14 +78,18 @@ protected:
RateLimitIndicator *m_rate_limit_indicator;
Gtk::Box *m_meta;
#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, 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, 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();

View File

@@ -160,10 +160,11 @@ void MemberList::UpdateMemberList() {
}
int num_rows = 0;
const auto guild = *discord.GetGuild(m_guild_id);
const auto guild = discord.GetGuild(m_guild_id);
if (!guild.has_value()) return;
auto add_user = [this, &num_rows, guild](const UserData &data) -> bool {
if (num_rows++ > MaxMemberListRows) return false;
auto *row = Gtk::manage(new MemberListUserRow(guild, data));
auto *row = Gtk::manage(new MemberListUserRow(*guild, data));
m_id_to_row[data.ID] = row;
AttachUserMenuHandler(row, data.ID);
m_listbox->add(*row);

View File

@@ -89,7 +89,28 @@ bool ChannelData::HasIcon() const noexcept {
}
std::string ChannelData::GetIconURL() const {
return "https://cdn.discordapp.com/channel-icons/" + std::to_string(ID) + "/" + *Icon + ".png";
if (HasIcon()) {
return "https://cdn.discordapp.com/channel-icons/" + std::to_string(ID) + "/" + *Icon + ".png";
} else {
const auto recipients = GetDMRecipients();
if (!recipients.empty())
return recipients[0].GetAvatarURL("png", "32");
else
return "https://cdn.discordapp.com/embed/avatars/0.png";
}
}
std::string ChannelData::GetDisplayName() const {
if (Name.has_value()) {
return "#" + *Name;
} else {
const auto recipients = GetDMRecipients();
if (Type == ChannelType::DM && !recipients.empty())
return recipients[0].Username;
else if (Type == ChannelType::GROUP_DM)
return std::to_string(recipients.size()) + " members";
}
return "Unknown";
}
std::vector<Snowflake> ChannelData::GetChildIDs() const {

View File

@@ -102,6 +102,7 @@ struct ChannelData {
[[nodiscard]] bool IsText() const noexcept;
[[nodiscard]] bool HasIcon() const noexcept;
[[nodiscard]] std::string GetIconURL() const;
[[nodiscard]] std::string GetDisplayName() const;
[[nodiscard]] std::vector<Snowflake> GetChildIDs() const;
[[nodiscard]] std::optional<PermissionOverwrite> GetOverwrite(Snowflake id) const;
[[nodiscard]] std::vector<UserData> GetDMRecipients() const;

View File

@@ -41,7 +41,7 @@ void DiscordClient::Start() {
m_websocket.StartConnection(GetGatewayURL());
}
void DiscordClient::Stop() {
bool DiscordClient::Stop() {
if (m_client_started) {
inflateEnd(&m_zstream);
m_compressed_buf.clear();
@@ -55,9 +55,15 @@ void DiscordClient::Stop() {
m_guild_to_users.clear();
m_websocket.Stop();
m_client_started = false;
return true;
}
m_client_started = false;
return false;
}
bool DiscordClient::IsStarted() const {
@@ -1622,18 +1628,27 @@ void DiscordClient::HandleGatewayChannelDelete(const GatewayMessage &msg) {
it->second.erase(id);
m_store.ClearChannel(id);
m_signal_channel_delete.emit(id);
m_signal_channel_accessibility_changed.emit(id, false);
}
void DiscordClient::HandleGatewayChannelUpdate(const GatewayMessage &msg) {
const auto id = msg.Data.at("id").get<Snowflake>();
auto cur = m_store.GetChannel(id);
if (cur.has_value()) {
const bool old_perms = HasChannelPermission(m_user_data.ID, id, Permission::VIEW_CHANNEL);
cur->update_from_json(msg.Data);
m_store.SetChannel(id, *cur);
if (cur->PermissionOverwrites.has_value())
for (const auto &p : *cur->PermissionOverwrites)
m_store.SetPermissionOverwrite(id, p.ID, p);
m_signal_channel_update.emit(id);
const bool new_perms = HasChannelPermission(m_user_data.ID, id, Permission::VIEW_CHANNEL);
if (old_perms && !new_perms)
m_signal_channel_accessibility_changed.emit(id, false);
else if (!old_perms && new_perms)
m_signal_channel_accessibility_changed.emit(id, true);
}
}
@@ -1660,29 +1675,37 @@ void DiscordClient::HandleGatewayGuildUpdate(const GatewayMessage &msg) {
void DiscordClient::HandleGatewayGuildRoleUpdate(const GatewayMessage &msg) {
GuildRoleUpdateObject data = msg.Data;
const auto channels = GetChannelsInGuild(data.GuildID);
std::unordered_set<Snowflake> accessible;
for (auto channel : channels) {
if (HasChannelPermission(m_user_data.ID, channel, Permission::VIEW_CHANNEL))
accessible.insert(channel);
}
m_store.SetRole(data.GuildID, data.Role);
m_signal_role_update.emit(data.GuildID, data.Role.ID);
for (auto channel : channels) {
const bool old_perms = accessible.find(channel) != accessible.end();
const bool new_perms = HasChannelPermission(m_user_data.ID, channel, Permission::VIEW_CHANNEL);
if (old_perms && !new_perms) {
m_signal_channel_accessibility_changed.emit(channel, false);
} else if (!old_perms && new_perms) {
m_signal_channel_accessibility_changed.emit(channel, true);
}
}
}
void DiscordClient::HandleGatewayGuildRoleCreate(const GatewayMessage &msg) {
GuildRoleCreateObject data = msg.Data;
auto guild = *m_store.GetGuild(data.GuildID);
guild.Roles->push_back(data.Role);
m_store.BeginTransaction();
m_store.SetRole(guild.ID, data.Role);
m_store.SetGuild(guild.ID, guild);
m_store.EndTransaction();
m_store.SetRole(data.GuildID, data.Role);
m_signal_role_create.emit(data.GuildID, data.Role.ID);
}
void DiscordClient::HandleGatewayGuildRoleDelete(const GatewayMessage &msg) {
GuildRoleDeleteObject data = msg.Data;
auto guild = *m_store.GetGuild(data.GuildID);
const auto pred = [id = data.RoleID](const RoleData &role) -> bool {
return role.ID == id;
};
guild.Roles->erase(std::remove_if(guild.Roles->begin(), guild.Roles->end(), pred), guild.Roles->end());
m_store.SetGuild(guild.ID, guild);
m_store.ClearRole(data.RoleID);
m_signal_role_delete.emit(data.GuildID, data.RoleID);
}
@@ -2152,7 +2175,7 @@ void DiscordClient::HandleGatewayGuildCreate(const GatewayMessage &msg) {
void DiscordClient::HandleGatewayGuildDelete(const GatewayMessage &msg) {
Snowflake id = msg.Data.at("id");
bool unavailable = msg.Data.contains("unavilable") && msg.Data.at("unavailable").get<bool>();
bool unavailable = msg.Data.contains("unavailable") && msg.Data.at("unavailable").get<bool>();
if (unavailable)
printf("guild %" PRIu64 " became unavailable\n", static_cast<uint64_t>(id));
@@ -2165,9 +2188,12 @@ void DiscordClient::HandleGatewayGuildDelete(const GatewayMessage &msg) {
}
m_store.ClearGuild(id);
if (guild->Channels.has_value())
for (const auto &c : *guild->Channels)
if (guild->Channels.has_value()) {
for (const auto &c : *guild->Channels) {
m_store.ClearChannel(c.ID);
m_signal_channel_accessibility_changed.emit(c.ID, false);
}
}
m_signal_guild_delete.emit(id);
}
@@ -2657,6 +2683,10 @@ DiscordClient::type_signal_guild_unmuted DiscordClient::signal_guild_unmuted() {
return m_signal_guild_unmuted;
}
DiscordClient::type_signal_channel_accessibility_changed DiscordClient::signal_channel_accessibility_changed() {
return m_signal_channel_accessibility_changed;
}
DiscordClient::type_signal_message_send_fail DiscordClient::signal_message_send_fail() {
return m_signal_message_send_fail;
}

View File

@@ -49,7 +49,7 @@ class DiscordClient {
public:
DiscordClient(bool mem_store = false);
void Start();
void Stop();
bool Stop();
bool IsStarted() const;
bool IsStoreValid() const;
@@ -396,6 +396,7 @@ public:
typedef sigc::signal<void, Snowflake> type_signal_channel_unmuted;
typedef sigc::signal<void, Snowflake> type_signal_guild_muted;
typedef sigc::signal<void, Snowflake> type_signal_guild_unmuted;
typedef sigc::signal<void, Snowflake, bool> type_signal_channel_accessibility_changed;
typedef sigc::signal<void, std::string /* nonce */, float /* retry_after */> type_signal_message_send_fail; // retry after param will be 0 if it failed for a reason that isnt slowmode
typedef sigc::signal<void, bool, GatewayCloseCode> type_signal_disconnected; // bool true if reconnecting
@@ -449,6 +450,7 @@ public:
type_signal_channel_unmuted signal_channel_unmuted();
type_signal_guild_muted signal_guild_muted();
type_signal_guild_unmuted signal_guild_unmuted();
type_signal_channel_accessibility_changed signal_channel_accessibility_changed();
type_signal_message_send_fail signal_message_send_fail();
type_signal_disconnected signal_disconnected();
type_signal_connected signal_connected();
@@ -502,6 +504,7 @@ protected:
type_signal_channel_unmuted m_signal_channel_unmuted;
type_signal_guild_muted m_signal_guild_muted;
type_signal_guild_unmuted m_signal_guild_unmuted;
type_signal_channel_accessibility_changed m_signal_channel_accessibility_changed;
type_signal_message_send_fail m_signal_message_send_fail;
type_signal_disconnected m_signal_disconnected;
type_signal_connected m_signal_connected;

View File

@@ -1078,6 +1078,14 @@ void Store::ClearRecipient(Snowflake channel_id, Snowflake user_id) {
s->Reset();
}
void Store::ClearRole(Snowflake id) {
auto &s = m_stmt_clr_role;
s->Bind(1, id);
s->Step();
s->Reset();
}
std::unordered_set<Snowflake> Store::GetChannels() const {
auto &s = m_stmt_get_chan_ids;
std::unordered_set<Snowflake> r;
@@ -1522,6 +1530,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;
}
@@ -2159,6 +2177,15 @@ bool Store::CreateStatements() {
return false;
}
m_stmt_clr_role = std::make_unique<Statement>(m_db, R"(
DELETE FROM roles
WHERE id = ?1;
)");
if (!m_stmt_clr_role->OK()) {
fprintf(stderr, "failed to prepare clear role statement: %s\n", m_db.ErrStr());
return false;
}
return true;
}

View File

@@ -54,6 +54,7 @@ public:
void ClearChannel(Snowflake id);
void ClearBan(Snowflake guild_id, Snowflake user_id);
void ClearRecipient(Snowflake channel_id, Snowflake user_id);
void ClearRole(Snowflake id);
std::unordered_set<Snowflake> GetChannels() const;
std::unordered_set<Snowflake> GetGuilds() const;
@@ -305,5 +306,6 @@ private:
STMT(get_reactions);
STMT(get_chan_ids_parent);
STMT(get_guild_member_ids);
STMT(clr_role);
#undef STMT
};

View File

@@ -24,9 +24,18 @@ void from_json(const nlohmann::json &j, ExpansionState &m) {
j.at("c").get_to(m.Children);
}
void to_json(nlohmann::json &j, const TabsState &m) {
j = m.Channels;
}
void from_json(const nlohmann::json &j, TabsState &m) {
j.get_to(m.Channels);
}
void to_json(nlohmann::json &j, const AbaddonApplicationState &m) {
j["active_channel"] = m.ActiveChannel;
j["expansion"] = m.Expansion;
j["tabs"] = m.Tabs;
}
void from_json(const nlohmann::json &j, AbaddonApplicationState &m) {
@@ -34,4 +43,6 @@ void from_json(const nlohmann::json &j, AbaddonApplicationState &m) {
j.at("active_channel").get_to(m.ActiveChannel);
if (j.contains("expansion"))
j.at("expansion").get_to(m.Expansion);
if (j.contains("tabs"))
j.at("tabs").get_to(m.Tabs);
}

View File

@@ -1,3 +1,4 @@
#pragma once
#include <vector>
#include <nlohmann/json.hpp>
#include "discord/snowflake.hpp"
@@ -18,9 +19,17 @@ struct ExpansionState {
friend void from_json(const nlohmann::json &j, ExpansionState &m);
};
struct TabsState {
std::vector<Snowflake> Channels;
friend void to_json(nlohmann::json &j, const TabsState &m);
friend void from_json(const nlohmann::json &j, TabsState &m);
};
struct AbaddonApplicationState {
Snowflake ActiveChannel;
ExpansionStateRoot Expansion;
TabsState Tabs;
friend void to_json(nlohmann::json &j, const AbaddonApplicationState &m);
friend void from_json(const nlohmann::json &j, AbaddonApplicationState &m);

View File

@@ -1,5 +1,6 @@
#include "util.hpp"
#include <array>
#include <cstring>
#include <filesystem>
void LaunchBrowser(const Glib::ustring &url) {

View File

@@ -27,6 +27,12 @@ MainWindow::MainWindow()
chat->set_hexpand(true);
chat->show();
#ifdef WITH_LIBHANDY
m_channel_list.signal_action_open_new_tab().connect([this](Snowflake id) {
m_chat.OpenNewTab(id);
});
#endif
m_channel_list.set_vexpand(true);
m_channel_list.set_size_request(-1, -1);
m_channel_list.show();
@@ -47,8 +53,10 @@ MainWindow::MainWindow()
m_chan_content_paned.pack1(m_channel_list);
m_chan_content_paned.pack2(m_content_members_paned);
m_chan_content_paned.child_property_shrink(m_channel_list) = false;
m_chan_content_paned.child_property_resize(m_channel_list) = false;
m_chan_content_paned.child_property_shrink(m_content_members_paned) = true;
m_chan_content_paned.child_property_resize(m_content_members_paned) = true;
m_chan_content_paned.child_property_shrink(m_channel_list) = true;
m_chan_content_paned.child_property_resize(m_channel_list) = true;
m_chan_content_paned.set_position(200);
m_chan_content_paned.show();
m_content_box.add(m_chan_content_paned);
@@ -56,8 +64,10 @@ MainWindow::MainWindow()
m_content_members_paned.pack1(m_content_stack);
m_content_members_paned.pack2(*member_list);
m_content_members_paned.child_property_shrink(*member_list) = false;
m_content_members_paned.child_property_resize(*member_list) = false;
m_content_members_paned.child_property_shrink(m_content_stack) = true;
m_content_members_paned.child_property_resize(m_content_stack) = true;
m_content_members_paned.child_property_shrink(*member_list) = true;
m_content_members_paned.child_property_resize(*member_list) = true;
int w, h;
get_default_size(w, h); // :s
m_content_members_paned.set_position(w - m_chan_content_paned.get_position() - 150);
@@ -95,10 +105,10 @@ void MainWindow::UpdateChatWindowContents() {
m_members.UpdateMemberList();
}
void MainWindow::UpdateChatActiveChannel(Snowflake id) {
void MainWindow::UpdateChatActiveChannel(Snowflake id, bool expand_to) {
m_chat.SetActiveChannel(id);
m_members.SetActiveChannel(id);
m_channel_list.SetActiveChannel(id);
m_channel_list.SetActiveChannel(id, expand_to);
m_content_stack.set_visible_child("chat");
}
@@ -142,6 +152,33 @@ void MainWindow::UpdateChatReactionRemove(Snowflake id, const Glib::ustring &par
m_chat.UpdateReactions(id);
}
void MainWindow::UpdateMenus() {
OnDiscordSubmenuPopup();
OnViewSubmenuPopup();
}
#ifdef WITH_LIBHANDY
void MainWindow::GoBack() {
m_chat.GoBack();
}
void MainWindow::GoForward() {
m_chat.GoForward();
}
void MainWindow::GoToPreviousTab() {
m_chat.GoToPreviousTab();
}
void MainWindow::GoToNextTab() {
m_chat.GoToNextTab();
}
void MainWindow::GoToTab(int idx) {
m_chat.GoToTab(idx);
}
#endif
void MainWindow::OnDiscordSubmenuPopup() {
auto &discord = Abaddon::Get().GetDiscordClient();
auto channel_id = GetChatActiveChannel();
@@ -227,22 +264,26 @@ void MainWindow::SetupMenu() {
m_menu_view_threads.set_label("Threads");
m_menu_view_mark_guild_as_read.set_label("Mark Server as Read");
m_menu_view_mark_guild_as_read.add_accelerator("activate", m_accels, GDK_KEY_Escape, Gdk::SHIFT_MASK, Gtk::ACCEL_VISIBLE);
#ifdef WITH_LIBHANDY
m_menu_view_go_back.set_label("Go Back");
m_menu_view_go_forward.set_label("Go Forward");
m_menu_view_go_back.add_accelerator("activate", m_accels, GDK_KEY_Left, Gdk::MOD1_MASK, Gtk::ACCEL_VISIBLE);
m_menu_view_go_forward.add_accelerator("activate", m_accels, GDK_KEY_Right, Gdk::MOD1_MASK, Gtk::ACCEL_VISIBLE);
#endif
m_menu_view_sub.append(m_menu_view_friends);
m_menu_view_sub.append(m_menu_view_pins);
m_menu_view_sub.append(m_menu_view_threads);
m_menu_view_sub.append(m_menu_view_mark_guild_as_read);
#ifdef WITH_LIBHANDY
m_menu_view_sub.append(m_menu_view_go_back);
m_menu_view_sub.append(m_menu_view_go_forward);
#endif
m_menu_bar.append(m_menu_file);
m_menu_bar.append(m_menu_discord);
m_menu_bar.append(m_menu_view);
m_menu_bar.show_all();
m_menu_bar.signal_event().connect([this](GdkEvent *ev) -> bool {
OnViewSubmenuPopup();
OnDiscordSubmenuPopup();
return false;
});
m_menu_discord_connect.signal_activate().connect([this] {
m_signal_action_connect.emit();
});
@@ -276,7 +317,7 @@ void MainWindow::SetupMenu() {
});
m_menu_view_friends.signal_activate().connect([this] {
UpdateChatActiveChannel(Snowflake::Invalid);
UpdateChatActiveChannel(Snowflake::Invalid, true);
m_members.UpdateMemberList();
m_content_stack.set_visible_child("friends");
});
@@ -297,6 +338,16 @@ void MainWindow::SetupMenu() {
discord.MarkGuildAsRead(*channel->GuildID, NOOP_CALLBACK);
}
});
#ifdef WITH_LIBHANDY
m_menu_view_go_back.signal_activate().connect([this] {
GoBack();
});
m_menu_view_go_forward.signal_activate().connect([this] {
GoForward();
});
#endif
}
MainWindow::type_signal_action_connect MainWindow::signal_action_connect() {

View File

@@ -13,7 +13,7 @@ public:
void UpdateMembers();
void UpdateChannelListing();
void UpdateChatWindowContents();
void UpdateChatActiveChannel(Snowflake id);
void UpdateChatActiveChannel(Snowflake id, bool expand_to);
Snowflake GetChatActiveChannel() const;
void UpdateChatNewMessage(const Message &data);
void UpdateChatMessageDeleted(Snowflake id, Snowflake channel_id);
@@ -23,6 +23,15 @@ public:
Snowflake GetChatOldestListedMessage();
void UpdateChatReactionAdd(Snowflake id, const Glib::ustring &param);
void UpdateChatReactionRemove(Snowflake id, const Glib::ustring &param);
void UpdateMenus();
#ifdef WITH_LIBHANDY
void GoBack();
void GoForward();
void GoToPreviousTab();
void GoToNextTab();
void GoToTab(int idx);
#endif
ChannelList *GetChannelList();
ChatWindow *GetChatWindow();
@@ -67,6 +76,10 @@ private:
Gtk::MenuItem m_menu_view_pins;
Gtk::MenuItem m_menu_view_threads;
Gtk::MenuItem m_menu_view_mark_guild_as_read;
#ifdef WITH_LIBHANDY
Gtk::MenuItem m_menu_view_go_back;
Gtk::MenuItem m_menu_view_go_forward;
#endif
void OnViewSubmenuPopup();
public: