remotes/origin/HEAD
Joe 6 years ago
commit ebe5b19e76
  1. 6
      .ci/install.sh
  2. 8
      .ci/linux/deploy.sh
  3. 14
      .ci/macos/deploy.sh
  4. 12
      .ci/script.sh
  5. 70
      .travis.yml
  6. 2
      CHANGELOG.md
  7. 15
      CMakeLists.txt
  8. 6
      README.md
  9. 7
      deps/CMakeLists.txt
  10. 17
      includes/jdenticoninterface.h
  11. BIN
      resources/fonts/EmojiOne/emojione-android.ttf
  12. 202
      resources/fonts/OpenSans/LICENSE.txt
  13. BIN
      resources/fonts/OpenSans/OpenSans-Bold.ttf
  14. BIN
      resources/fonts/OpenSans/OpenSans-BoldItalic.ttf
  15. BIN
      resources/fonts/OpenSans/OpenSans-ExtraBold.ttf
  16. BIN
      resources/fonts/OpenSans/OpenSans-ExtraBoldItalic.ttf
  17. BIN
      resources/fonts/OpenSans/OpenSans-Italic.ttf
  18. BIN
      resources/fonts/OpenSans/OpenSans-Light.ttf
  19. BIN
      resources/fonts/OpenSans/OpenSans-LightItalic.ttf
  20. BIN
      resources/fonts/OpenSans/OpenSans-Regular.ttf
  21. BIN
      resources/fonts/OpenSans/OpenSans-Semibold.ttf
  22. BIN
      resources/fonts/OpenSans/OpenSans-SemiboldItalic.ttf
  23. BIN
      resources/icons/emoji-categories/activity.png
  24. BIN
      resources/icons/emoji-categories/activity@2x.png
  25. BIN
      resources/icons/emoji-categories/flags.png
  26. BIN
      resources/icons/emoji-categories/flags@2x.png
  27. BIN
      resources/icons/emoji-categories/foods.png
  28. BIN
      resources/icons/emoji-categories/foods@2x.png
  29. BIN
      resources/icons/emoji-categories/nature.png
  30. BIN
      resources/icons/emoji-categories/nature@2x.png
  31. BIN
      resources/icons/emoji-categories/objects.png
  32. BIN
      resources/icons/emoji-categories/objects@2x.png
  33. BIN
      resources/icons/emoji-categories/people.png
  34. BIN
      resources/icons/emoji-categories/people@2x.png
  35. BIN
      resources/icons/emoji-categories/symbols.png
  36. BIN
      resources/icons/emoji-categories/symbols@2x.png
  37. BIN
      resources/icons/emoji-categories/travel.png
  38. BIN
      resources/icons/emoji-categories/travel@2x.png
  39. 23
      resources/res.qrc
  40. 17
      resources/styles/nheko-dark.qss
  41. 17
      resources/styles/nheko.qss
  42. 5
      resources/styles/system.qss
  43. 29
      src/Cache.cpp
  44. 6
      src/Cache.h
  45. 13
      src/ChatPage.cpp
  46. 5
      src/ChatPage.h
  47. 35
      src/MainWindow.cpp
  48. 6
      src/MainWindow.h
  49. 8
      src/RoomInfoListItem.cpp
  50. 9
      src/RoomInfoListItem.h
  51. 4
      src/RoomList.cpp
  52. 2
      src/RoomList.h
  53. 35
      src/TextInputWidget.cpp
  54. 5
      src/TextInputWidget.h
  55. 42
      src/UserSettingsPage.cpp
  56. 6
      src/UserSettingsPage.h
  57. 122
      src/Utils.cpp
  58. 19
      src/Utils.h
  59. 10
      src/dialogs/ImageOverlay.cpp
  60. 2
      src/dialogs/ImageOverlay.h
  61. 90
      src/emoji/Category.cpp
  62. 59
      src/emoji/Category.h
  63. 48
      src/emoji/ItemDelegate.cpp
  64. 43
      src/emoji/ItemDelegate.h
  65. 236
      src/emoji/Panel.cpp
  66. 66
      src/emoji/Panel.h
  67. 82
      src/emoji/PickButton.cpp
  68. 55
      src/emoji/PickButton.h
  69. 10
      src/main.cpp
  70. 84
      src/timeline/TimelineItem.cpp
  71. 18
      src/timeline/TimelineItem.h
  72. 1
      src/timeline/widgets/ImageItem.cpp

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

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

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

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

@ -1,19 +1,21 @@
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
osx_image: xcode9
env:
- DEPLOYMENT=1
- USE_BUNDLED_BOOST=0
@ -30,11 +32,8 @@ matrix:
- USE_BUNDLED_CMARK=1
addons:
apt:
sources:
- ubuntu-toolchain-r-test
packages:
- g++-5
- ninja-build
sources: ["ubuntu-toolchain-r-test"]
packages: ["g++-5", "ninja-build"]
- os: linux
compiler: gcc
env:
@ -46,11 +45,8 @@ matrix:
- USE_BUNDLED_CMARK=1
addons:
apt:
sources:
- ubuntu-toolchain-r-test
packages:
- g++-8
- ninja-build
sources: ["ubuntu-toolchain-r-test"]
packages: ["g++-8", "ninja-build"]
- os: linux
compiler: clang
env:
@ -62,57 +58,55 @@ matrix:
- USE_BUNDLED_CMARK=1
addons:
apt:
sources:
- ubuntu-toolchain-r-test
- llvm-toolchain-trusty-5.0
packages:
- clang-5.0
- g++-7
- ninja-build
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
- provider: bintray
user: "redsky17"
key:
secure: CAVzWZPxYSOTollo9bpD4tvEbfxXjqelc32aApV48GKyJrMQljQ+mvSe25BuUtnDehxnw8affgGX23AYXmvG8P7w4hM2d7//8Lgan1zCmusV8JE432jknev6X641B4cvrywqSe0Dj3l0kS9Xgirq4BGavlI0y2vUjeJfQEv0y8GYoI72LwgyH0i82v/1Qi92Fh8429IJIb0eKmC1wGWXCmo2kd8StZRL5mSlc4TmyWI0SHpA5GrLMiQwLAuD7DjDl5mpaK2yQx+H4vBcI2SUMvmlHGgVjXikJG5gURlHbnIaaBFvO67INc1/65KtMokWuMP12zxqJiaMPtsAskOpQv4FLAYDfnigH3NxufyOIGp2cxS5RhJDQhbNsxHEDnUo1kHcO23ZYNWCuC1yUdn0RXzKhWcUsz8mKF8KJs22Ty4VjfUMZ+vqK/AbHyq4rkl8DizVRZqKF1KjSWrSv/2sT4itnHk9pmcgxAYfGuALcjrJJveI4MTwDhzXB62CKnMOqLq3sAMqvE0+BdA0BykQr7qrKtptuyP2/OFx6RDbfHQl5Klkb6cSOjxm0oUzh/8iaxgsVdCrhfE67eqkhFZ+a8lJkB/rZ4zSK1Q2Cp4nLtnxenUCW+Ptk2l7zZN6kXM1/+tcgqVROChYJ6asMUpsjFOOAVQ8SZ4TcxX1rq+pxlA=
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: true
tags: false
all_branches: true
deploy:
- skip_cleanup: 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=
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
- nheko-${VERSION}-x86_64.AppImage
on:
condition: "$TRAVIS_OS_NAME == linux && $DEPLOYMENT == 1"
repo: Nheko-Reborn/nheko
tags: true
- skip_cleanup: 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
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
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=

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

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

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

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

@ -0,0 +1,17 @@
#ifndef JDENTICONINTERFACE_H
#define JDENTICONINTERFACE_H
#include <QString>
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

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 603 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 416 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 824 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 537 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 667 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 606 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 581 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 504 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1001 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 439 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 840 B

@ -63,6 +63,22 @@
<file>icons/ui/edit.png</file>
<file>icons/ui/edit@2x.png</file>
<file>icons/emoji-categories/people.png</file>
<file>icons/emoji-categories/people@2x.png</file>
<file>icons/emoji-categories/nature.png</file>
<file>icons/emoji-categories/nature@2x.png</file>
<file>icons/emoji-categories/foods.png</file>
<file>icons/emoji-categories/foods@2x.png</file>
<file>icons/emoji-categories/activity.png</file>
<file>icons/emoji-categories/activity@2x.png</file>
<file>icons/emoji-categories/travel.png</file>
<file>icons/emoji-categories/travel@2x.png</file>
<file>icons/emoji-categories/objects.png</file>
<file>icons/emoji-categories/objects@2x.png</file>
<file>icons/emoji-categories/symbols.png</file>
<file>icons/emoji-categories/symbols@2x.png</file>
<file>icons/emoji-categories/flags.png</file>
<file>icons/emoji-categories/flags@2x.png</file>
</qresource>
<qresource prefix="/logos">
<file>nheko.png</file>
@ -83,6 +99,13 @@
<file>nheko-32.png</file>
<file>nheko-16.png</file>
</qresource>
<qresource prefix="/fonts">
<file>fonts/OpenSans/OpenSans-Regular.ttf</file>
<file>fonts/OpenSans/OpenSans-Italic.ttf</file>
<file>fonts/OpenSans/OpenSans-Bold.ttf</file>
<file>fonts/OpenSans/OpenSans-Semibold.ttf</file>
<file>fonts/EmojiOne/emojione-android.ttf</file>
</qresource>
<qresource prefix="/styles">
<file>styles/system.qss</file>
<file>styles/nheko.qss</file>

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

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

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

@ -2059,6 +2059,7 @@ Cache::roomMembers(const std::string &room_id)
QHash<QString, QString> Cache::DisplayNames;
QHash<QString, QString> Cache::AvatarUrls;
QHash<QString, QString> 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();
}

@ -282,13 +282,16 @@ public:
static QHash<QString, QString> DisplayNames;
static QHash<QString, QString> AvatarUrls;
static QHash<QString, QString> 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();

@ -546,7 +546,9 @@ ChatPage::ChatPage(QSharedPointer<UserSettings> 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) {

@ -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<Collection> &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 &);

@ -17,6 +17,7 @@
#include <QApplication>
#include <QLayout>
#include <QPluginLoader>
#include <QSettings>
#include <QShortcut>
@ -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<JdenticonInterface *>(plugin);
if (jdenticonInteface_) {
nhlog::ui()->info("Found jdenticon plugin.");
return true;
}
}
}
}
nhlog::ui()->info("jdenticon plugin not found.");
return false;
}

@ -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;
};

@ -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);
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;
unreadHighlightedMsgCount_ = highlightedCount;
update();
}

@ -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)
@ -185,6 +188,7 @@ private:
bool hasUnreadMessages_ = true;
int unreadMsgCount_ = 0;
int unreadHighlightedMsgCount_ = 0;
QColor highlightedBackgroundColor_;
QColor hoverBackgroundColor_;
@ -206,6 +210,7 @@ private:
QRectF declineBtnRegion_;
// Fonts
QColor mentionedFontColor_;
QFont unreadCountFont_;
int bubbleDiameter_;

@ -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();
}

@ -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<QString, bool> &status);

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

@ -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_;
};

@ -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<UserSettings> 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<UserSettings> 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<UserSettings> 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<UserSettings> settings, QWidge
connect(themeCombo_,
static_cast<void (QComboBox::*)(const QString &)>(&QComboBox::activated),
[this](const QString &text) { settings_->setTheme(text.toLower()); });
[this](const QString &text) {
settings_->setTheme(text.toLower());
emit themeChanged();
});
connect(scaleFactorCombo_,
static_cast<void (QComboBox::*)(const QString &)>(&QComboBox::activated),
[](const QString &factor) { utils::setScaleFactor(factor.toFloat()); });
connect(fontSizeCombo_,
static_cast<void (QComboBox::*)(const QString &)>(&QComboBox::activated),
[this](const QString &size) { settings_->setFontSize(size.trimmed().toDouble()); });
connect(fontSelectionCombo_,
static_cast<void (QComboBox::*)(const QString &)>(&QComboBox::activated),
[this](const QString &family) { settings_->setFontFamily(family.trimmed()); });
connect(trayToggle_, &Toggle::toggled, this, [this](bool isDisabled) {
settings_->setTray(!isDisabled);
if (isDisabled) {

@ -18,6 +18,7 @@
#pragma once
#include <QComboBox>
#include <QFontDatabase>
#include <QFrame>
#include <QLabel>
#include <QLayout>
@ -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;
};

@ -15,6 +15,8 @@
using TimelineEvent = mtx::events::collections::TimelineEvents;
QHash<QString, QString> 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)
{

@ -14,6 +14,8 @@
#include <mtx/events/collections.hpp>
#include <mtx/events/common.hpp>
#include <qmath.h>
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);

@ -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();
}

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

@ -0,0 +1,90 @@
/*
* nheko Copyright (C) 2017 Konstantinos Sideris <siderisk@auth.gr>
*
* 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 <http://www.gnu.org/licenses/>.
*/
#include <QPainter>
#include <QScrollBar>
#include <QStyleOption>
#include "Config.h"
#include "emoji/Category.h"
using namespace emoji;
Category::Category(QString category, std::vector<Emoji> 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);
}

@ -0,0 +1,59 @@
/*
* nheko Copyright (C) 2017 Konstantinos Sideris <siderisk@auth.gr>
*
* 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 <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <QLabel>
#include <QLayout>
#include <QListView>
#include <QStandardItemModel>
#include "ItemDelegate.h"
namespace emoji {
class Category : public QWidget
{
Q_OBJECT
public:
Category(QString category, std::vector<Emoji> 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

@ -0,0 +1,48 @@
/*
* nheko Copyright (C) 2017 Konstantinos Sideris <siderisk@auth.gr>
*
* 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 <http://www.gnu.org/licenses/>.
*/
#include <QDebug>
#include <QPainter>
#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);
}

@ -0,0 +1,43 @@
/*
* nheko Copyright (C) 2017 Konstantinos Sideris <siderisk@auth.gr>
*
* 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 <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <QModelIndex>
#include <QStandardItemModel>
#include <QStyledItemDelegate>
#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

@ -0,0 +1,236 @@
/*
* nheko Copyright (C) 2017 Konstantinos Sideris <siderisk@auth.gr>
*
* 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 <http://www.gnu.org/licenses/>.
*/
#include <QPushButton>
#include <QScrollBar>
#include <QVBoxLayout>
#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());
}

@ -0,0 +1,66 @@
/*
* nheko Copyright (C) 2017 Konstantinos Sideris <siderisk@auth.gr>
*
* 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 <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <QScrollArea>
#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

@ -0,0 +1,82 @@
/*
* nheko Copyright (C) 2017 Konstantinos Sideris <siderisk@auth.gr>
*
* 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 <http://www.gnu.org/licenses/>.
*/
#include <QDebug>
#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<Panel>(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);
}

@ -0,0 +1,55 @@
/*
* nheko Copyright (C) 2017 Konstantinos Sideris <siderisk@auth.gr>
*
* 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 <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <QEvent>
#include <QTimer>
#include <QWidget>
#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> panel_;
QTimer hideTimer_;
};
} // namespace emoji

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

@ -14,6 +14,7 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include <functional>
#include <QContextMenuEvent>
#include <QDesktopServices>
@ -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<QString>(this);
connect(colorGenerating_,
&QFutureWatcher<QString>::finished,
this,
&TimelineItem::finishedGeneratingColor);
topLayout_ = new QHBoxLayout(this);
mainLayout_ = new QVBoxLayout;
messageLayout_ = new QHBoxLayout;
@ -556,6 +564,12 @@ TimelineItem::TimelineItem(const mtx::events::RoomEvent<mtx::events::msg::Text>
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<QString()> 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("<span style=\"color: #999\"> %1 </span>").arg(time.toString("HH:mm")));
}
QString
TimelineItem::replaceEmoji(const QString &body)
{
QString fmtBody = "";
QVector<uint> utf32_string = body.toUcs4();
for (auto &code : utf32_string) {
// TODO: Be more precise here.
if (code > 9000)
fmtBody += QString("<span style=\"font-family: emoji;\">") +
QString::fromUcs4(&code, 1) + "</span>";
else
fmtBody += QString::fromUcs4(&code, 1);
}
return fmtBody;
}
void
TimelineItem::setupAvatarLayout(const QString &userName)
{

@ -26,6 +26,8 @@
#include <QSettings>
#include <QTimer>
#include <QtConcurrent>
#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<mtx::events::msg::Notice> &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<QString> *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<class Widget>

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

Loading…
Cancel
Save