diff --git a/.ci/install.sh b/.ci/install.sh index 59114ba..72c3412 100755 --- a/.ci/install.sh +++ b/.ci/install.sh @@ -2,10 +2,10 @@ set -ex -if [ $TRAVIS_OS_NAME == osx ]; then +if [ "$TRAVIS_OS_NAME" == "osx" ]; then brew update brew install qt5 lmdb clang-format ninja libsodium cmark - brew upgrade boost cmake || true + brew upgrade boost cmake icu4c || true curl https://bootstrap.pypa.io/get-pip.py -o get-pip.py sudo python get-pip.py @@ -17,7 +17,7 @@ if [ $TRAVIS_OS_NAME == osx ]; then fi -if [ $TRAVIS_OS_NAME == linux ]; then +if [ "$TRAVIS_OS_NAME" == "linux" ]; then if [ -z "$QT_VERSION" ]; then QT_VERSION="592" diff --git a/.ci/linux/deploy.sh b/.ci/linux/deploy.sh index 4e71600..38317a4 100755 --- a/.ci/linux/deploy.sh +++ b/.ci/linux/deploy.sh @@ -36,8 +36,10 @@ export LD_LIBRARY_PATH=.deps/usr/lib/:$LD_LIBRARY_PATH ./linuxdeployqt*.AppImage ${DIR}/usr/share/applications/*.desktop -bundle-non-qt-libs ./linuxdeployqt*.AppImage ${DIR}/usr/share/applications/*.desktop -appimage -chmod +x nheko-x86_64.AppImage +chmod +x nheko-*x86_64.AppImage -if [ ! -z $TRAVIS_TAG ]; then - mv nheko-x86_64.AppImage nheko-${TRAVIS_TAG}-x86_64.AppImage -fi +if [ ! -z $VERSION ]; then + # commented out for now, as AppImage file appears to already contain the version. + #mv nheko-*x86_64.AppImage nheko-${VERSION}-x86_64.AppImage + echo "nheko-${VERSION}-x86_64.AppImage" +fi \ No newline at end of file diff --git a/.ci/macos/deploy.sh b/.ci/macos/deploy.sh index 1de95a4..7970124 100755 --- a/.ci/macos/deploy.sh +++ b/.ci/macos/deploy.sh @@ -8,7 +8,15 @@ TAG=`git tag -l --points-at HEAD` PATH=/usr/local/opt/qt/bin/:${PATH} pushd build -sudo macdeployqt nheko.app -dmg + +# macdeployqt does not copy symlinks over. +# this specifically addresses icu4c issues but nothing else. +export ICU_LIB="$(brew --prefix icu4c)/lib" +mkdir -p nheko.app/Contents/Frameworks +find ${ICU_LIB} -type l -name "*.dylib" -exec cp -a -n {} nheko.app/Contents/Frameworks/ \; || true + +sudo macdeployqt nheko.app -dmg -always-overwrite + user=$(id -nu) sudo chown ${user} nheko.dmg mv nheko.dmg .. @@ -16,6 +24,6 @@ popd dmgbuild -s ./.ci/macos/settings.json "Nheko" nheko.dmg -if [ ! -z $TRAVIS_TAG ]; then - mv nheko.dmg nheko-${TRAVIS_TAG}.dmg +if [ ! -z $VERSION ]; then + mv nheko.dmg nheko-${VERSION}.dmg fi diff --git a/.ci/script.sh b/.ci/script.sh index 622bda9..ce65cb1 100755 --- a/.ci/script.sh +++ b/.ci/script.sh @@ -2,7 +2,7 @@ set -ex -if [ $TRAVIS_OS_NAME == linux ]; then +if [ "$TRAVIS_OS_NAME" == "linux" ]; then export CC=${C_COMPILER} export CXX=${CXX_COMPILER} @@ -13,11 +13,11 @@ if [ $TRAVIS_OS_NAME == linux ]; then sudo update-alternatives --set g++ "/usr/bin/${CXX_COMPILER}" fi -if [ $TRAVIS_OS_NAME == linux ]; then +if [ "$TRAVIS_OS_NAME" == "linux" ]; then source /opt/qt${QT_PKG}/bin/qt${QT_PKG}-env.sh || true; fi -if [ $TRAVIS_OS_NAME == osx ]; then +if [ "$TRAVIS_OS_NAME" == "osx" ]; then export CMAKE_PREFIX_PATH=/usr/local/opt/qt5 fi @@ -33,14 +33,14 @@ cmake -GNinja -H. -Bbuild \ -DCMAKE_INSTALL_PREFIX=.deps/usr cmake --build build -if [ $TRAVIS_OS_NAME == osx ]; then +if [ "$TRAVIS_OS_NAME" == "osx" ]; then make lint; - if [ $DEPLOYMENT == 1 ] && [ ! -z $TRAVIS_TAG ]; then + if [ $DEPLOYMENT == 1 ] && [ ! -z $VERSION ] ; then make macos-deploy; fi fi -if [ $TRAVIS_OS_NAME == linux ] && [ $DEPLOYMENT == 1 ] && [ ! -z $TRAVIS_TAG ]; then +if [ "$TRAVIS_OS_NAME" == "linux" ] && [ $DEPLOYMENT == 1 ] && [ ! -z $VERSION ]; then make linux-deploy; fi diff --git a/.travis.yml b/.travis.yml index 77d0083..afe6fb6 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,118 +1,112 @@ language: cpp sudo: required dist: trusty + notifications: - email: false webhooks: urls: - https://scalar.vector.im/api/neb/services/hooks/dHJhdmlzLWNpLyU0MHJlZF9za3klM0FvY2Vhbi5qb2Vkb25vZnJ5LmNvbS8lMjFldkFxa1BIWnVQSElHZWVuaGklM0FvY2Vhbi5qb2Vkb25vZnJ5LmNvbQ on_success: always on_failure: always on_start: never + email: false + matrix: - include: - - os: osx - osx_image: xcode9 - compiler: clang - env: - - DEPLOYMENT=1 - - USE_BUNDLED_BOOST=0 - - USE_BUNDLED_CMARK=0 - - os: linux - compiler: gcc - env: - - CXX_COMPILER=g++-5 - - C_COMPILER=gcc-5 - - QT_VERSION="-5.10.1" - - QT_PKG=510 - - DEPLOYMENT=1 - - USE_BUNDLED_BOOST=1 - - USE_BUNDLED_CMARK=1 - addons: - apt: - sources: - - ubuntu-toolchain-r-test - packages: - - g++-5 - - ninja-build - - os: linux - compiler: gcc - env: - - CXX_COMPILER=g++-8 - - C_COMPILER=gcc-8 - - QT_VERSION=571 - - QT_PKG=57 - - USE_BUNDLED_BOOST=1 - - USE_BUNDLED_CMARK=1 - addons: - apt: - sources: - - ubuntu-toolchain-r-test - packages: - - g++-8 - - ninja-build - - os: linux - compiler: clang - env: - - CXX_COMPILER=clang++-5.0 - - C_COMPILER=clang-5.0 - - QT_VERSION=592 - - QT_PKG=59 - - USE_BUNDLED_BOOST=1 - - USE_BUNDLED_CMARK=1 - addons: - apt: - sources: - - ubuntu-toolchain-r-test - - llvm-toolchain-trusty-5.0 - packages: - - clang-5.0 - - g++-7 - - ninja-build + include: + - os: osx + compiler: clang + osx_image: xcode9 + env: + - DEPLOYMENT=1 + - USE_BUNDLED_BOOST=0 + - USE_BUNDLED_CMARK=0 + - os: linux + compiler: gcc + env: + - CXX_COMPILER=g++-5 + - C_COMPILER=gcc-5 + - QT_VERSION="-5.10.1" + - QT_PKG=510 + - DEPLOYMENT=1 + - USE_BUNDLED_BOOST=1 + - USE_BUNDLED_CMARK=1 + addons: + apt: + sources: ["ubuntu-toolchain-r-test"] + packages: ["g++-5", "ninja-build"] + - os: linux + compiler: gcc + env: + - CXX_COMPILER=g++-8 + - C_COMPILER=gcc-8 + - QT_VERSION=571 + - QT_PKG=57 + - USE_BUNDLED_BOOST=1 + - USE_BUNDLED_CMARK=1 + addons: + apt: + sources: ["ubuntu-toolchain-r-test"] + packages: ["g++-8", "ninja-build"] + - os: linux + compiler: clang + env: + - CXX_COMPILER=clang++-5.0 + - C_COMPILER=clang-5.0 + - QT_VERSION=592 + - QT_PKG=59 + - USE_BUNDLED_BOOST=1 + - USE_BUNDLED_CMARK=1 + addons: + apt: + sources: ["ubuntu-toolchain-r-test", "llvm-toolchain-trusty-5.0"] + packages: ["clang-5.0", "g++-7", "ninja-build"] + before_install: -- export CXX=${CXX_COMPILER} -- export CC=${C_COMPILER} + - export CXX=${CXX_COMPILER} + - export CC=${C_COMPILER} + # Use TRAVIS_TAG if defined, or the short commit SHA otherwise + - export VERSION=${TRAVIS_TAG:-$(git rev-parse --short HEAD)} install: -- "./.ci/install.sh" -- export PATH=/usr/local/bin:${PATH} + - ./.ci/install.sh + - export PATH=/usr/local/bin:${PATH} + script: -- "./.ci/script.sh" -- sed -i -e "s/VERSION_NAME_VALUE/${TRAVIS_TAG}/g" ./.ci/bintray-release.json || true -- cp ./.ci/bintray-release.json . + - ./.ci/script.sh + - sed -i -e "s/VERSION_NAME_VALUE/${VERSION}/g" ./.ci/bintray-release.json || true + - cp ./.ci/bintray-release.json . deploy: - - provider: bintray - user: redsky17 - key: - secure: CAVzWZPxYSOTollo9bpD4tvEbfxXjqelc32aApV48GKyJrMQljQ+mvSe25BuUtnDehxnw8affgGX23AYXmvG8P7w4hM2d7//8Lgan1zCmusV8JE432jknev6X641B4cvrywqSe0Dj3l0kS9Xgirq4BGavlI0y2vUjeJfQEv0y8GYoI72LwgyH0i82v/1Qi92Fh8429IJIb0eKmC1wGWXCmo2kd8StZRL5mSlc4TmyWI0SHpA5GrLMiQwLAuD7DjDl5mpaK2yQx+H4vBcI2SUMvmlHGgVjXikJG5gURlHbnIaaBFvO67INc1/65KtMokWuMP12zxqJiaMPtsAskOpQv4FLAYDfnigH3NxufyOIGp2cxS5RhJDQhbNsxHEDnUo1kHcO23ZYNWCuC1yUdn0RXzKhWcUsz8mKF8KJs22Ty4VjfUMZ+vqK/AbHyq4rkl8DizVRZqKF1KjSWrSv/2sT4itnHk9pmcgxAYfGuALcjrJJveI4MTwDhzXB62CKnMOqLq3sAMqvE0+BdA0BykQr7qrKtptuyP2/OFx6RDbfHQl5Klkb6cSOjxm0oUzh/8iaxgsVdCrhfE67eqkhFZ+a8lJkB/rZ4zSK1Q2Cp4nLtnxenUCW+Ptk2l7zZN6kXM1/+tcgqVROChYJ6asMUpsjFOOAVQ8SZ4TcxX1rq+pxlA= - skip_cleanup: true - overwrite: true - file: bintray-release.json - on: - condition: "$DEPLOYMENT == 1" - repo: Nheko-Reborn/nheko - tags: true - deploy: - - skip_cleanup: true - overwrite: true - provider: releases - api_key: - secure: JROFCxI1Dj0j8GKftCk1M16PovGmbCQb/i6JKm+YKWIhoYoMJBFl3TzhN0D0KlT8VeClZ0WV4MOom6elAkxlYTGR1kcoJ5ESt/AS0B1ULxq2exbmqzqJgJJBb65JVo4nglLHZPnUHOY5s/QGtg05nPeexcK8b3lFvhMRI+Y5jqX9i4FGsEBk6tG2OLfXB0odU8f6rhEeIGWgJw1LVyiTk3VoQcJtBi7Vsg3p4othMaLDlkVHsepNY+xSO14NbNpUjXSzYWZJEM9HqCOaOlAjZR5Q0Ad365TqN9zj6NOVwxEdN4Zl3/Ux838Or6TobhdhGjqqO2JmWt6C+xV4XJ9wX+8LPb+hYYVBrItp32g3grtW/e4nNsp4j3nm1P87kzKPxC4oAaskyn0dlwC4Vo3LH67beQiceAIuM9ywej4Zwr94+MeKjIVtqI6Qz7Tjlt1pFGI1lmfkKQOXiFlkwPbyCPV3smpJ1WSOC4Npkht6tFPBlLV2DFySYUMRAdH2RwBxWhjzwsSJlx/dEKUUL5yffKtg2tANM6aCCyXMEqEXXVkFe9e9ymPbGmmQuf56xo3rYQj5BcQWA9JHAancqLkxoR0rbRBBmai5qDQP7rBss/HR7Uec5xSnYkS6YYI9zpZ+FTfPa7lnVI3c8hj+ukua1EnsYytB8F8l95jrO8fnTxU= - file_glob: true - file: - - nheko-${TRAVIS_TAG}-x86_64.AppImage - on: - condition: "$TRAVIS_OS_NAME == linux && $DEPLOYMENT == 1" - repo: Nheko-Reborn/nheko - tags: true - - skip_cleanup: true - overwrite: true - provider: releases - api_key: - secure: JROFCxI1Dj0j8GKftCk1M16PovGmbCQb/i6JKm+YKWIhoYoMJBFl3TzhN0D0KlT8VeClZ0WV4MOom6elAkxlYTGR1kcoJ5ESt/AS0B1ULxq2exbmqzqJgJJBb65JVo4nglLHZPnUHOY5s/QGtg05nPeexcK8b3lFvhMRI+Y5jqX9i4FGsEBk6tG2OLfXB0odU8f6rhEeIGWgJw1LVyiTk3VoQcJtBi7Vsg3p4othMaLDlkVHsepNY+xSO14NbNpUjXSzYWZJEM9HqCOaOlAjZR5Q0Ad365TqN9zj6NOVwxEdN4Zl3/Ux838Or6TobhdhGjqqO2JmWt6C+xV4XJ9wX+8LPb+hYYVBrItp32g3grtW/e4nNsp4j3nm1P87kzKPxC4oAaskyn0dlwC4Vo3LH67beQiceAIuM9ywej4Zwr94+MeKjIVtqI6Qz7Tjlt1pFGI1lmfkKQOXiFlkwPbyCPV3smpJ1WSOC4Npkht6tFPBlLV2DFySYUMRAdH2RwBxWhjzwsSJlx/dEKUUL5yffKtg2tANM6aCCyXMEqEXXVkFe9e9ymPbGmmQuf56xo3rYQj5BcQWA9JHAancqLkxoR0rbRBBmai5qDQP7rBss/HR7Uec5xSnYkS6YYI9zpZ+FTfPa7lnVI3c8hj+ukua1EnsYytB8F8l95jrO8fnTxU= - file: nheko-${TRAVIS_TAG}.dmg - on: - condition: "$TRAVIS_OS_NAME == osx && $DEPLOYMENT == 1" - repo: Nheko-Reborn/nheko - tags: true +- provider: bintray + user: "redsky17" key: - secure: q4V4k6mAEDBgA/13NiCw+5/Jh7/xmtRBybFSr/0I6JTatkaLs2pj4zIRyHIrBVZtOd1oFVmq6aDHXXR+fbSo1Euj3s5Eo+7TTAqKAlyRMcme/+0S0bfZfisA+6LskZcmacq1FzEkAXd5OjMXB6rIakDC1sgkgo5bpM9w5r/9ZFXXgCfDzW07LJOypDyph0Gg4rlU22o5fAcKmuglTgbJWceznv46HcHvW1s2JzkJQugpAh8LkpiEkuNnH1wZ0WDI1wQQFI+ti5GSBkHicS2kgkOL3IlCvfzS0ym85XF1FTncqDEClxudwWOhVm3qpSOm28I+lB4i0ha1LNzsl4S8ClVTxJRJMJBHmLmkh3lOasAn6v8Vc2WASygfnTC2VGMaRWYMfphLm7e1CcT8OPfoNcEJLvR6YTxgm7AadomOV4f8q9FUwvrkyJkbR+sV+DkJ5yQ/uF1pDOMnUUzjDYpCfYXEECqh8gH8iUXhWabrJyaFlzZaOsai/ujyepLOkUtJaGcOrnCHlOQlfXgBhmOCUFau8ByJhSrHGGlBPb9JhC/jzWq++dmN/5zn1coc4kNqKB55h1AFVtTTW7t14RzNKER2/opl7LFoywvyMyERusmxHfGzNihFHO4GoBY+WtEpphCAdqCjLaJM95w9spQ0sgR0/qy4883MhTWBctT9K6s= + secure: "2C+ESOClZdNCDzfUfE8Rcdn9+BM3/5x1lebGS0qRxICMAuylx50C8RAxlqKIFNI1J+IMnj5xhK+1oWsLg6wUdo8B3JocvM6P1NbC/WwfY5UJVwtSmzcqvEC+IjM90Bng8OoM5f6ED3IAUyYPBgo8J14+2l5Oruvr99t0mrpniLXDf66TJrI9u/+05JYa2pm0EWlQkDVaziI+96zAt1pMIX9Z/ToTmUbq2OAlHaN8H4hR5aBRmYuF11FQVyARK64eqRsBV4RXKJ9DjW7BR+pQV008hQUoXrCxK8xIf/Qph9h1fz63961NJxfru62LeuZ4Z8MUcV9L+p0HYZzkhNdn5Z3QFf3SXbrVCYs6LK6+uKHpmYS7vB5XwjDx9SWc2szsd5OVKJg3YEdBcC4KUSL3VEbBHDSQ3GFsVKoBsZvopwHkyCmPotY47HtEiPBLdEcsOvcVyeU5Gfq9EOp9dB+nJbNrujBgav1l13CorkBG2rgD+NRivV1otclXnPsIj85ySrc6xk0VDN3zogLAdLw0f+fNOaK17N8GmhPbCu/iX2Vjem/fdjh4NPC4pSNDei2MoWUfqGBmoTxuQEsZeywigwV2c51W9RZuwo8X810N3ehVcBmmNyLjbKmYT+8Kp+dJbaf/ErFnvdZl06mX2bgyxFakv5iQY3dwX4WYHvl77c4=" + skip_cleanup: true + overwrite: true + file: bintray-release.json + on: + condition: "$DEPLOYMENT == 1" + repo: Nheko-Reborn/nheko + tags: false + all_branches: true + deploy: +- skip_cleanup: true + overwrite: true + provider: releases + api_key: + secure: "JROFCxI1Dj0j8GKftCk1M16PovGmbCQb/i6JKm+YKWIhoYoMJBFl3TzhN0D0KlT8VeClZ0WV4MOom6elAkxlYTGR1kcoJ5ESt/AS0B1ULxq2exbmqzqJgJJBb65JVo4nglLHZPnUHOY5s/QGtg05nPeexcK8b3lFvhMRI+Y5jqX9i4FGsEBk6tG2OLfXB0odU8f6rhEeIGWgJw1LVyiTk3VoQcJtBi7Vsg3p4othMaLDlkVHsepNY+xSO14NbNpUjXSzYWZJEM9HqCOaOlAjZR5Q0Ad365TqN9zj6NOVwxEdN4Zl3/Ux838Or6TobhdhGjqqO2JmWt6C+xV4XJ9wX+8LPb+hYYVBrItp32g3grtW/e4nNsp4j3nm1P87kzKPxC4oAaskyn0dlwC4Vo3LH67beQiceAIuM9ywej4Zwr94+MeKjIVtqI6Qz7Tjlt1pFGI1lmfkKQOXiFlkwPbyCPV3smpJ1WSOC4Npkht6tFPBlLV2DFySYUMRAdH2RwBxWhjzwsSJlx/dEKUUL5yffKtg2tANM6aCCyXMEqEXXVkFe9e9ymPbGmmQuf56xo3rYQj5BcQWA9JHAancqLkxoR0rbRBBmai5qDQP7rBss/HR7Uec5xSnYkS6YYI9zpZ+FTfPa7lnVI3c8hj+ukua1EnsYytB8F8l95jrO8fnTxU=" + file_glob: true + file: + - nheko-${VERSION}-x86_64.AppImage + on: + condition: "$TRAVIS_OS_NAME == linux && $DEPLOYMENT == 1" + repo: Nheko-Reborn/nheko + tags: true +- skip_cleanup: true + overwrite: true + provider: releases + api_key: + secure: "JROFCxI1Dj0j8GKftCk1M16PovGmbCQb/i6JKm+YKWIhoYoMJBFl3TzhN0D0KlT8VeClZ0WV4MOom6elAkxlYTGR1kcoJ5ESt/AS0B1ULxq2exbmqzqJgJJBb65JVo4nglLHZPnUHOY5s/QGtg05nPeexcK8b3lFvhMRI+Y5jqX9i4FGsEBk6tG2OLfXB0odU8f6rhEeIGWgJw1LVyiTk3VoQcJtBi7Vsg3p4othMaLDlkVHsepNY+xSO14NbNpUjXSzYWZJEM9HqCOaOlAjZR5Q0Ad365TqN9zj6NOVwxEdN4Zl3/Ux838Or6TobhdhGjqqO2JmWt6C+xV4XJ9wX+8LPb+hYYVBrItp32g3grtW/e4nNsp4j3nm1P87kzKPxC4oAaskyn0dlwC4Vo3LH67beQiceAIuM9ywej4Zwr94+MeKjIVtqI6Qz7Tjlt1pFGI1lmfkKQOXiFlkwPbyCPV3smpJ1WSOC4Npkht6tFPBlLV2DFySYUMRAdH2RwBxWhjzwsSJlx/dEKUUL5yffKtg2tANM6aCCyXMEqEXXVkFe9e9ymPbGmmQuf56xo3rYQj5BcQWA9JHAancqLkxoR0rbRBBmai5qDQP7rBss/HR7Uec5xSnYkS6YYI9zpZ+FTfPa7lnVI3c8hj+ukua1EnsYytB8F8l95jrO8fnTxU=" + file: nheko-${VERSION}.dmg + on: + condition: "$TRAVIS_OS_NAME == osx && $DEPLOYMENT == 1" + repo: Nheko-Reborn/nheko + tags: true \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 6cb83ea..8c49229 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,8 +14,6 @@ ### Other changes - Removed room re-ordering option. -- Removed built-in emoji picker. -- Removed bundled fonts. ## [0.6.1] - 2018-09-26 diff --git a/CMakeLists.txt b/CMakeLists.txt index 6fd936b..ad48e7d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,4 +1,4 @@ -cmake_minimum_required(VERSION 3.1) +cmake_minimum_required(VERSION 3.11) option(APPVEYOR_BUILD "Build on appveyor" OFF) option(ASAN "Compile with address sanitizers" OFF) @@ -183,6 +183,10 @@ set(SRC_FILES src/dialogs/RoomSettings.cpp # Emoji + src/emoji/Category.cpp + src/emoji/ItemDelegate.cpp + src/emoji/Panel.cpp + src/emoji/PickButton.cpp src/emoji/Provider.cpp # Timeline @@ -289,6 +293,9 @@ include_directories(SYSTEM ${TWEENY_INCLUDE_DIR}) include_directories(${CMAKE_SOURCE_DIR}/src) include_directories(${Boost_INCLUDE_DIRS}) +# local inclue directory +include_directories(includes) + qt5_wrap_cpp(MOC_HEADERS # Dialogs src/dialogs/CreateRoom.h @@ -305,6 +312,12 @@ qt5_wrap_cpp(MOC_HEADERS src/dialogs/ReCaptcha.h src/dialogs/RoomSettings.h + # Emoji + src/emoji/Category.h + src/emoji/ItemDelegate.h + src/emoji/Panel.h + src/emoji/PickButton.h + # Timeline src/timeline/TimelineItem.h src/timeline/TimelineView.h diff --git a/README.md b/README.md index 66675d6..f3fad16 100644 --- a/README.md +++ b/README.md @@ -264,5 +264,11 @@ Here is a screen shot to get a feel for the UI, but things will probably change. ![nheko](https://dl.dropboxusercontent.com/s/3zjcezdtk8kqe4i/nheko-v0.4.0.png) +### Third party + +- [Emoji One](http://emojione.com) +- [Font Awesome](http://fontawesome.io/) +- [Open Sans](https://fonts.google.com/specimen/Open+Sans) + [Matrix]:https://matrix.org [Riot]:https://riot.im diff --git a/deps/CMakeLists.txt b/deps/CMakeLists.txt index e7a09db..9d70d10 100644 --- a/deps/CMakeLists.txt +++ b/deps/CMakeLists.txt @@ -1,4 +1,4 @@ -cmake_minimum_required(VERSION 3.1) +cmake_minimum_required(VERSION 3.11) project(NHEKO_DEPS) # Point CMake at any custom modules we may ship @@ -30,6 +30,11 @@ option(USE_BUNDLED_LMDBXX "Use the bundled version of lmdbxx." ${USE_BUNDLED}) option(USE_BUNDLED_MATRIX_CLIENT "Use the bundled version of mtxclient." ${USE_BUNDLED}) +if(USE_BUNDLED_BOOST) + # bundled boost is 1.68, which requires CMake 3.12 or greater. + cmake_minimum_required(VERSION 3.12) +endif() + include(ExternalProject) set(BOOST_URL diff --git a/includes/jdenticoninterface.h b/includes/jdenticoninterface.h new file mode 100644 index 0000000..2108a72 --- /dev/null +++ b/includes/jdenticoninterface.h @@ -0,0 +1,17 @@ +#ifndef JDENTICONINTERFACE_H +#define JDENTICONINTERFACE_H + +#include + +class JdenticonInterface +{ +public: + virtual ~JdenticonInterface() {} + virtual QString generate(const QString &message, uint16_t size) = 0; +}; + +#define JdenticonInterface_iid "redsky17.Qt.JdenticonInterface" + +Q_DECLARE_INTERFACE(JdenticonInterface, JdenticonInterface_iid) + +#endif // JDENTICONINTERFACE_H diff --git a/resources/fonts/EmojiOne/emojione-android.ttf b/resources/fonts/EmojiOne/emojione-android.ttf new file mode 100644 index 0000000..4cd640d Binary files /dev/null and b/resources/fonts/EmojiOne/emojione-android.ttf differ diff --git a/resources/fonts/OpenSans/LICENSE.txt b/resources/fonts/OpenSans/LICENSE.txt new file mode 100644 index 0000000..d645695 --- /dev/null +++ b/resources/fonts/OpenSans/LICENSE.txt @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/resources/fonts/OpenSans/OpenSans-Bold.ttf b/resources/fonts/OpenSans/OpenSans-Bold.ttf new file mode 100644 index 0000000..fd79d43 Binary files /dev/null and b/resources/fonts/OpenSans/OpenSans-Bold.ttf differ diff --git a/resources/fonts/OpenSans/OpenSans-BoldItalic.ttf b/resources/fonts/OpenSans/OpenSans-BoldItalic.ttf new file mode 100644 index 0000000..9bc8009 Binary files /dev/null and b/resources/fonts/OpenSans/OpenSans-BoldItalic.ttf differ diff --git a/resources/fonts/OpenSans/OpenSans-ExtraBold.ttf b/resources/fonts/OpenSans/OpenSans-ExtraBold.ttf new file mode 100644 index 0000000..21f6f84 Binary files /dev/null and b/resources/fonts/OpenSans/OpenSans-ExtraBold.ttf differ diff --git a/resources/fonts/OpenSans/OpenSans-ExtraBoldItalic.ttf b/resources/fonts/OpenSans/OpenSans-ExtraBoldItalic.ttf new file mode 100644 index 0000000..31cb688 Binary files /dev/null and b/resources/fonts/OpenSans/OpenSans-ExtraBoldItalic.ttf differ diff --git a/resources/fonts/OpenSans/OpenSans-Italic.ttf b/resources/fonts/OpenSans/OpenSans-Italic.ttf new file mode 100644 index 0000000..c90da48 Binary files /dev/null and b/resources/fonts/OpenSans/OpenSans-Italic.ttf differ diff --git a/resources/fonts/OpenSans/OpenSans-Light.ttf b/resources/fonts/OpenSans/OpenSans-Light.ttf new file mode 100644 index 0000000..0d38189 Binary files /dev/null and b/resources/fonts/OpenSans/OpenSans-Light.ttf differ diff --git a/resources/fonts/OpenSans/OpenSans-LightItalic.ttf b/resources/fonts/OpenSans/OpenSans-LightItalic.ttf new file mode 100644 index 0000000..68299c4 Binary files /dev/null and b/resources/fonts/OpenSans/OpenSans-LightItalic.ttf differ diff --git a/resources/fonts/OpenSans/OpenSans-Regular.ttf b/resources/fonts/OpenSans/OpenSans-Regular.ttf new file mode 100644 index 0000000..db43334 Binary files /dev/null and b/resources/fonts/OpenSans/OpenSans-Regular.ttf differ diff --git a/resources/fonts/OpenSans/OpenSans-Semibold.ttf b/resources/fonts/OpenSans/OpenSans-Semibold.ttf new file mode 100644 index 0000000..1a7679e Binary files /dev/null and b/resources/fonts/OpenSans/OpenSans-Semibold.ttf differ diff --git a/resources/fonts/OpenSans/OpenSans-SemiboldItalic.ttf b/resources/fonts/OpenSans/OpenSans-SemiboldItalic.ttf new file mode 100644 index 0000000..59b6d16 Binary files /dev/null and b/resources/fonts/OpenSans/OpenSans-SemiboldItalic.ttf differ diff --git a/resources/icons/emoji-categories/activity.png b/resources/icons/emoji-categories/activity.png new file mode 100644 index 0000000..2d36076 Binary files /dev/null and b/resources/icons/emoji-categories/activity.png differ diff --git a/resources/icons/emoji-categories/activity@2x.png b/resources/icons/emoji-categories/activity@2x.png new file mode 100644 index 0000000..d8f8871 Binary files /dev/null and b/resources/icons/emoji-categories/activity@2x.png differ diff --git a/resources/icons/emoji-categories/flags.png b/resources/icons/emoji-categories/flags.png new file mode 100644 index 0000000..9a52000 Binary files /dev/null and b/resources/icons/emoji-categories/flags.png differ diff --git a/resources/icons/emoji-categories/flags@2x.png b/resources/icons/emoji-categories/flags@2x.png new file mode 100644 index 0000000..4535059 Binary files /dev/null and b/resources/icons/emoji-categories/flags@2x.png differ diff --git a/resources/icons/emoji-categories/foods.png b/resources/icons/emoji-categories/foods.png new file mode 100644 index 0000000..15c3106 Binary files /dev/null and b/resources/icons/emoji-categories/foods.png differ diff --git a/resources/icons/emoji-categories/foods@2x.png b/resources/icons/emoji-categories/foods@2x.png new file mode 100644 index 0000000..bbdd2a3 Binary files /dev/null and b/resources/icons/emoji-categories/foods@2x.png differ diff --git a/resources/icons/emoji-categories/nature.png b/resources/icons/emoji-categories/nature.png new file mode 100644 index 0000000..eb1786c Binary files /dev/null and b/resources/icons/emoji-categories/nature.png differ diff --git a/resources/icons/emoji-categories/nature@2x.png b/resources/icons/emoji-categories/nature@2x.png new file mode 100644 index 0000000..81db5c0 Binary files /dev/null and b/resources/icons/emoji-categories/nature@2x.png differ diff --git a/resources/icons/emoji-categories/objects.png b/resources/icons/emoji-categories/objects.png new file mode 100644 index 0000000..45c6eb3 Binary files /dev/null and b/resources/icons/emoji-categories/objects.png differ diff --git a/resources/icons/emoji-categories/objects@2x.png b/resources/icons/emoji-categories/objects@2x.png new file mode 100644 index 0000000..01fd5cb Binary files /dev/null and b/resources/icons/emoji-categories/objects@2x.png differ diff --git a/resources/icons/emoji-categories/people.png b/resources/icons/emoji-categories/people.png new file mode 100644 index 0000000..710e808 Binary files /dev/null and b/resources/icons/emoji-categories/people.png differ diff --git a/resources/icons/emoji-categories/people@2x.png b/resources/icons/emoji-categories/people@2x.png new file mode 100644 index 0000000..142ba09 Binary files /dev/null and b/resources/icons/emoji-categories/people@2x.png differ diff --git a/resources/icons/emoji-categories/symbols.png b/resources/icons/emoji-categories/symbols.png new file mode 100644 index 0000000..08184de Binary files /dev/null and b/resources/icons/emoji-categories/symbols.png differ diff --git a/resources/icons/emoji-categories/symbols@2x.png b/resources/icons/emoji-categories/symbols@2x.png new file mode 100644 index 0000000..b5e7cc6 Binary files /dev/null and b/resources/icons/emoji-categories/symbols@2x.png differ diff --git a/resources/icons/emoji-categories/travel.png b/resources/icons/emoji-categories/travel.png new file mode 100644 index 0000000..93da773 Binary files /dev/null and b/resources/icons/emoji-categories/travel.png differ diff --git a/resources/icons/emoji-categories/travel@2x.png b/resources/icons/emoji-categories/travel@2x.png new file mode 100644 index 0000000..2f72a28 Binary files /dev/null and b/resources/icons/emoji-categories/travel@2x.png differ diff --git a/resources/res.qrc b/resources/res.qrc index 559d6de..cef5577 100644 --- a/resources/res.qrc +++ b/resources/res.qrc @@ -63,6 +63,22 @@ icons/ui/edit.png icons/ui/edit@2x.png + icons/emoji-categories/people.png + icons/emoji-categories/people@2x.png + icons/emoji-categories/nature.png + icons/emoji-categories/nature@2x.png + icons/emoji-categories/foods.png + icons/emoji-categories/foods@2x.png + icons/emoji-categories/activity.png + icons/emoji-categories/activity@2x.png + icons/emoji-categories/travel.png + icons/emoji-categories/travel@2x.png + icons/emoji-categories/objects.png + icons/emoji-categories/objects@2x.png + icons/emoji-categories/symbols.png + icons/emoji-categories/symbols@2x.png + icons/emoji-categories/flags.png + icons/emoji-categories/flags@2x.png nheko.png @@ -83,6 +99,13 @@ nheko-32.png nheko-16.png + + fonts/OpenSans/OpenSans-Regular.ttf + fonts/OpenSans/OpenSans-Italic.ttf + fonts/OpenSans/OpenSans-Bold.ttf + fonts/OpenSans/OpenSans-Semibold.ttf + fonts/EmojiOne/emojione-android.ttf + styles/system.qss styles/nheko.qss diff --git a/resources/styles/nheko-dark.qss b/resources/styles/nheko-dark.qss index 86056bb..5567f32 100644 --- a/resources/styles/nheko-dark.qss +++ b/resources/styles/nheko-dark.qss @@ -3,6 +3,10 @@ QLabel { color: #caccd1; } +TimelineItem { + qproperty-backgroundColor: #202228; +} + #chatPage, #chatPage > * { background-color: #202228; @@ -86,6 +90,7 @@ RaisedButton { } RoomInfoListItem { + qproperty-mentionedColor: #a82353; qproperty-highlightedBackgroundColor: #4d84c7; qproperty-hoverBackgroundColor: rgba(230, 230, 230, 30); qproperty-backgroundColor: #2d3139; @@ -188,6 +193,18 @@ RegisterPage { color: #caccd1; } +emoji--Panel, +emoji--Panel > * { + background-color: #202228; + color: #caccd1; +} + +emoji--Category, +emoji--Category > * { + background-color: #2d3139; + color: #caccd1; +} + FloatingButton { qproperty-backgroundColor: #2d3139; qproperty-foregroundColor: white; diff --git a/resources/styles/nheko.qss b/resources/styles/nheko.qss index ca5a8f0..58e83c2 100644 --- a/resources/styles/nheko.qss +++ b/resources/styles/nheko.qss @@ -3,6 +3,10 @@ QLabel { color: #333; } +TimelineItem { + qproperty-backgroundColor: white; +} + #chatPage, #chatPage > * { background-color: white; @@ -83,6 +87,7 @@ RaisedButton { } RoomInfoListItem { + qproperty-mentionedColor: #a82353; qproperty-highlightedBackgroundColor: #38A3D8; qproperty-hoverBackgroundColor: rgba(200, 200, 200, 70); qproperty-hoverTitleColor: #f2f5f8; @@ -185,6 +190,18 @@ RegisterPage { color: #333; } +emoji--Panel, +emoji--Panel > * { + background-color: #eee; + color: #333; +} + +emoji--Category, +emoji--Category > * { + background-color: white; + color: #ccc; +} + FloatingButton { qproperty-backgroundColor: #efefef; qproperty-foregroundColor: black; diff --git a/resources/styles/system.qss b/resources/styles/system.qss index 45263e9..c1e8898 100644 --- a/resources/styles/system.qss +++ b/resources/styles/system.qss @@ -3,6 +3,10 @@ TypingDisplay { qproperty-backgroundColor: palette(window); } +TimelineItem { + qproperty-backgroundColor: palette(window); +} + TimelineView, TimelineView > * { border: none; @@ -82,6 +86,7 @@ QListWidget { } RoomInfoListItem { + qproperty-mentionedColor: palette(alternate-base); qproperty-highlightedBackgroundColor: palette(highlight); qproperty-hoverBackgroundColor: palette(base); qproperty-backgroundColor: palette(window); diff --git a/src/Cache.cpp b/src/Cache.cpp index a9094e2..d6a7b50 100644 --- a/src/Cache.cpp +++ b/src/Cache.cpp @@ -2059,6 +2059,7 @@ Cache::roomMembers(const std::string &room_id) QHash Cache::DisplayNames; QHash Cache::AvatarUrls; +QHash Cache::UserColors; QString Cache::displayName(const QString &room_id, const QString &user_id) @@ -2090,6 +2091,16 @@ Cache::avatarUrl(const QString &room_id, const QString &user_id) return QString(); } +QString +Cache::userColor(const QString &user_id) +{ + if (UserColors.contains(user_id)) { + return UserColors[user_id]; + } + + return QString(); +} + void Cache::insertDisplayName(const QString &room_id, const QString &user_id, @@ -2119,3 +2130,21 @@ Cache::removeAvatarUrl(const QString &room_id, const QString &user_id) auto fmt = QString("%1 %2").arg(room_id).arg(user_id); AvatarUrls.remove(fmt); } + +void +Cache::insertUserColor(const QString &user_id, const QString &color_name) +{ + UserColors.insert(user_id, color_name); +} + +void +Cache::removeUserColor(const QString &user_id) +{ + UserColors.remove(user_id); +} + +void +Cache::clearUserColors() +{ + UserColors.clear(); +} \ No newline at end of file diff --git a/src/Cache.h b/src/Cache.h index b730d6f..1a13361 100644 --- a/src/Cache.h +++ b/src/Cache.h @@ -282,13 +282,16 @@ public: static QHash DisplayNames; static QHash AvatarUrls; + static QHash UserColors; static std::string displayName(const std::string &room_id, const std::string &user_id); static QString displayName(const QString &room_id, const QString &user_id); static QString avatarUrl(const QString &room_id, const QString &user_id); + static QString userColor(const QString &user_id); static void removeDisplayName(const QString &room_id, const QString &user_id); static void removeAvatarUrl(const QString &room_id, const QString &user_id); + static void removeUserColor(const QString &user_id); static void insertDisplayName(const QString &room_id, const QString &user_id, @@ -296,6 +299,9 @@ public: static void insertAvatarUrl(const QString &room_id, const QString &user_id, const QString &avatar_url); + static void insertUserColor(const QString &user_id, const QString &color_name); + + static void clearUserColors(); //! Load saved data for the display names & avatars. void populateMembers(); diff --git a/src/ChatPage.cpp b/src/ChatPage.cpp index a60c09c..dd23fb8 100644 --- a/src/ChatPage.cpp +++ b/src/ChatPage.cpp @@ -546,7 +546,9 @@ ChatPage::ChatPage(QSharedPointer userSettings, QWidget *parent) updateTypingUsers(room_id, room.second.ephemeral.typing); updateRoomNotificationCount( - room_id, room.second.unread_notifications.notification_count); + room_id, + room.second.unread_notifications.notification_count, + room.second.unread_notifications.highlight_count); if (room.second.unread_notifications.notification_count > 0) hasNotifications = true; @@ -908,9 +910,11 @@ ChatPage::setGroupViewState(bool isEnabled) } void -ChatPage::updateRoomNotificationCount(const QString &room_id, uint16_t notification_count) +ChatPage::updateRoomNotificationCount(const QString &room_id, + uint16_t notification_count, + uint16_t highlight_count) { - room_list_->updateUnreadMessageCount(room_id, notification_count); + room_list_->updateUnreadMessageCount(room_id, notification_count, highlight_count); } void @@ -1098,7 +1102,8 @@ ChatPage::trySync() void ChatPage::joinRoom(const QString &room) { - const auto room_id = room.toStdString(); + // Percent escape the room ID + const auto room_id = QUrl::toPercentEncoding(room).toStdString(); http::client()->join_room( room_id, [this, room_id](const nlohmann::json &, mtx::http::RequestErr err) { diff --git a/src/ChatPage.h b/src/ChatPage.h index 2c728c1..7d3b327 100644 --- a/src/ChatPage.h +++ b/src/ChatPage.h @@ -148,6 +148,7 @@ signals: const QImage &icon); void updateGroupsInfo(const mtx::responses::JoinedGroups &groups); + void themeChanged(); private slots: void showUnreadMessageNotification(int count); @@ -196,7 +197,9 @@ private: Memberships getMemberships(const std::vector &events) const; //! Update the room with the new notification count. - void updateRoomNotificationCount(const QString &room_id, uint16_t notification_count); + void updateRoomNotificationCount(const QString &room_id, + uint16_t notification_count, + uint16_t highlight_count); //! Send desktop notification for the received messages. void sendDesktopNotifications(const mtx::responses::Notifications &); diff --git a/src/MainWindow.cpp b/src/MainWindow.cpp index 55dbba3..7d9a890 100644 --- a/src/MainWindow.cpp +++ b/src/MainWindow.cpp @@ -17,6 +17,7 @@ #include #include +#include #include #include @@ -112,7 +113,11 @@ MainWindow::MainWindow(QWidget *parent) connect( userSettingsPage_, SIGNAL(trayOptionChanged(bool)), trayIcon_, SLOT(setVisible(bool))); - + connect(userSettingsPage_, &UserSettingsPage::themeChanged, this, []() { + Cache::clearUserColors(); + }); + connect( + userSettingsPage_, &UserSettingsPage::themeChanged, chat_page_, &ChatPage::themeChanged); connect(trayIcon_, SIGNAL(activated(QSystemTrayIcon::ActivationReason)), this, @@ -162,6 +167,10 @@ MainWindow::MainWindow(QWidget *parent) showChatPage(); } + + if (loadJdenticonPlugin()) { + nhlog::ui()->info("loaded jdenticon."); + } } void @@ -475,3 +484,27 @@ MainWindow::showDialog(QWidget *dialog) dialog->raise(); dialog->show(); } + +bool +MainWindow::loadJdenticonPlugin() +{ + QDir pluginsDir(qApp->applicationDirPath()); + + bool plugins = pluginsDir.cd("plugins"); + if (plugins) { + foreach (QString fileName, pluginsDir.entryList(QDir::Files)) { + QPluginLoader pluginLoader(pluginsDir.absoluteFilePath(fileName)); + QObject *plugin = pluginLoader.instance(); + if (plugin) { + jdenticonInteface_ = qobject_cast(plugin); + if (jdenticonInteface_) { + nhlog::ui()->info("Found jdenticon plugin."); + return true; + } + } + } + } + + nhlog::ui()->info("jdenticon plugin not found."); + return false; +} \ No newline at end of file diff --git a/src/MainWindow.h b/src/MainWindow.h index 2336a92..1aadbf4 100644 --- a/src/MainWindow.h +++ b/src/MainWindow.h @@ -31,6 +31,8 @@ #include "dialogs/UserProfile.h" #include "ui/OverlayModal.h" +#include "jdenticoninterface.h" + class ChatPage; class LoadingIndicator; class OverlayModal; @@ -129,6 +131,8 @@ private slots: void removeOverlayProgressBar(); private: + bool loadJdenticonPlugin(); + void showDialog(QWidget *dialog); bool hasActiveUser(); void restoreWindowSize(); @@ -158,4 +162,6 @@ private: //! Overlay modal used to project other widgets. OverlayModal *modal_ = nullptr; LoadingIndicator *spinner_ = nullptr; + + JdenticonInterface *jdenticonInteface_ = nullptr; }; diff --git a/src/RoomInfoListItem.cpp b/src/RoomInfoListItem.cpp index fcf5bd7..f17b383 100644 --- a/src/RoomInfoListItem.cpp +++ b/src/RoomInfoListItem.cpp @@ -101,6 +101,7 @@ RoomInfoListItem::RoomInfoListItem(QString room_id, RoomInfo info, QWidget *pare , roomName_{QString::fromStdString(std::move(info.name))} , isPressed_(false) , unreadMsgCount_(0) + , unreadHighlightedMsgCount_(0) { init(parent); @@ -301,7 +302,11 @@ RoomInfoListItem::paintEvent(QPaintEvent *event) if (unreadMsgCount_ > 0) { QBrush brush; brush.setStyle(Qt::SolidPattern); - brush.setColor(bubbleBgColor()); + if (unreadHighlightedMsgCount_ > 0) { + brush.setColor(mentionedColor()); + } else { + brush.setColor(bubbleBgColor()); + } if (isPressed_) brush.setColor(bubbleFgColor()); @@ -354,9 +359,10 @@ RoomInfoListItem::paintEvent(QPaintEvent *event) } void -RoomInfoListItem::updateUnreadMessageCount(int count) +RoomInfoListItem::updateUnreadMessageCount(int count, int highlightedCount) { - unreadMsgCount_ = count; + unreadMsgCount_ = count; + unreadHighlightedMsgCount_ = highlightedCount; update(); } diff --git a/src/RoomInfoListItem.h b/src/RoomInfoListItem.h index 5fa8985..40c938c 100644 --- a/src/RoomInfoListItem.h +++ b/src/RoomInfoListItem.h @@ -59,14 +59,15 @@ class RoomInfoListItem : public QWidget Q_PROPERTY(QColor hoverTitleColor READ hoverTitleColor WRITE setHoverTitleColor) Q_PROPERTY(QColor hoverSubtitleColor READ hoverSubtitleColor WRITE setHoverSubtitleColor) + Q_PROPERTY(QColor mentionedColor READ mentionedColor WRITE setMentionedColor) Q_PROPERTY(QColor btnColor READ btnColor WRITE setBtnColor) Q_PROPERTY(QColor btnTextColor READ btnTextColor WRITE setBtnTextColor) public: RoomInfoListItem(QString room_id, RoomInfo info, QWidget *parent = 0); - void updateUnreadMessageCount(int count); - void clearUnreadMessageCount() { updateUnreadMessageCount(0); }; + void updateUnreadMessageCount(int count, int highlightedCount); + void clearUnreadMessageCount() { updateUnreadMessageCount(0, 0); }; QString roomId() { return roomId_; } bool isPressed() const { return isPressed_; } @@ -97,6 +98,7 @@ public: QColor bubbleFgColor() const { return bubbleFgColor_; } QColor bubbleBgColor() const { return bubbleBgColor_; } + QColor mentionedColor() const { return mentionedFontColor_; } void setHighlightedBackgroundColor(QColor &color) { highlightedBackgroundColor_ = color; } void setHoverBackgroundColor(QColor &color) { hoverBackgroundColor_ = color; } @@ -120,6 +122,7 @@ public: void setBubbleFgColor(QColor &color) { bubbleFgColor_ = color; } void setBubbleBgColor(QColor &color) { bubbleBgColor_ = color; } + void setMentionedColor(QColor &color) { mentionedFontColor_ = color; } void setRoomName(const QString &name) { roomName_ = name; } void setRoomType(bool isInvite) @@ -184,7 +187,8 @@ private: bool isPressed_ = false; bool hasUnreadMessages_ = true; - int unreadMsgCount_ = 0; + int unreadMsgCount_ = 0; + int unreadHighlightedMsgCount_ = 0; QColor highlightedBackgroundColor_; QColor hoverBackgroundColor_; @@ -206,6 +210,7 @@ private: QRectF declineBtnRegion_; // Fonts + QColor mentionedFontColor_; QFont unreadCountFont_; int bubbleDiameter_; diff --git a/src/RoomList.cpp b/src/RoomList.cpp index c1b080c..1abf353 100644 --- a/src/RoomList.cpp +++ b/src/RoomList.cpp @@ -143,7 +143,7 @@ RoomList::removeRoom(const QString &room_id, bool reset) } void -RoomList::updateUnreadMessageCount(const QString &roomid, int count) +RoomList::updateUnreadMessageCount(const QString &roomid, int count, int highlightedCount) { if (!roomExists(roomid)) { nhlog::ui()->warn("updateUnreadMessageCount: unknown room_id {}", @@ -151,7 +151,7 @@ RoomList::updateUnreadMessageCount(const QString &roomid, int count) return; } - rooms_[roomid]->updateUnreadMessageCount(count); + rooms_[roomid]->updateUnreadMessageCount(count, highlightedCount); calculateUnreadMessageCount(); } diff --git a/src/RoomList.h b/src/RoomList.h index 88e6d1a..155a969 100644 --- a/src/RoomList.h +++ b/src/RoomList.h @@ -68,7 +68,7 @@ signals: public slots: void updateRoomAvatar(const QString &roomid, const QPixmap &img); void highlightSelectedRoom(const QString &room_id); - void updateUnreadMessageCount(const QString &roomid, int count); + void updateUnreadMessageCount(const QString &roomid, int count, int highlightedCount); void updateRoomDescription(const QString &roomid, const DescInfo &info); void closeJoinRoomDialog(bool isJoining, QString roomAlias); void updateReadStatus(const std::map &status); diff --git a/src/TextInputWidget.cpp b/src/TextInputWidget.cpp index 8951303..5fcba7a 100644 --- a/src/TextInputWidget.cpp +++ b/src/TextInputWidget.cpp @@ -513,8 +513,22 @@ TextInputWidget::TextInputWidget(QWidget *parent) sendMessageBtn_->setIcon(send_message_icon); sendMessageBtn_->setIconSize(QSize(ButtonHeight, ButtonHeight)); + emojiBtn_ = new emoji::PickButton(this); + emojiBtn_->setToolTip(tr("Emoji")); + +#if defined(Q_OS_MAC) + // macOS has a native emoji picker. + emojiBtn_->hide(); +#endif + + QIcon emoji_icon; + emoji_icon.addFile(":/icons/icons/ui/smile.png"); + emojiBtn_->setIcon(emoji_icon); + emojiBtn_->setIconSize(QSize(ButtonHeight, ButtonHeight)); + topLayout_->addWidget(sendFileBtn_); topLayout_->addWidget(input_); + topLayout_->addWidget(emojiBtn_); topLayout_->addWidget(sendMessageBtn_); setLayout(topLayout_); @@ -527,6 +541,11 @@ TextInputWidget::TextInputWidget(QWidget *parent) connect(input_, &FilteredTextEdit::audio, this, &TextInputWidget::uploadAudio); connect(input_, &FilteredTextEdit::video, this, &TextInputWidget::uploadVideo); connect(input_, &FilteredTextEdit::file, this, &TextInputWidget::uploadFile); + connect(emojiBtn_, + SIGNAL(emojiSelected(const QString &)), + this, + SLOT(addSelectedEmoji(const QString &))); + connect(input_, &FilteredTextEdit::startedTyping, this, &TextInputWidget::startedTyping); connect(input_, &FilteredTextEdit::stoppedTyping, this, &TextInputWidget::stoppedTyping); @@ -535,6 +554,22 @@ TextInputWidget::TextInputWidget(QWidget *parent) input_, &FilteredTextEdit::startedUpload, this, &TextInputWidget::showUploadSpinner); } +void +TextInputWidget::addSelectedEmoji(const QString &emoji) +{ + QTextCursor cursor = input_->textCursor(); + + QTextCharFormat charfmt; + input_->setCurrentCharFormat(charfmt); + + input_->insertPlainText(emoji); + cursor.movePosition(QTextCursor::End); + + input_->setCurrentCharFormat(charfmt); + + input_->show(); +} + void TextInputWidget::command(QString command, QString args) { diff --git a/src/TextInputWidget.h b/src/TextInputWidget.h index 1fb6d7f..8f634f6 100644 --- a/src/TextInputWidget.h +++ b/src/TextInputWidget.h @@ -30,6 +30,7 @@ #include "SuggestionsPopup.h" #include "dialogs/PreviewUploadOverlay.h" +#include "emoji/PickButton.h" namespace dialogs { class PreviewUploadOverlay; @@ -159,6 +160,9 @@ public slots: void focusLineEdit() { input_->setFocus(); } void addReply(const QString &username, const QString &msg); +private slots: + void addSelectedEmoji(const QString &emoji); + signals: void sendTextMessage(QString msg); void sendEmoteMessage(QString msg); @@ -189,6 +193,7 @@ private: FlatButton *sendFileBtn_; FlatButton *sendMessageBtn_; + emoji::PickButton *emojiBtn_; QColor borderColor_; }; diff --git a/src/UserSettingsPage.cpp b/src/UserSettingsPage.cpp index 15ad72e..e3c0d19 100644 --- a/src/UserSettingsPage.cpp +++ b/src/UserSettingsPage.cpp @@ -49,7 +49,7 @@ UserSettings::load() isTypingNotificationsEnabled_ = settings.value("user/typing_notifications", true).toBool(); isReadReceiptsEnabled_ = settings.value("user/read_receipts", true).toBool(); theme_ = settings.value("user/theme", "light").toString(); - + font_ = settings.value("user/font_family", "default").toString(); baseFontSize_ = settings.value("user/font_size", QFont().pointSizeF()).toDouble(); applyTheme(); @@ -62,6 +62,13 @@ UserSettings::setFontSize(double size) save(); } +void +UserSettings::setFontFamily(QString family) +{ + font_ = family; + save(); +} + void UserSettings::setTheme(QString theme) { @@ -106,6 +113,7 @@ UserSettings::save() settings.setValue("group_view", isGroupViewEnabled_); settings.setValue("desktop_notifications", hasDesktopNotifications_); settings.setValue("theme", theme()); + settings.setValue("font_family", font_); settings.endGroup(); } @@ -220,6 +228,23 @@ UserSettingsPage::UserSettingsPage(QSharedPointer settings, QWidge fontSizeOptionLayout->addWidget(fontSizeLabel); fontSizeOptionLayout->addWidget(fontSizeCombo_, 0, Qt::AlignRight); + auto fontFamilyOptionLayout = new QHBoxLayout; + fontFamilyOptionLayout->setContentsMargins(0, OptionMargin, 0, OptionMargin); + auto fontFamilyLabel = new QLabel(tr("Font Family"), this); + fontFamilyLabel->setFont(font); + fontSelectionCombo_ = new QComboBox(this); + QFontDatabase fontDb; + auto fontFamilies = fontDb.families(); + for (const auto &family : fontFamilies) { + fontSelectionCombo_->addItem(family); + } + + int fontIndex = fontSelectionCombo_->findText(settings_->font()); + fontSelectionCombo_->setCurrentIndex(fontIndex); + + fontFamilyOptionLayout->addWidget(fontFamilyLabel); + fontFamilyOptionLayout->addWidget(fontSelectionCombo_, 0, Qt::AlignRight); + auto themeOptionLayout_ = new QHBoxLayout; themeOptionLayout_->setContentsMargins(0, OptionMargin, 0, OptionMargin); auto themeLabel_ = new QLabel(tr("Theme"), this); @@ -229,6 +254,11 @@ UserSettingsPage::UserSettingsPage(QSharedPointer settings, QWidge themeCombo_->addItem("Dark"); themeCombo_->addItem("System"); + QString themeStr = settings_->theme(); + themeStr.replace(0, 1, themeStr[0].toUpper()); + int themeIndex = themeCombo_->findText(themeStr); + themeCombo_->setCurrentIndex(themeIndex); + themeOptionLayout_->addWidget(themeLabel_); themeOptionLayout_->addWidget(themeCombo_, 0, Qt::AlignRight); @@ -319,6 +349,7 @@ UserSettingsPage::UserSettingsPage(QSharedPointer settings, QWidge mainLayout_->addLayout(scaleFactorOptionLayout); mainLayout_->addLayout(fontSizeOptionLayout); + mainLayout_->addLayout(fontFamilyOptionLayout); mainLayout_->addWidget(new HorizontalLine(this)); mainLayout_->addLayout(themeOptionLayout_); mainLayout_->addWidget(new HorizontalLine(this)); @@ -348,14 +379,19 @@ UserSettingsPage::UserSettingsPage(QSharedPointer settings, QWidge connect(themeCombo_, static_cast(&QComboBox::activated), - [this](const QString &text) { settings_->setTheme(text.toLower()); }); + [this](const QString &text) { + settings_->setTheme(text.toLower()); + emit themeChanged(); + }); connect(scaleFactorCombo_, static_cast(&QComboBox::activated), [](const QString &factor) { utils::setScaleFactor(factor.toFloat()); }); connect(fontSizeCombo_, static_cast(&QComboBox::activated), [this](const QString &size) { settings_->setFontSize(size.trimmed().toDouble()); }); - + connect(fontSelectionCombo_, + static_cast(&QComboBox::activated), + [this](const QString &family) { settings_->setFontFamily(family.trimmed()); }); connect(trayToggle_, &Toggle::toggled, this, [this](bool isDisabled) { settings_->setTray(!isDisabled); if (isDisabled) { diff --git a/src/UserSettingsPage.h b/src/UserSettingsPage.h index 373126a..900f57e 100644 --- a/src/UserSettingsPage.h +++ b/src/UserSettingsPage.h @@ -18,6 +18,7 @@ #pragma once #include +#include #include #include #include @@ -54,6 +55,7 @@ public: } void setFontSize(double size); + void setFontFamily(QString family); void setGroupView(bool state) { @@ -90,6 +92,7 @@ public: bool isReadReceiptsEnabled() const { return isReadReceiptsEnabled_; } bool hasDesktopNotifications() const { return hasDesktopNotifications_; } double fontSize() const { return baseFontSize_; } + QString font() const { return font_; } signals: void groupViewStateChanged(bool state); @@ -103,6 +106,7 @@ private: bool isReadReceiptsEnabled_; bool hasDesktopNotifications_; double baseFontSize_; + QString font_; }; class HorizontalLine : public QFrame @@ -128,6 +132,7 @@ protected: signals: void moveBack(); void trayOptionChanged(bool value); + void themeChanged(); private slots: void importSessionKeys(); @@ -154,6 +159,7 @@ private: QComboBox *themeCombo_; QComboBox *scaleFactorCombo_; QComboBox *fontSizeCombo_; + QComboBox *fontSelectionCombo_; int sideMargin_ = 0; }; diff --git a/src/Utils.cpp b/src/Utils.cpp index 8176cb4..0d3e938 100644 --- a/src/Utils.cpp +++ b/src/Utils.cpp @@ -15,6 +15,8 @@ using TimelineEvent = mtx::events::collections::TimelineEvents; +QHash authorColors_; + QString utils::localUser() { @@ -382,6 +384,126 @@ utils::linkColor() return QPalette().color(QPalette::Link).name(); } +int +utils::hashQString(const QString &input) +{ + auto hash = 0; + + for (int i = 0; i < input.length(); i++) { + hash = input.at(i).digitValue() + ((hash << 5) - hash); + } + + return hash; +} + +QString +utils::generateContrastingHexColor(const QString &input, const QString &background) +{ + const QColor backgroundCol(background); + const qreal backgroundLum = luminance(background); + + // Create a color for the input + auto hash = hashQString(input); + // create a hue value based on the hash of the input. + auto userHue = qAbs(hash % 360); + // start with moderate saturation and lightness values. + auto sat = 220; + auto lightness = 125; + + // converting to a QColor makes the luminance calc easier. + QColor inputColor = QColor::fromHsl(userHue, sat, lightness); + + // calculate the initial luminance and contrast of the + // generated color. It's possible that no additional + // work will be necessary. + auto lum = luminance(inputColor); + auto contrast = computeContrast(lum, backgroundLum); + + // If the contrast doesn't meet our criteria, + // try again and again until they do by modifying first + // the lightness and then the saturation of the color. + while (contrast < 5) { + // if our lightness is at it's bounds, try changing + // saturation instead. + if (lightness == 242 || lightness == 13) { + qreal newSat = qBound(26.0, sat * 1.25, 242.0); + + inputColor.setHsl(userHue, qFloor(newSat), lightness); + auto tmpLum = luminance(inputColor); + auto higherContrast = computeContrast(tmpLum, backgroundLum); + if (higherContrast > contrast) { + contrast = higherContrast; + sat = newSat; + } else { + newSat = qBound(26.0, sat / 1.25, 242.0); + inputColor.setHsl(userHue, qFloor(newSat), lightness); + tmpLum = luminance(inputColor); + auto lowerContrast = computeContrast(tmpLum, backgroundLum); + if (lowerContrast > contrast) { + contrast = lowerContrast; + sat = newSat; + } + } + } else { + qreal newLightness = qBound(13.0, lightness * 1.25, 242.0); + + inputColor.setHsl(userHue, sat, qFloor(newLightness)); + + auto tmpLum = luminance(inputColor); + auto higherContrast = computeContrast(tmpLum, backgroundLum); + + // Check to make sure we have actually improved contrast + if (higherContrast > contrast) { + contrast = higherContrast; + lightness = newLightness; + // otherwise, try going the other way instead. + } else { + newLightness = qBound(13.0, lightness / 1.25, 242.0); + inputColor.setHsl(userHue, sat, qFloor(newLightness)); + tmpLum = luminance(inputColor); + auto lowerContrast = computeContrast(tmpLum, backgroundLum); + if (lowerContrast > contrast) { + contrast = lowerContrast; + lightness = newLightness; + } + } + } + } + + // get the hex value of the generated color. + auto colorHex = inputColor.name(); + + return colorHex; +} + +qreal +utils::computeContrast(const qreal &one, const qreal &two) +{ + auto ratio = (one + 0.05) / (two + 0.05); + + if (two > one) { + ratio = 1 / ratio; + } + + return ratio; +} + +qreal +utils::luminance(const QColor &col) +{ + int colRgb[3] = {col.red(), col.green(), col.blue()}; + qreal lumRgb[3]; + + for (int i = 0; i < 3; i++) { + qreal v = colRgb[i] / 255.0; + v <= 0.03928 ? lumRgb[i] = v / 12.92 : lumRgb[i] = qPow((v + 0.055) / 1.055, 2.4); + } + + auto lum = lumRgb[0] * 0.2126 + lumRgb[1] * 0.7152 + lumRgb[2] * 0.0722; + + return lum; +} + void utils::centerWidget(QWidget *widget, QWidget *parent) { diff --git a/src/Utils.h b/src/Utils.h index 2a02994..8672e7d 100644 --- a/src/Utils.h +++ b/src/Utils.h @@ -14,6 +14,8 @@ #include #include +#include + class QComboBox; namespace utils { @@ -227,6 +229,23 @@ markdownToHtml(const QString &text); QString linkColor(); +//! Returns the hash code of the input QString +int +hashQString(const QString &input); + +//! Generate a color (matching #RRGGBB) that has an acceptable contrast to background that is based +//! on the input string. +QString +generateContrastingHexColor(const QString &input, const QString &background); + +//! Given two luminance values, compute the contrast ratio between them. +qreal +computeContrast(const qreal &one, const qreal &two); + +//! Compute the luminance of a single color. Based on https://stackoverflow.com/a/9733420 +qreal +luminance(const QColor &col); + //! Center a widget in relation to another widget. void centerWidget(QWidget *widget, QWidget *parent); diff --git a/src/dialogs/ImageOverlay.cpp b/src/dialogs/ImageOverlay.cpp index b40aa16..dbf5bbe 100644 --- a/src/dialogs/ImageOverlay.cpp +++ b/src/dialogs/ImageOverlay.cpp @@ -76,6 +76,8 @@ ImageOverlay::paintEvent(QPaintEvent *event) content_ = QRect(outer_margin + diff_x / 2, diff_y / 2, image_.width(), image_.height()); close_button_ = QRect(screen_.width() - margin - buttonSize, margin, buttonSize, buttonSize); + save_button_ = + QRect(screen_.width() - (2 * margin) - (2 * buttonSize), margin, buttonSize, buttonSize); // Draw main content_. painter.drawPixmap(content_, image_); @@ -91,6 +93,12 @@ ImageOverlay::paintEvent(QPaintEvent *event) painter.setPen(pen); painter.drawLine(center - QPointF(15, 15), center + QPointF(15, 15)); painter.drawLine(center + QPointF(15, -15), center - QPointF(15, -15)); + + // Draw download button + center = save_button_.center(); + painter.drawLine(center - QPointF(0, 15), center + QPointF(0, 15)); + painter.drawLine(center - QPointF(15, 0), center + QPointF(0, 15)); + painter.drawLine(center + QPointF(0, 15), center + QPointF(15, 0)); } void @@ -101,6 +109,8 @@ ImageOverlay::mousePressEvent(QMouseEvent *event) if (close_button_.contains(event->pos())) emit closing(); + else if (save_button_.contains(event->pos())) + emit saving(); else if (!content_.contains(event->pos())) emit closing(); } diff --git a/src/dialogs/ImageOverlay.h b/src/dialogs/ImageOverlay.h index b4d42ac..26257fc 100644 --- a/src/dialogs/ImageOverlay.h +++ b/src/dialogs/ImageOverlay.h @@ -35,6 +35,7 @@ protected: signals: void closing(); + void saving(); private: QPixmap originalImage_; @@ -42,6 +43,7 @@ private: QRect content_; QRect close_button_; + QRect save_button_; QRect screen_; }; } // dialogs diff --git a/src/emoji/Category.cpp b/src/emoji/Category.cpp new file mode 100644 index 0000000..fbfbf4f --- /dev/null +++ b/src/emoji/Category.cpp @@ -0,0 +1,90 @@ +/* + * nheko Copyright (C) 2017 Konstantinos Sideris + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include +#include +#include + +#include "Config.h" + +#include "emoji/Category.h" + +using namespace emoji; + +Category::Category(QString category, std::vector emoji, QWidget *parent) + : QWidget(parent) +{ + mainLayout_ = new QVBoxLayout(this); + mainLayout_->setMargin(0); + mainLayout_->setSpacing(0); + + emojiListView_ = new QListView(); + itemModel_ = new QStandardItemModel(this); + + delegate_ = new ItemDelegate(this); + data_ = new Emoji; + + emojiListView_->setItemDelegate(delegate_); + emojiListView_->setModel(itemModel_); + emojiListView_->setViewMode(QListView::IconMode); + emojiListView_->setFlow(QListView::LeftToRight); + emojiListView_->setResizeMode(QListView::Adjust); + emojiListView_->verticalScrollBar()->setEnabled(false); + emojiListView_->horizontalScrollBar()->setEnabled(false); + + const int cols = 7; + const int rows = emoji.size() / 7; + + // TODO: Be precise here. Take the parent into consideration. + emojiListView_->setFixedSize(cols * 50 + 20, rows * 50 + 20); + emojiListView_->setGridSize(QSize(50, 50)); + emojiListView_->setDragEnabled(false); + emojiListView_->setEditTriggers(QAbstractItemView::NoEditTriggers); + + for (const auto &e : emoji) { + data_->unicode = e.unicode; + + auto item = new QStandardItem; + item->setSizeHint(QSize(24, 24)); + + QVariant unicode(data_->unicode); + item->setData(unicode.toString(), Qt::UserRole); + + itemModel_->appendRow(item); + } + + QFont font; + font.setWeight(QFont::Medium); + + category_ = new QLabel(category, this); + category_->setFont(font); + category_->setStyleSheet("margin: 20px 0 20px 8px;"); + + mainLayout_->addWidget(category_); + mainLayout_->addWidget(emojiListView_); + + connect(emojiListView_, &QListView::clicked, this, &Category::clickIndex); +} + +void +Category::paintEvent(QPaintEvent *) +{ + QStyleOption opt; + opt.init(this); + QPainter p(this); + style()->drawPrimitive(QStyle::PE_Widget, &opt, &p, this); +} diff --git a/src/emoji/Category.h b/src/emoji/Category.h new file mode 100644 index 0000000..a14029c --- /dev/null +++ b/src/emoji/Category.h @@ -0,0 +1,59 @@ +/* + * nheko Copyright (C) 2017 Konstantinos Sideris + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#pragma once + +#include +#include +#include +#include + +#include "ItemDelegate.h" + +namespace emoji { + +class Category : public QWidget +{ + Q_OBJECT + +public: + Category(QString category, std::vector emoji, QWidget *parent = nullptr); + +signals: + void emojiSelected(const QString &emoji); + +protected: + void paintEvent(QPaintEvent *event) override; + +private slots: + void clickIndex(const QModelIndex &index) + { + emit emojiSelected(index.data(Qt::UserRole).toString()); + }; + +private: + QVBoxLayout *mainLayout_; + + QStandardItemModel *itemModel_; + QListView *emojiListView_; + + emoji::Emoji *data_; + emoji::ItemDelegate *delegate_; + + QLabel *category_; +}; +} // namespace emoji diff --git a/src/emoji/ItemDelegate.cpp b/src/emoji/ItemDelegate.cpp new file mode 100644 index 0000000..b79ae0f --- /dev/null +++ b/src/emoji/ItemDelegate.cpp @@ -0,0 +1,48 @@ +/* + * nheko Copyright (C) 2017 Konstantinos Sideris + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include +#include + +#include "emoji/ItemDelegate.h" + +using namespace emoji; + +ItemDelegate::ItemDelegate(QObject *parent) + : QStyledItemDelegate(parent) +{ + data_ = new Emoji; +} + +ItemDelegate::~ItemDelegate() { delete data_; } + +void +ItemDelegate::paint(QPainter *painter, + const QStyleOptionViewItem &option, + const QModelIndex &index) const +{ + Q_UNUSED(index); + + QStyleOptionViewItem viewOption(option); + + auto emoji = index.data(Qt::UserRole).toString(); + + // QFont font("Emoji One"); + QFont font; + painter->setFont(font); + painter->drawText(viewOption.rect, Qt::AlignCenter, emoji); +} diff --git a/src/emoji/ItemDelegate.h b/src/emoji/ItemDelegate.h new file mode 100644 index 0000000..e045630 --- /dev/null +++ b/src/emoji/ItemDelegate.h @@ -0,0 +1,43 @@ +/* + * nheko Copyright (C) 2017 Konstantinos Sideris + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#pragma once + +#include +#include +#include + +#include "Provider.h" + +namespace emoji { + +class ItemDelegate : public QStyledItemDelegate +{ + Q_OBJECT + +public: + explicit ItemDelegate(QObject *parent = nullptr); + ~ItemDelegate(); + + void paint(QPainter *painter, + const QStyleOptionViewItem &option, + const QModelIndex &index) const override; + +private: + Emoji *data_; +}; +} // namespace emoji diff --git a/src/emoji/Panel.cpp b/src/emoji/Panel.cpp new file mode 100644 index 0000000..710b501 --- /dev/null +++ b/src/emoji/Panel.cpp @@ -0,0 +1,236 @@ +/* + * nheko Copyright (C) 2017 Konstantinos Sideris + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include +#include +#include + +#include "ui/DropShadow.h" +#include "ui/FlatButton.h" + +#include "emoji/Category.h" +#include "emoji/Panel.h" + +using namespace emoji; + +Panel::Panel(QWidget *parent) + : QWidget(parent) + , shadowMargin_{2} + , width_{370} + , height_{350} + , categoryIconSize_{20} +{ + setStyleSheet("QWidget {border: none;}" + "QScrollBar:vertical { width: 0px; margin: 0px; }" + "QScrollBar::handle:vertical { min-height: 30px; }"); + + setAttribute(Qt::WA_ShowWithoutActivating, true); + setWindowFlags(Qt::Tool | Qt::FramelessWindowHint | Qt::NoDropShadowWindowHint); + + auto mainWidget = new QWidget(this); + mainWidget->setMaximumSize(width_, height_); + + auto topLayout = new QVBoxLayout(this); + topLayout->addWidget(mainWidget); + topLayout->setMargin(shadowMargin_); + topLayout->setSpacing(0); + + auto contentLayout = new QVBoxLayout(mainWidget); + contentLayout->setMargin(0); + contentLayout->setSpacing(0); + + auto emojiCategories = new QFrame(mainWidget); + + auto categoriesLayout = new QHBoxLayout(emojiCategories); + categoriesLayout->setSpacing(0); + categoriesLayout->setMargin(0); + + QIcon icon; + + auto peopleCategory = new FlatButton(emojiCategories); + icon.addFile(":/icons/icons/emoji-categories/people.png"); + peopleCategory->setIcon(icon); + peopleCategory->setIconSize(QSize(categoryIconSize_, categoryIconSize_)); + + auto natureCategory_ = new FlatButton(emojiCategories); + icon.addFile(":/icons/icons/emoji-categories/nature.png"); + natureCategory_->setIcon(icon); + natureCategory_->setIconSize(QSize(categoryIconSize_, categoryIconSize_)); + + auto foodCategory_ = new FlatButton(emojiCategories); + icon.addFile(":/icons/icons/emoji-categories/foods.png"); + foodCategory_->setIcon(icon); + foodCategory_->setIconSize(QSize(categoryIconSize_, categoryIconSize_)); + + auto activityCategory = new FlatButton(emojiCategories); + icon.addFile(":/icons/icons/emoji-categories/activity.png"); + activityCategory->setIcon(icon); + activityCategory->setIconSize(QSize(categoryIconSize_, categoryIconSize_)); + + auto travelCategory = new FlatButton(emojiCategories); + icon.addFile(":/icons/icons/emoji-categories/travel.png"); + travelCategory->setIcon(icon); + travelCategory->setIconSize(QSize(categoryIconSize_, categoryIconSize_)); + + auto objectsCategory = new FlatButton(emojiCategories); + icon.addFile(":/icons/icons/emoji-categories/objects.png"); + objectsCategory->setIcon(icon); + objectsCategory->setIconSize(QSize(categoryIconSize_, categoryIconSize_)); + + auto symbolsCategory = new FlatButton(emojiCategories); + icon.addFile(":/icons/icons/emoji-categories/symbols.png"); + symbolsCategory->setIcon(icon); + symbolsCategory->setIconSize(QSize(categoryIconSize_, categoryIconSize_)); + + auto flagsCategory = new FlatButton(emojiCategories); + icon.addFile(":/icons/icons/emoji-categories/flags.png"); + flagsCategory->setIcon(icon); + flagsCategory->setIconSize(QSize(categoryIconSize_, categoryIconSize_)); + + categoriesLayout->addWidget(peopleCategory); + categoriesLayout->addWidget(natureCategory_); + categoriesLayout->addWidget(foodCategory_); + categoriesLayout->addWidget(activityCategory); + categoriesLayout->addWidget(travelCategory); + categoriesLayout->addWidget(objectsCategory); + categoriesLayout->addWidget(symbolsCategory); + categoriesLayout->addWidget(flagsCategory); + + scrollArea_ = new QScrollArea(this); + scrollArea_->setWidgetResizable(true); + scrollArea_->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + + auto scrollWidget = new QWidget(this); + auto scrollLayout = new QVBoxLayout(scrollWidget); + + scrollLayout->setMargin(0); + scrollLayout->setSpacing(0); + scrollArea_->setWidget(scrollWidget); + + auto peopleEmoji = + new Category(tr("Smileys & People"), emoji_provider_.people, scrollWidget); + scrollLayout->addWidget(peopleEmoji); + + auto natureEmoji = + new Category(tr("Animals & Nature"), emoji_provider_.nature, scrollWidget); + scrollLayout->addWidget(natureEmoji); + + auto foodEmoji = new Category(tr("Food & Drink"), emoji_provider_.food, scrollWidget); + scrollLayout->addWidget(foodEmoji); + + auto activityEmoji = new Category(tr("Activity"), emoji_provider_.activity, scrollWidget); + scrollLayout->addWidget(activityEmoji); + + auto travelEmoji = + new Category(tr("Travel & Places"), emoji_provider_.travel, scrollWidget); + scrollLayout->addWidget(travelEmoji); + + auto objectsEmoji = new Category(tr("Objects"), emoji_provider_.objects, scrollWidget); + scrollLayout->addWidget(objectsEmoji); + + auto symbolsEmoji = new Category(tr("Symbols"), emoji_provider_.symbols, scrollWidget); + scrollLayout->addWidget(symbolsEmoji); + + auto flagsEmoji = new Category(tr("Flags"), emoji_provider_.flags, scrollWidget); + scrollLayout->addWidget(flagsEmoji); + + contentLayout->addWidget(scrollArea_); + contentLayout->addWidget(emojiCategories); + + connect(peopleEmoji, &Category::emojiSelected, this, &Panel::emojiSelected); + connect(peopleCategory, &QPushButton::clicked, [this, peopleEmoji]() { + this->showCategory(peopleEmoji); + }); + + connect(natureEmoji, &Category::emojiSelected, this, &Panel::emojiSelected); + connect(natureCategory_, &QPushButton::clicked, [this, natureEmoji]() { + this->showCategory(natureEmoji); + }); + + connect(foodEmoji, &Category::emojiSelected, this, &Panel::emojiSelected); + connect(foodCategory_, &QPushButton::clicked, [this, foodEmoji]() { + this->showCategory(foodEmoji); + }); + + connect(activityEmoji, &Category::emojiSelected, this, &Panel::emojiSelected); + connect(activityCategory, &QPushButton::clicked, [this, activityEmoji]() { + this->showCategory(activityEmoji); + }); + + connect(travelEmoji, &Category::emojiSelected, this, &Panel::emojiSelected); + connect(travelCategory, &QPushButton::clicked, [this, travelEmoji]() { + this->showCategory(travelEmoji); + }); + + connect(objectsEmoji, &Category::emojiSelected, this, &Panel::emojiSelected); + connect(objectsCategory, &QPushButton::clicked, [this, objectsEmoji]() { + this->showCategory(objectsEmoji); + }); + + connect(symbolsEmoji, &Category::emojiSelected, this, &Panel::emojiSelected); + connect(symbolsCategory, &QPushButton::clicked, [this, symbolsEmoji]() { + this->showCategory(symbolsEmoji); + }); + + connect(flagsEmoji, &Category::emojiSelected, this, &Panel::emojiSelected); + connect(flagsCategory, &QPushButton::clicked, [this, flagsEmoji]() { + this->showCategory(flagsEmoji); + }); +} + +void +Panel::showCategory(const Category *category) +{ + auto posToGo = category->mapToParent(QPoint()).y(); + auto current = scrollArea_->verticalScrollBar()->value(); + + if (current == posToGo) + return; + + // HACK + // If we want to go to a previous category and position the label at the top + // the 6*50 offset won't work because not all the categories have the same + // height. To ensure the category is at the top, we move to the top and go as + // normal to the next category. + if (current > posToGo) + this->scrollArea_->ensureVisible(0, 0, 0, 0); + + posToGo += 6 * 50; + this->scrollArea_->ensureVisible(0, posToGo, 0, 0); +} + +void +Panel::paintEvent(QPaintEvent *event) +{ + Q_UNUSED(event); + + QStyleOption opt; + opt.init(this); + QPainter p(this); + style()->drawPrimitive(QStyle::PE_Widget, &opt, &p, this); + + DropShadow::draw(p, + shadowMargin_, + 4.0, + QColor(120, 120, 120, 92), + QColor(255, 255, 255, 0), + 0.0, + 1.0, + 0.6, + width(), + height()); +} diff --git a/src/emoji/Panel.h b/src/emoji/Panel.h new file mode 100644 index 0000000..ad233c2 --- /dev/null +++ b/src/emoji/Panel.h @@ -0,0 +1,66 @@ +/* + * nheko Copyright (C) 2017 Konstantinos Sideris + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#pragma once + +#include + +#include "Provider.h" + +namespace emoji { + +class Category; + +class Panel : public QWidget +{ + Q_OBJECT + +public: + Panel(QWidget *parent = nullptr); + +signals: + void mouseLeft(); + void emojiSelected(const QString &emoji); + +protected: + void leaveEvent(QEvent *event) override + { + emit leaving(); + QWidget::leaveEvent(event); + } + + void paintEvent(QPaintEvent *event) override; + +signals: + void leaving(); + +private: + void showCategory(const Category *category); + + Provider emoji_provider_; + + QScrollArea *scrollArea_; + + int shadowMargin_; + + // Panel dimensions. + int width_; + int height_; + + int categoryIconSize_; +}; +} // namespace emoji diff --git a/src/emoji/PickButton.cpp b/src/emoji/PickButton.cpp new file mode 100644 index 0000000..608b4fa --- /dev/null +++ b/src/emoji/PickButton.cpp @@ -0,0 +1,82 @@ +/* + * nheko Copyright (C) 2017 Konstantinos Sideris + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include + +#include "emoji/Panel.h" +#include "emoji/PickButton.h" + +using namespace emoji; + +// Number of milliseconds after which the panel will be hidden +// if the mouse cursor is not on top of the widget. +constexpr int HIDE_TIMEOUT = 300; + +PickButton::PickButton(QWidget *parent) + : FlatButton(parent) + , panel_{nullptr} +{ + connect(&hideTimer_, &QTimer::timeout, this, &PickButton::hidePanel); + connect(this, &QPushButton::clicked, this, [this]() { + if (panel_ && panel_->isVisible()) { + hidePanel(); + return; + } + + showPanel(); + }); +} + +void +PickButton::hidePanel() +{ + if (panel_ && !panel_->underMouse()) { + hideTimer_.stop(); + panel_->hide(); + } +} + +void +PickButton::showPanel() +{ + if (panel_.isNull()) { + panel_ = QSharedPointer(new Panel(this)); + connect(panel_.data(), &Panel::emojiSelected, this, &PickButton::emojiSelected); + connect(panel_.data(), &Panel::leaving, this, [this]() { panel_->hide(); }); + } + + if (panel_->isVisible()) + return; + + QPoint pos(rect().x(), rect().y()); + pos = this->mapToGlobal(pos); + + auto panel_size = panel_->sizeHint(); + + auto x = pos.x() - panel_size.width() + horizontal_distance_; + auto y = pos.y() - panel_size.height() - vertical_distance_; + + panel_->move(x, y); + panel_->show(); +} + +void +PickButton::leaveEvent(QEvent *e) +{ + hideTimer_.start(HIDE_TIMEOUT); + FlatButton::leaveEvent(e); +} diff --git a/src/emoji/PickButton.h b/src/emoji/PickButton.h new file mode 100644 index 0000000..97ed8c3 --- /dev/null +++ b/src/emoji/PickButton.h @@ -0,0 +1,55 @@ +/* + * nheko Copyright (C) 2017 Konstantinos Sideris + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#pragma once + +#include +#include +#include + +#include "ui/FlatButton.h" + +namespace emoji { + +class Panel; + +class PickButton : public FlatButton +{ + Q_OBJECT +public: + explicit PickButton(QWidget *parent = nullptr); + +signals: + void emojiSelected(const QString &emoji); + +protected: + void leaveEvent(QEvent *e) override; + +private: + void showPanel(); + void hidePanel(); + + // Vertical distance from panel's bottom. + int vertical_distance_ = 10; + + // Horizontal distance from panel's bottom right corner. + int horizontal_distance_ = 70; + + QSharedPointer panel_; + QTimer hideTimer_; +}; +} // namespace emoji diff --git a/src/main.cpp b/src/main.cpp index 591d348..0c196a3 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -127,6 +127,12 @@ main(int argc, char *argv[]) parser.addVersionOption(); parser.process(app); + QFontDatabase::addApplicationFont(":/fonts/fonts/OpenSans/OpenSans-Regular.ttf"); + QFontDatabase::addApplicationFont(":/fonts/fonts/OpenSans/OpenSans-Italic.ttf"); + QFontDatabase::addApplicationFont(":/fonts/fonts/OpenSans/OpenSans-Bold.ttf"); + QFontDatabase::addApplicationFont(":/fonts/fonts/OpenSans/OpenSans-Semibold.ttf"); + QFontDatabase::addApplicationFont(":/fonts/fonts/EmojiOne/emojione-android.ttf"); + app.setWindowIcon(QIcon(":/logos/nheko.png")); http::init(); @@ -147,6 +153,10 @@ main(int argc, char *argv[]) QSettings settings; QFont font; + QString userFontFamily = settings.value("user/font_family", "").toString(); + if (!userFontFamily.isEmpty()) { + font.setFamily(userFontFamily); + } font.setPointSizeF(settings.value("user/font_size", font.pointSizeF()).toDouble()); app.setFont(font); diff --git a/src/timeline/TimelineItem.cpp b/src/timeline/TimelineItem.cpp index e962d46..d23dbf4 100644 --- a/src/timeline/TimelineItem.cpp +++ b/src/timeline/TimelineItem.cpp @@ -14,6 +14,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ +#include #include #include @@ -192,10 +193,17 @@ TimelineItem::init() emit eventRedacted(event_id_); }); }); - + connect( + ChatPage::instance(), &ChatPage::themeChanged, this, &TimelineItem::refreshAuthorColor); connect(markAsRead_, &QAction::triggered, this, &TimelineItem::sendReadReceipt); connect(viewRawMessage_, &QAction::triggered, this, &TimelineItem::openRawMessageViewer); + colorGenerating_ = new QFutureWatcher(this); + connect(colorGenerating_, + &QFutureWatcher::finished, + this, + &TimelineItem::finishedGeneratingColor); + topLayout_ = new QHBoxLayout(this); mainLayout_ = new QVBoxLayout; messageLayout_ = new QHBoxLayout; @@ -556,6 +564,12 @@ TimelineItem::TimelineItem(const mtx::events::RoomEvent adjustMessageLayout(); } +TimelineItem::~TimelineItem() +{ + colorGenerating_->cancel(); + colorGenerating_->waitForFinished(); +} + void TimelineItem::markSent() { @@ -594,7 +608,7 @@ TimelineItem::markReceived(bool isEncrypted) void TimelineItem::generateBody(const QString &body) { - body_ = new TextLabel(body, this); + body_ = new TextLabel(replaceEmoji(body), this); body_->setTextInteractionFlags(Qt::TextSelectableByMouse | Qt::TextBrowserInteraction); connect(body_, &TextLabel::userProfileTriggered, this, [](const QString &user_id) { @@ -603,6 +617,47 @@ TimelineItem::generateBody(const QString &body) }); } +void +TimelineItem::refreshAuthorColor() +{ + // Cancel and wait if we are already generating the color. + if (colorGenerating_->isRunning()) { + colorGenerating_->cancel(); + colorGenerating_->waitForFinished(); + } + if (userName_) { + // generate user's unique color. + std::function generate = [this]() { + QString userColor = utils::generateContrastingHexColor( + userName_->toolTip(), backgroundColor().name()); + return userColor; + }; + + QString userColor = Cache::userColor(userName_->toolTip()); + + // If the color is empty, then generate it asynchronously + if (userColor.isEmpty()) { + colorGenerating_->setFuture(QtConcurrent::run(generate)); + } else { + userName_->setStyleSheet("QLabel { color : " + userColor + "; }"); + } + } +} + +void +TimelineItem::finishedGeneratingColor() +{ + nhlog::ui()->debug("finishedGeneratingColor for: {}", userName_->toolTip().toStdString()); + QString userColor = colorGenerating_->result(); + + if (!userColor.isEmpty()) { + // another TimelineItem might have inserted in the meantime. + if (Cache::userColor(userName_->toolTip()).isEmpty()) { + Cache::insertUserColor(userName_->toolTip(), userColor); + } + userName_->setStyleSheet("QLabel { color : " + userColor + "; }"); + } +} // The username/timestamp is displayed along with the message body. void TimelineItem::generateBody(const QString &user_id, const QString &displayname, const QString &body) @@ -623,7 +678,7 @@ TimelineItem::generateUserName(const QString &user_id, const QString &displaynam } QFont usernameFont; - usernameFont.setPointSizeF(usernameFont.pointSizeF()); + usernameFont.setPointSizeF(usernameFont.pointSizeF() * 1.1); usernameFont.setWeight(QFont::Medium); QFontMetrics fm(usernameFont); @@ -637,6 +692,10 @@ TimelineItem::generateUserName(const QString &user_id, const QString &displaynam userName_->setAlignment(Qt::AlignLeft | Qt::AlignTop); userName_->setFixedWidth(QFontMetrics(userName_->font()).width(userName_->text())); + // Set the user color asynchronously if it hasn't been generated yet, + // otherwise this will just set it. + refreshAuthorColor(); + auto filter = new UserProfileFilter(user_id, userName_); userName_->installEventFilter(filter); userName_->setCursor(Qt::PointingHandCursor); @@ -667,6 +726,25 @@ TimelineItem::generateTimestamp(const QDateTime &time) QString(" %1 ").arg(time.toString("HH:mm"))); } +QString +TimelineItem::replaceEmoji(const QString &body) +{ + QString fmtBody = ""; + + QVector utf32_string = body.toUcs4(); + + for (auto &code : utf32_string) { + // TODO: Be more precise here. + if (code > 9000) + fmtBody += QString("") + + QString::fromUcs4(&code, 1) + ""; + else + fmtBody += QString::fromUcs4(&code, 1); + } + + return fmtBody; +} + void TimelineItem::setupAvatarLayout(const QString &userName) { @@ -837,4 +915,4 @@ TimelineItem::openRawMessageViewer() const "failed to serialize event ({}, {})", room_id, event_id); } }); -} +} \ No newline at end of file diff --git a/src/timeline/TimelineItem.h b/src/timeline/TimelineItem.h index 8159e37..7bf6a07 100644 --- a/src/timeline/TimelineItem.h +++ b/src/timeline/TimelineItem.h @@ -26,6 +26,8 @@ #include #include +#include + #include "AvatarProvider.h" #include "RoomInfoListItem.h" #include "Utils.h" @@ -132,6 +134,8 @@ private: class TimelineItem : public QWidget { Q_OBJECT + Q_PROPERTY(QColor backgroundColor READ backgroundColor WRITE setBackgroundColor) + public: TimelineItem(const mtx::events::RoomEvent &e, bool with_sender, @@ -202,6 +206,11 @@ public: const QString &room_id, QWidget *parent); + ~TimelineItem(); + + void setBackgroundColor(const QColor &color) { backgroundColor_ = color; } + QColor backgroundColor() const { return backgroundColor_; } + void setUserAvatar(const QImage &pixmap); DescInfo descriptionMessage() const { return descriptionMsg_; } QString eventId() const { return event_id_; } @@ -222,6 +231,10 @@ signals: void eventRedacted(const QString &event_id); void redactionFailed(const QString &msg); +public slots: + void refreshAuthorColor(); + void finishedGeneratingColor(); + protected: void paintEvent(QPaintEvent *event) override; void contextMenuEvent(QContextMenuEvent *event) override; @@ -256,6 +269,9 @@ private: //! has been acknowledged by the server. bool isReceived_ = false; + QFutureWatcher *colorGenerating_; + + QString replaceEmoji(const QString &body); QString event_id_; QString room_id_; @@ -282,6 +298,8 @@ private: QLabel *timestamp_; QLabel *userName_; TextLabel *body_; + + QColor backgroundColor_; }; template diff --git a/src/timeline/widgets/ImageItem.cpp b/src/timeline/widgets/ImageItem.cpp index f06b9a5..4ee9e42 100644 --- a/src/timeline/widgets/ImageItem.cpp +++ b/src/timeline/widgets/ImageItem.cpp @@ -158,6 +158,7 @@ ImageItem::mousePressEvent(QMouseEvent *event) } else { auto imgDialog = new dialogs::ImageOverlay(image_); imgDialog->show(); + connect(imgDialog, &dialogs::ImageOverlay::saving, this, &ImageItem::saveAs); } }